# Build a general-task agent from scratch (Manus lineage): follow one task, assemble it piece by piece

> **How to read this tutorial**: we do not hand you a vague checklist. We **walk one real task from start to finish**.
> The user said “summarize this URL for me”—at the start our agent can do nothing. Every time it hits a wall, we add one part; after the part is in, it takes one step forward. At the end you hold an agent that can research on its own, deliver on its own, and close on its own.
>
> **Mirrors**: at each part I will point you at what it looks like in a real project—
> OpenManus (commit `52a13f2a`, Python, the “heart”) + Kortix/Suna (`5862e956`, TypeScript, the “skeleton”). Every `file:line` can be checked directly under `参考项目/`.
>
> **Two promises**: ① every part gets an **acceptance assertion you can actually run**—not “it feels written,” but “this command went green.” ② every part tells you **the crash it is most likely to have**.
>
> **Assumed reader**: a beginner with a little programming. Python / TypeScript both work. Pseudocode in the text stays close to OpenManus naming so you can compare.

---

## Contents

- [See the finish line first: the thing we assemble looks like this](#see-the-finish-line-first-the-thing-we-assemble-looks-like-this)
- [Step 0 · The task enters: one URL, one summary](#step-0--the-task-enters-one-url-one-summary)
- [Step 1 · Give it a brain (but it can only speak)](#step-1--give-it-a-brain-but-it-can-only-speak)
- [Step 2 · Give it a hand (but who actually acts?)](#step-2--give-it-a-hand-but-who-actually-acts)
- [Step 3 · Make it turn: the heartbeat loop](#step-3--make-it-turn-the-heartbeat-loop)
- [Step 4 · Teach it to clock out: terminate](#step-4--teach-it-to-clock-out-terminate)
- [Step 5 · Give it memory (the model is actually amnesiac)](#step-5--give-it-memory-the-model-is-actually-amnesiac)
- [Step 6 · Give it a manual: turn a general model into a specialist](#step-6--give-it-a-manual-turn-a-general-model-into-a-specialist)
- [Step 7 · Make it actually research: search → open → extract](#step-7--make-it-actually-research-search--open--extract)
- [Step 8 · Make the deliverable look presentable: templates](#step-8--make-the-deliverable-look-presentable-templates)
- [Step 9 · Fit the brakes: network down, over budget, stuck](#step-9--fit-the-brakes-network-down-over-budget-stuck)
- [Step 10 · Clone a new specialist: swap only the manual](#step-10--clone-a-new-specialist-swap-only-the-manual)
- [🎬 Full replay: how this task runs from start to finish](#-full-replay-how-this-task-runs-from-start-to-finish)
- [Gate 2 · From “can work” to “can work a lot, safely, together”](#gate-2--from-can-work-to-can-work-a-lot-safely-together)
  - [Pain A · Two tasks overwrite each other’s files → workstation isolation](#pain-a--two-tasks-overwrite-each-others-files--workstation-isolation)
  - [Pain B · Secrets leak into logs → secret gateway](#pain-b--secrets-leak-into-logs--secret-gateway)
  - [Pain C · A broken prompt must be revertible → change discipline](#pain-c--a-broken-prompt-must-be-revertible--change-discipline)
  - [Pain D · Trigger from phone / Slack → channel delivery](#pain-d--trigger-from-phone--slack--channel-delivery)
- [Appendix 1 · Schedule: string these steps into a rhythm](#appendix-1--schedule-string-these-steps-into-a-rhythm)
- [Appendix 2 · Golden tasks: the machine-check list before you ship](#appendix-2--golden-tasks-the-machine-check-list-before-you-ship)
- [Appendix 3 · Ten ways it most easily blows up](#appendix-3--ten-ways-it-most-easily-blows-up)
- [Appendix 4 · Comparative reading index](#appendix-4--comparative-reading-index)

---

## See the finish line first: the thing we assemble looks like this

Before you start, put the finish line in front of you. The figure below is what we will assemble piece by piece. **Green** is “Gate 1”—make it able to work; **blue** is “Gate 2”—make it able to work a lot, safely, with other people.

```mermaid
flowchart TB
    U(["🧑 用户：帮我总结这个网址"]) --> BRAIN

    subgraph L1["第一关 · 让它能干活（单机就够）"]
        BRAIN["🧠 大脑<br/>大模型，只会输出文字"]
        HAND["🖐️ 手 / 工具<br/>读网页 · 写文件 · 搜索"]
        LOOP["🔁 心跳循环<br/>想一步 → 做一步 → 看结果"]
        MEM["📓 记忆<br/>每圈把经过重新喂回去"]
        BOOK["📜 说明书<br/>把通用模型变成专员"]
        BELL["🔔 下班铃 terminate<br/>做完就显式收尾"]
        BRAIN --- LOOP
        HAND --- LOOP
        MEM --- LOOP
        BOOK --- LOOP
        BELL --- LOOP
    end

    LOOP --> OUT(["📄 workspace/summary.md<br/>交付物落盘"])

    subgraph L2["第二关 · 能安全地一起干很多活（要上线才需要）"]
        ISO["🚧 工位隔离<br/>一个任务一个工位，互不踩"]
        GATE["🔐 密钥网关<br/>Agent 永远看不到真钥匙"]
        GIT["📋 变更纪律<br/>改坏了能一键回滚"]
        CH["📡 渠道投递<br/>手机 / Slack 也能触发"]
    end

    OUT -. 当你要上线 .-> L2

    style L1 fill:#e7f8f1,stroke:#12b981
    style L2 fill:#eaf1ff,stroke:#2f6bff
    style U fill:#fff3df
    style OUT fill:#fff3df
```

One sentence for the whole figure:

> **An agent that can work on its own = a brain + hands + a loop that keeps turning, plus memory, a manual, and a clock-out bell. When you go live, add: workstations, a key vault, rollback, and multiple entries.**

This entire tutorial is filling that figure, step by step, from the leftmost “can do nothing” state.

---

## Step 0 · The task enters: one URL, one summary

First nail the task we will run alongside. The whole tutorial turns around it:

> **Script α**: the user throws a URL and says “summarize https://example.com for me, write it as an abstract.”
> The result we want: `workspace/summary.md` lands on disk (≥200 characters, includes the original URL), then the agent **itself says “I am done.”**

Why nail one concrete task first? Because **“whether this one script can run through” is the only standard that inspects every later part**. Any capability that sounds impressive, if it cannot run this most plain script, is a castle in the air.

While we are here, say clearly what we are **not** building, so the work does not drift:

| | What we build (Manus lineage: general task / research / deliverable) | Claude Code (coding agent) |
|---|---|---|
| What success looks like | A deliverable on disk (an MD / HTML / table) | A code patch merged + tests green |
| Main “hands” | Search / browser / fetch a page | File tree / grep / code jump |
| How it closes | Explicitly call `terminate`, state “I am done” | Often relies on “task complete” text or a test gate |
| One session | One-shot job: give a task, hand something in | Long session: keep editing inside one repo |

> 🔑 **Copy the table above into your README.** It is the referee every time you later wrestle with “should I build this feature”—if it does not serve “research + deliver,” put it down for now.

Right now our agent is a blank page. The user’s sentence has already been said. Start adding parts.

---

## Step 1 · Give it a brain (but it can only speak)

**Where the task is stuck**: the user said “summarize this URL,” and we have nothing in our hands. To let it “understand” that sentence and “think,” the first thing is to attach a large model—that is its **brain**.

**Intuition**: the brain is responsible for thinking and deciding. But remember one counter-intuitive fact that will run through the whole tutorial—**the only thing the brain can do is emit text**. It cannot read your files, cannot run commands, cannot even go online by itself. After you attach it, the most it can do is chat with you about “you should go read that URL”—it cannot touch a single byte.

So this step we build only one thing: a client that **hands speech to the large model and takes the reply back**.

### What you write: the single entry `chat`

```text
chat(messages, tools, *, system=None, tool_choice="auto") ->
  { content: str | None,                      # 模型说的话
    tool_calls: [{id, name, arguments: dict}]  # 模型想调用的工具（可能为空）
  }
```

- `messages`: the dialogue history so far (who said what).
- `tools`: this turn, tell the model “which tools you have in your hands” (the next step will use this; leave the slot now).
- `tool_calls[].arguments`: **must already be a dictionary after `json.loads`**. Do not pass the raw string downstream—this is one of the most common blow-ups.

### Compare with real source

OpenManus’s entry at this layer is `LLM.ask_tool` (`app/llm.py`). Three details are worth copying its wisdom:

- **Give timeout enough room**: default 300 seconds (`llm.py:648`). Research tasks often wait a long time for the model to think; a timeout that is too short will kill it by mistake.
- **Do not retry quota errors in a death loop**: network jitter can retry, but an error such as “token exceeded” will not help no matter how many times you retry. OpenManus specifically comments Don’t retry `TokenLimitExceeded` (`llm.py:641-642`).
- **Empty replies must error**: the model occasionally returns empty `choices`; then you must throw explicitly (`llm.py:737-740`). You **must not** silently treat it as “nothing to say”—otherwise the loop will spin empty.

### Acceptance (after this step, run these)

```text
断言 1：不给任何工具，纯聊天，能拿到 content。
断言 2：给它一个「结束工具」的说明，然后 prompt「请直接结束」，它能返回一个 name=terminate 的 tool_call。
断言 3：故意填错 API key → 抛错或记下可读的错误，绝不静默空转。
```

### How it most easily blows up

- Pass `arguments` to the tool as a raw string → execution downstream will explode. **`json.loads` it at this client layer.**
- Reach for “streaming” immediately, then execute a tool call whose arguments arrived only half-formed → for Gate 1, first use **non-streaming**, get a complete reply, then talk about the rest.

> 🧠 **One sentence**: the brain can only speak. It can give you a perfect suggestion, and it cannot change a single byte—so next, we have to give it a hand.

---

## Step 2 · Give it a hand (but who actually acts?)

**Where the task is stuck**: the brain understood “summarize the URL.” In its head it says “I have to fetch this page first.” But—**it cannot read it itself**. Who goes and reads?

**Intuition**: this is the most critical, and most easily misunderstood, point in the whole agent; it deserves its own figure. The model **does not execute anything**. It only says a sentence: “I want to call `fetch_url`, with this URL as the argument.” What actually goes and reads the page is **the program we wrote outside the model**—we call it the harness.

```mermaid
flowchart LR
    subgraph BRAIN["🧠 大脑（模型）· 只会说话"]
        SAY["「我想调用<br/>fetch_url(url=...)」"]
    end
    subgraph HARNESS["🖐️ harness（我们写的外壳）· 真正动手"]
        RUN["真的去把网页读回来"]
        BACK["把读到的内容<br/>包成一条「观察」塞回历史"]
    end
    SAY -->|"模型只是'点单'"| RUN
    RUN --> BACK
    BACK -->|"下一圈，模型才'看见'结果"| SAY
    style BRAIN fill:#eef2f7
    style HARNESS fill:#eaf1ff
```

Remember the division of labor: **the model is responsible for “decide,” the harness is responsible for “execute.”** The model orders; the kitchen plates. That boundary is the root of agent safety—because the power to “approve or not approve doing a thing” always sits in the harness’s hand; the model cannot steal it (Gate 2 will use this).

### What a tool looks like: one mold for all

Every “hand” is a tool object that looks the same:

```text
Tool:
  name: str                 # 模型按这个名字点单
  description: str          # 告诉模型「这只手能干嘛、什么时候用」
  parameters: JSONSchema    # 规定参数格式
  async execute(**kwargs) -> ToolResult   # 真正干活的函数

ToolCollection（工具箱）:
  to_params()               # 把所有工具的「说明书」打包给模型看
  execute(name, arguments)  # 按名字找到对应工具、跑它
```

Compare OpenManus: `BaseTool.to_param` (`app/tool/base.py:124-137`) turns a tool into a format the model can read; `ToolCollection.execute` (`tool_collection.py:25-35`) dispatches by name.

### First build the two plainest hands; script α is enough

**Hand one: `fetch_url` — read a URL**

```text
parameters: { url: string, max_chars?: number = 20000 }
行为:
  - 只允许 http / https
  - 超时 15 秒
  - 剥掉 <script>/<style>，正文转成干净文本
  - 太长就切断，并注明「已截断」
失败: 返回 ToolResult(error="...")，不要抛异常炸穿循环
```

**Hand two: `write_file` — write the summary to disk**

```text
parameters: { path: string, content: string }
行为:
  - 算出真实路径后，必须仍在 workspace 目录内（防止 ../../ 穿越出去）
  - 自动创建父目录
  - 返回「已写入 N 字节到 ...」
失败: 路径穿越 / 磁盘错误 → 返回 error 字符串
```

### One iron rule: tool failure must become an “observation”; it must not blow up the whole task

This is the core of why a research agent can survive dirty pages and bad links. When you execute any hand, walk this pipe:

```mermaid
flowchart TD
    CALL["模型点单：调用某个工具"] --> N{"这个工具存在吗？"}
    N -->|"不存在"| E1["返回一句 Error 文本"]
    N -->|"存在"| J{"参数能解析成 JSON 吗？"}
    J -->|"不能"| E2["返回一句 Error 文本"]
    J -->|"能"| X{"执行时抛异常了吗？"}
    X -->|"抛了"| E3["返回一句 Error 文本 + 记日志"]
    X -->|"没抛"| OK["返回执行结果"]
    E1 --> OBS["全都变成一条『观察』<br/>写回对话历史"]
    E2 --> OBS
    E3 --> OBS
    OK --> OBS
    OBS --> NEXT["模型下一圈看到它，自己换招"]
    style OBS fill:#fff3df
    style NEXT fill:#e7f8f1
```

**Principle: tool failure, by default, becomes an observation text fed back to the model, so it can change tactics next turn.** Do not write it as “one failure blows up the whole task.” Compare OpenManus’s `execute_tool` (`toolcall.py:166-208`)—unknown tool, JSON parse failure, execution exception, all are collected into an `Error: ...` string and returned, not thrown out.

### Acceptance

```text
断言：调用一个不存在的工具 → 循环继续，历史末尾是一条 error 观察。
断言：给 write_file 传 path="../../etc/passwd" → 返回 error，且你电脑上那个文件没被动。
断言：工具箱的 to_params() 能被真实的 OpenAI SDK 接受（拿真 API 打一轮）。
```

### How it most easily blows up

- `write_file` does not validate paths → one sentence from the model can write a file anywhere on your whole disk. **After you compute the absolute path, you must confirm it is still inside workspace.**
- The tool `raise`s on first failure → one bad link can blow up the whole task. Soft-fail, always soft-fail.

> 🖐️ **One sentence**: the model orders, the harness plates. Tool failure is not an accident; it is “one observation”—let the model change tactics itself.

---

## Step 3 · Make it turn: the heartbeat loop

**Where the task is stuck**: now the brain can think and the hand can act. They are not yet connected—the brain says “read the page,” the hand finishes reading, and then? The fetched content has to **be shown to the brain again**, so it can decide “is it enough? should I keep going?” That circle of “think → do → see the result → think again” is the agent’s **heartbeat**.

**Intuition**: do not make it complicated. Its core is a `while` loop:

> **As long as the model still wants to use a tool, the loop does not stop; the moment the model does not call a tool and just speaks, the task is over.**

### Skeleton of the loop

```mermaid
flowchart TD
    START(["进入循环：历史里已经有用户那句话"]) --> GATE{"还没超步数上限？<br/>而且还没喊停？"}
    GATE -->|"否"| BUDGET["预算耗尽 → 收尾走人"]
    GATE -->|"是"| THINK["🧠 think：把历史发给模型<br/>问它下一步干嘛"]
    THINK --> Q{"模型这一圈<br/>调工具了吗？"}
    Q -->|"没调，只说了话"| MAYBE["它是不是想收尾了？<br/>（见第 4 步）"]
    Q -->|"调了"| ACT["🖐️ act：真去执行工具<br/>把结果写回历史"]
    ACT --> STOP{"刚才那个工具<br/>是 terminate 吗？"}
    STOP -->|"是"| FIN["翻状态 = 完成 → 退出"]
    STOP -->|"否"| GATE
    MAYBE --> GATE
    style THINK fill:#eef2f7
    style ACT fill:#eaf1ff
    style FIN fill:#e7f8f1
```

### Compare with real source: three thin layers stacked

OpenManus’s heart is not some 2000-line mega-function. It is three small files stacked:

| Layer | What it does | Source |
|---|---|---|
| `BaseAgent.run` | Outer shift: gate check → loop → cleanup | `base.py:116-154` |
| `ReActAgent.step` | Split one step into `think()` + `act()` | `react.py:33-38` |
| `ToolCallAgent.think / act` | think asks the model, act executes tools | `toolcall.py:39-164` |

`step` is short enough to understand at a glance:

```python
# react.py:33-38
async def step(self) -> str:
    should_act = await self.think()          # 想：要不要动手？
    if not should_act:
        return "Thinking complete - no action needed"
    return await self.act()                  # 做：真去执行
```

The outer `run` is also a plain while (`base.py:116-154`; here we grab the skeleton):

```python
if self.state != AgentState.IDLE:            # ① 门禁：非空闲不许跑
    raise RuntimeError(...)
if request:
    self.update_memory("user", request)      # ② 把用户那句话记进历史
while self.current_step < self.max_steps and self.state != AgentState.FINISHED:
    self.current_step += 1
    await self.step()                        # ③ 想一步、做一步
    if self.is_stuck():                      # ④ 卡住了？（第 9 步讲）
        self.handle_stuck_state()
```

### Three disciplines you must copy

1. **Gate**: if the state is not “idle,” it may not start (`base.py:128-129`). Prevents the same agent from being triggered concurrently at random.
2. **Tools execute serially**: if the model ordered several tools in the same turn, **one by one**, do not concurrent (`toolcall.py:141`). Gate 1 wants stability, not speed.
3. **Cleanup must always run**: success, failure, or exception, browser and connections and those resources must be cleaned at the end (`cleanup` at `toolcall.py:229-250`).

### On the “step budget”

Giving the loop a `max_steps` is the safety rope that **stops it from burning money forever**. In OpenManus, Manus defaults to 20 steps (`manus.py:27-28`).

- Running golden tasks: set 12–20 steps.
- Want to test the “budget exhausted” path: deliberately set it to 3 steps.

Compare other projects and you will see what magnitude 20 is: some agents default to 40, even 200 steps. OpenManus’s 20 steps reveal its position—**one “homework shift.”** Long tasks get another planning layer outside, rather than stretching one loop to hundreds of steps.

### Acceptance

```text
断言：max_steps=1，任务又确实需要用工具 → 结果是「预算耗尽」，行为稳定可复现。
断言：同一圈里模型点了两个工具 → 它们按顺序执行（看日志时间戳是递增的）。
断言：清理函数在「成功 / 失败 / 抛异常」三条路径下都会跑到（用一个计数器 mock 验证）。
```

### How it most easily blows up

- The loop has no step budget → the model can wander into a dead end and burn your wallet empty.
- Week one you already want “several tools in parallel” → concurrency bugs will keep you debugging at the moment you most need to verify the main logic. **Serial first.**

> 🔁 **One sentence**: think one step, do one step, stuff the result back, think again—this circle turning, turn after turn, is the agent’s entire life.

---

## Step 4 · Teach it to clock out: terminate

**Where the task is stuck**: the loop is turning. But—how does it know “the summary is written, time to stop”? If nobody tells it “stop,” it will keep turning until the step budget is gone, wasting several turns for nothing.

**Intuition**: you might think “the model stopped calling tools and started speaking; that means it is done”—**exactly not**. This is a pit that is especially easy to step in.

Look at OpenManus’s real behavior (`toolcall.py:118-119, 133-138`): in the default mode, when the model only speaks and does not call a tool in a turn, the loop **does not** therefore consider the task finished; it keeps turning. That is: **“the model started speaking human” ≠ “clocked out.”**

So we need an explicit clock-out bell: a **tool** named `terminate`. The model must **actively call it** before the job is formally closed.

```mermaid
flowchart LR
    A["模型这一圈<br/>调用了 terminate(status)"] --> B["harness 执行它"]
    B --> C["翻转状态<br/>state = FINISHED"]
    C --> D["下一次循环判断条件<br/>不满足 → 退出"]
    style A fill:#eef2f7
    style C fill:#e7f8f1
```

Notice a subtlety: the `terminate` tool **itself almost does nothing**—its `execute` only returns a sentence “ended, status xxx” (OpenManus `terminate.py:23-25`). **What actually stops the loop is the harness detecting “this is a special tool” and flipping state to FINISHED** (`toolcall.py:210-227`). The tool is only a signal; flipping state is the real action.

### What you write: the `terminate` contract

```json
{
  "name": "terminate",
  "description": "任务完成、或你无法继续时，调用它来结束工作。",
  "parameters": {
    "type": "object",
    "properties": {
      "status": { "type": "string", "enum": ["success", "failure"] },
      "summary": { "type": "string", "description": "给用户的一段话，说清你干了什么" }
    },
    "required": ["status"]
  }
}
```

Compare OpenManus `app/tool/terminate.py`—it has only the one enum field `status`. We add a `summary` on top of that, so when the model closes it can hand the user one sentence of human speech.

### A tool is not enough; you have to teach it in the “manual”

The model will not be born knowing “after you write the file, call terminate.” You have to say it in the system prompt (step 6 will expand this; remember this hard rule first):

> When the thing to deliver already exists under `workspace/`, you **must** call `terminate(success)`. Do not keep chatting.

Compare OpenManus’s prompt (`prompt/manus.py:9`): “If you want to stop the interaction at any point, use the `terminate` tool.”

### Acceptance

```text
断言：prompt「什么都别做，直接结束」→ 头一两步内就出现 terminate 调用。
断言：terminate 之后不再发起任何模型调用（看日志，步数停住了）。
断言：status=failure 时，进程的退出码 ≠ 0（或返回结果里 ok=false）。
```

### How it most easily blows up

- Expect “the model went quiet = finished” → it will empty-spin until the budget is gone. **You must have explicit terminate.**
- Treat terminate’s “return value” as the stop signal → stopping depends on **flipping state**, not on that return text.

> 🔔 **One sentence**: clocking out requires punching the card. The model saying “I am done” does not count; it has to press the terminate bell with its own hand.

---

## Step 5 · Give it memory (the model is actually amnesiac)

**Where the task is stuck**: when the loop reaches turn 2, the model has to write the summary “based on the page content it read in turn 1.” Here is the problem—**every reply from the model is amnesiac**; it does not remember what happened last turn. How does turn 2 pick up?

**Intuition**: we **re-feed, every turn, the entire history so far, complete**. Your question, the pages it read, every tool result… sent again, unchanged. That blob is called **context**. The model’s “memory” is an illusion; we feed it by hand every turn.

```mermaid
flowchart TD
    subgraph MEM["📓 记忆 = 一个消息列表，越滚越长"]
        direction TB
        M1["user：帮我总结这个网址"]
        M2["assistant：我要调 fetch_url"]
        M3["tool：（网页正文）"]
        M4["assistant：我要调 write_file"]
        M5["tool：已写入 summary.md"]
    end
    MEM -->|"每一圈，把整个列表<br/>重新打包发给模型"| LLM["🧠 模型"]
    LLM -->|"它的新回复<br/>又追加到列表末尾"| MEM
    style MEM fill:#eef2f7
    style LLM fill:#e7f8f1
```

### What you write: Message + Memory

```text
Message（一条消息）:
  role: system | user | assistant | tool   # 谁说的
  content: str | None                       # 说了什么
  tool_calls?: [...]                         # assistant 发起的工具调用
  tool_call_id? / name?                      # tool 回执用
  base64_image?: str                         # 可选，调研看图时用

Memory（记忆本）:
  messages: list[Message]
  max_messages: int = 100                    # 上限
  add_message() / clear() / get_recent(n)
```

Compare OpenManus `app/schema.py:54-62` (Message) and `:159-187` (Memory).

### What if memory will not fit? Gate 1: hard cut

The model’s context has a capacity limit. History rolls longer and longer; eventually it will not fit. The plainest method is **keep only the last N messages**:

```python
# schema.py:167-168
if len(self.messages) > self.max_messages:
    self.messages = self.messages[-self.max_messages:]
```

**Hard cut, no summary**—OpenManus does exactly this at Gate 1, default keep the last 100 (`schema.py:161`). Do not rush to the advanced game of “use a large model to compact history.” Get it running first.

### A discipline table for “what goes into memory”

This table is where beginners most easily dirty the context. Follow it:

| Content | Into memory? | What OpenManus does |
|---|---|---|
| system prompt (the manual) | **Usually not** | Passed separately as a parameter every turn, not stuffed into history (`toolcall.py:47-53`) |
| The user’s task | Yes | At start, `update_memory("user", ...)` |
| Tool calls the model initiated | Yes | `from_tool_calls` |
| Observations the tools returned | Yes | `Message.tool_message` |
| Each turn’s “this-turn reminder” | Yes, but be careful | See the blow-up below |

### How it most easily blows up (OpenManus itself stepped in this pit)

Every turn OpenManus **appends** a “this-turn reminder” (the next_step sticky note) **into memory** (`toolcall.py:41-43`). Run 20 turns and history may be stacked with nearly 20 almost identical sticky notes, eating the 100-message quota for nothing.

Your improvement, pick one:

1. **Recommended**: this-turn reminder is sent to the model only as a “temporary message,” **not** written permanently into memory.
2. Writing it in is also fine, but raise the cap to 200, and mark “truncation happened” in the log.

Two more pits:

- The same agent instance runs two tasks in a row, you forgot `memory.clear()` → the second task will “taste of the first,” reading the first task’s history (when OpenManus exhausts the budget it resets the step count, but **does not clear memory**, `base.py:149-152`).
- Large blobs such as browser screenshots stacked directly into memory → tokens explode fast.

### Acceptance

```text
断言：连续跑 30 步后，记忆条数 ≤ max_messages。
断言：同一个 Agent 实例第二次跑之前，必须先 memory.clear()——否则串味。
断言（若你选了「说明书不进记忆」策略）：导出记忆，里面不该有 role=system 的条目。
```

> 📓 **One sentence**: the model is amnesiac. That it can pick up each turn is entirely because we re-feed the whole history—that is context, and it is also why long conversations get slower and more expensive.

---

## Step 6 · Give it a manual: turn a general model into a specialist

**Where the task is stuck**: by here the agent can turn, can use tools, has memory. It is still a “general model”—if you do not tell it “who you are, what you have in your hands, how you should work,” it may finish reading the page and start chatting, not know it should write a file, still less know that after writing it should terminate.

**Intuition**: the secret is a **manual**—a paragraph written dead at the very start of the conversation, in the jargon a system prompt. The same large model, swap one manual, can become the one that writes summaries, the one that books itineraries, or the one that does data analysis. The manual decides its “persona” and its way of working.

A Manus-lineage manual usually has two layers, with different jobs:

| Layer | What it governs | How often it changes |
|---|---|---|
| **system prompt** (the job handbook) | Identity, working directory, delivery contract, safety floor | One per specialist, almost never changes |
| **next_step prompt** (this-turn sticky note) | This turn’s strategy reminder, current browser state | Can change almost every step |

Compare OpenManus: Manus’s `SYSTEM_PROMPT` carries a `{directory}` placeholder (`prompt/manus.py`); `NEXT_STEP_PROMPT` teaches the model how to pick tools and not forget terminate.

### What you write: Gate 1’s system template (usable as-is)

```text
你是 {name}，一个「调研并交付」的 Agent。
工作目录：{workspace 的绝对路径}
你只能在这个目录下面产出文件。

本次要交付的东西：
{deliverable_spec}   # 例：一份 summary.md，≥200 字，含原网址

规矩：
1. 优先用工具，别靠猜。缺数据就去抓、去搜。
2. 交付物在硬盘上出现后，立刻调 terminate(status="success", summary=...)。
3. 卡住了（断网/付费墙），调 terminate(status="failure", summary=...)。
4. 绝不要把 API key 写进任何文件或摘要里。
```

This template formally writes step 4’s hard rule—“after writing, you must terminate”—into the manual.

### Why two layers?

Because some reminders are “identity that never changes” (who you are, which directory you may write) and some are “the temporary situation of this second” (which page the browser is on now). Mix them in one file and changing one disturbs the other. Split them and you can swap only one layer—when step 10 “clones a new specialist,” you will see that **swapping only the system layer produces a brand-new specialist, without changing one line of loop code**.

### Acceptance

```text
断言：同一套循环，只换 system prompt 文件，行为就从「写摘要」变成「订行程」。
断言：日志能打印出本轮实际用的 next_step 是哪一版，方便回放调试。
```

> 📜 **One sentence**: the manual decides the persona. Swap one manual and the same brain goes from “chat contestant” to “your specialist.”

---

## Step 7 · Make it actually research: search → open → extract

**Where the task is stuck**: in script α the user handed over a URL; `fetch_url` is enough. Real research tasks are often “look up which sights Kyoto has”—**there is no ready URL**. The agent has to **search** on its own, **open** the most relevant from the results, then **extract** useful information. This closed loop of “search → open → extract” is the Manus lineage’s **identity papers** relative to a coding agent.

**Intuition**: think of it as the three moves a person makes researching on the web—first search keywords with a search engine, click the most credible link, read it and write down the points. Missing any one ring is not research.

```mermaid
sequenceDiagram
    participant A as 🧠 Agent
    participant S as 🔍 搜索
    participant P as 🌐 网页
    participant L as 🧠 二次抽取

    A->>S: ① 搜「京都 必去景点」
    S-->>A: 一串标题 + 链接
    A->>P: ② 打开最相关的那条链接
    P-->>A: 网页正文（一大坨 HTML/文本）
    A->>L: ③ 「从这坨正文里，抽出对'景点'有用的要点」
    L-->>A: 结构化要点（干净的清单）
    Note over A: 信息还不够？<br/>换个更窄的关键词，再来一轮
    A->>A: 够了 → 写交付物 → terminate
```

### Contracts of the three stages

| Stage | Input | Output | How to implement |
|---|---|---|---|
| **Search** search | Keywords | Title + link list (≥3 items) | Attach a search API; advanced can do “multi-engine cascade fallback” |
| **Open** open | One link | Page body | Step 2’s `fetch_url` is enough; advanced uses a real browser |
| **Extract** extract | Goal + body | Structured points | **Call the model once more** (no tools, pure distillation), or use simple rules |

Compare OpenManus: this closed loop hides in the action chain of the `browser_use` tool (`browser_use_tool.py`). One key detail is worth noticing—its `web_search` action **searches and then jumps straight to the first result** (`browser_use_tool.py:251-268`), gluing “search” and “open” together. And the “extract” step (`extract_content`, `:375-444`) **calls the model once more inside the tool** to distill the body—“a model nested inside a tool,” a common method for raising research quality, at the cost of slower and more tokens burned.

### You can run the loop without a real browser first

Do not reach for Playwright on day one. First use the plainest four-piece set to prove the closed-loop semantics are connected:

```text
web_search(query)         -> [{title, url}]     # 搜
fetch_url(url)            -> 干净正文            # 开（复用第 2 步）
extract_facts(goal, text) -> 要点清单           # 抽（内部再调一次模型）
write_file(...) + terminate                     # 交付 + 收尾
```

First prove “search → open → extract → deliver” strings together, then consider swapping in a real browser.

> ⚠️ **When you go to a real browser, notice**: OpenManus defaults `disable_security=True` (`browser_use_tool.py:144`)—it turned off the browser’s security restrictions to save trouble. **This configuration must never be taken to a public deployment**; before you go live you must turn it off and audit yourself.

### Acceptance

```text
断言：跑一个「查城市景点」的任务，日志里至少有 1 次 search、1 次 open、1 次 write_file、1 次 terminate。
断言：产出的文件里 ≥ 3 个真实链接。
断言：故意让搜索 API 失效 → 得到一条 error 观察，Agent 要么换引擎成功、要么最终 terminate(failure)，不许空转。
```

> 🔍 **One sentence**: search, open, extract; missing one is not research. This closed loop is where a “general-task agent” differs from a “chatbot.”

---

## Step 8 · Make the deliverable look presentable: templates

**Where the task is stuck**: if the task must produce HTML (for example a travel booklet), letting the model **hand-write HTML + CSS from zero** has two problems: it wastes several turns writing styles; and the structure it generates is different every time, so you cannot automatically check whether it is right.

**Intuition**: split the deliverable into “**a fixed shell** + **the meat the model fills**.” You write styles and skeleton dead; the model is only responsible for filling content. Faster, more stable, easier to machine-check.

```text
render_itinerary(city, places: [{name, url, note}]) -> 写出 workspace/itinerary.html
```

The model is only responsible for giving the `places` data. **It does not touch CSS**. The shell looks like this (styles written dead; the model is forbidden to change them):

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"/><title>{{city}} 行程</title>
  <style>/* 固定样式 */ body{font-family:sans-serif;max-width:720px;margin:2rem auto}</style>
</head>
<body>
  <h1>{{city}} 一日游</h1>
  <section id="places">{{places_html}}</section>
  <footer>Generated by my-agent</footer>
</body>
</html>
```

This step does not need a figure—it is essentially “fill in the blanks”; a piece of code is clearer than a picture.

### Acceptance

```text
断言：输出的 HTML 里含那句固定的 footer 字符串。
断言：places ≥ 3，且每一项都带 url。
```

> 🧱 **One sentence**: you decide the shell; it fills the meat. Do not let the model waste effort writing CSS.

---

## Step 9 · Fit the brakes: network down, over budget, stuck

**Where the task is stuck**: so far we have only considered “everything goes well.” In reality the network dies, the budget overruns, the model gets stuck on the same thought and cannot get out. Without brakes, these cases make the agent either crash or empty-spin until the budget is burned.

**Intuition**: besides the normal terminate, the agent also needs several ways to “clock out abnormally.”

### Besides terminate, three more clock-out bells

| Situation | Outcome | Compare |
|---|---|---|
| The model called `terminate` | Success / failure close | `toolcall.py:210-218` |
| Step count filled `max_steps` | Budget exhausted | `base.py:149-152` |
| Token cumulative over limit | Budget exhausted | `toolcall.py:60-72` |
| The user hit Ctrl-C | Cancel | Each entry catches it |

### “Stuck” detection: the model started repeating

Sometimes the model will say the exact same thing for several turns in a row—it is stuck in one thought and cannot get out. OpenManus’s detection is plain (`base.py:163-186`): **if this latest assistant message has appeared, character-for-character identical, ≥2 times among earlier ones, judge stuck**.

The handling is also restrained—**do not force-terminate**; only add one sentence to the “this-turn reminder,” forcing it to change tactics:

> “Noticed you are repeating. Change strategy; do not walk a dead road you already tried.”

Compare `handle_stuck_state` (`base.py:163-168`). You can strengthen: stuck 3 times in a row and still not changing, then terminate(failure).

### Leave one log line per step

Debugging depends on it. Drop one JSON line per step (one line, one step):

```json
{"step":3, "phase":"act", "tool":"fetch_url", "ok":true, "ms":812, "args":{"url":"https://..."}}
```

**Never print a complete secret or a whole page of original text into the log** (truncate to 500 characters). This log piece does not get a figure—it is line after line of text; a table and an example are clearer than a picture.

### Acceptance

```text
断言：断网跑剧本 α → 日志里出现 ok:false；进程不无限 while。
断言：max_steps=3 → 结局是「预算耗尽」，且 workspace 里可以有半成品。
断言：人为让模型复读同一句话 → 出现「卡住」日志，或本轮提醒里加进了那句换招提醒。
```

> 🛑 **One sentence**: network down, over budget, repeating—you need all three brakes, or the agent will crash or empty-spin and burn money.

---

## Step 10 · Clone a new specialist: swap only the manual

**Where the task is stuck**: the summary specialist is built. Now the boss says “give me one that books itineraries.” Do you have to rewrite the whole loop, tools, memory?

**Intuition**: not at all. Every earlier part—loop, memory, tool pipe, terminate—is **generic**. To build a new specialist you only need to swap three things: **manual + tool subset + deliverable spec**. Not one line of loop code changes.

```text
AgentSpec（一个专员 = 一张配置）:
  name: str
  system_prompt: str        # 换这个
  tools: [工具名子集]        # 换这个
  deliverable_spec: str     # 换这个
  max_steps: int

loop.run(spec, user_request)  # ← 唯一的循环实现，所有专员共用
```

Two specialists, one loop:

| Specialist | Toolbox | Deliverable |
|---|---|---|
| Summary specialist | fetch_url, write_file, terminate | summary.md |
| Itinerary specialist | web_search, fetch_url, render_itinerary, terminate | itinerary.html |

Compare OpenManus: it swaps varieties by “inheritance” (`BrowserAgent` / `DataAnalysis` are each a subclass, swapping prompt + toolbox). For your Gate 1, “configuration” is enough, lighter than inheritance.

### Acceptance (this is Gate 1’s graduation exam)

```text
断言：git diff 显示循环那个文件『零改动』，只新增了 specs/ 和 prompts/ 下的文件。
断言：两个专员都能独立跑通各自的黄金题。
```

### Deliberately do not do yet

- A “planning flow” where several agents dispatch each other—OpenManus itself marked it unstable; Gate 1 does not touch it.
- Moving the whole MCP plugin ecosystem in—when you need it, then talk.

> 👥 **One sentence**: the loop is the generic chassis; a specialist is just a swapped manual. That is why adding a new specialist is almost zero cost.

---

## 🎬 Full replay: how this task runs from start to finish

The parts are all in. Now run step 0’s task—“summarize https://example.com for me”—all the way through, and you will see how each earlier part meshes.

```mermaid
sequenceDiagram
    participant U as 🧑 用户
    participant LP as 🔁 循环
    participant Brain as 🧠 模型
    participant Hand as 🖐️ 工具
    participant Disk as 📄 workspace

    U->>LP: 帮我总结 https://example.com，写成摘要
    Note over LP: 把这句话记进「记忆」

    rect rgb(238, 242, 247)
    LP->>Brain: 第1圈：说明书 + 记忆（就这一句）
    Brain-->>LP: 我要调 fetch_url(url=example.com)
    LP->>Hand: 执行 fetch_url
    Hand-->>LP: （网页正文）→ 记进记忆
    end

    rect rgb(234, 241, 255)
    LP->>Brain: 第2圈：记忆（多了网页正文）
    Brain-->>LP: 我要调 write_file(summary.md, 内容...)
    LP->>Hand: 执行 write_file
    Hand->>Disk: 写入 summary.md ✓
    Hand-->>LP: 已写入 → 记进记忆
    end

    rect rgb(231, 248, 241)
    LP->>Brain: 第3圈：记忆（多了「已写入」）
    Brain-->>LP: 摘要已完成，我要调 terminate(success)
    LP->>LP: 翻状态 = 完成 → 退出循环
    end

    LP-->>U: ✅ 摘要写好了，在 summary.md
```

Three turns, done. Look turn by turn; every part is present:

| Turn | What is sent to the model (accumulating memory) | Model decides | Harness action | Parts used |
|---|---|---|---|---|
| 1 | Manual + the user’s sentence | Call `fetch_url` | Read the page, append the body into memory | Brain · hand · memory |
| 2 | All of turn 1 + page body | Call `write_file` | Summary lands on disk, append the receipt into memory | Loop · hand |
| 3 | All of the first two turns + disk receipt | Call `terminate` | Flip state, exit | Clock-out bell |

Notice memory **rolls longer and longer**: the request on turn 3 contains everything from the first two turns. That is why long tasks get slower and more expensive as they run—and it is the entire reason the “memory” step exists.

**Here, Gate 1 is cleared.** Your agent can think, can act, can loop, has memory, knows how to close, and can become a new specialist by swapping a manual. For the vast majority of “give a task, hand something in” scenes, it is already enough.

---

## Gate 2 · From “can work” to “can work a lot, safely, together”

That Gate 1 agent, on your own machine, one job at a time, is very usable. When you want to **take it live**—let several tasks run at once, let colleagues use it, attach real third-party accounts—four new troubles will surface together.

**The key reminder: every part in Gate 2 is forced out by a concrete pain. If you have not hit that pain, do not build it early.** Building these too early will only drown you in complexity before the main logic even runs.

This gate’s mirror switches to **Kortix/Suna**—a production platform that “runs the company as a Git repository.” We borrow only its four hardest ideas.

```mermaid
flowchart LR
    P1["😱 两个任务<br/>互相改文件"] --> S1["🚧 工位隔离"]
    P2["😱 密钥<br/>漏进了日志"] --> S2["🔐 密钥网关"]
    P3["😱 Agent 改坏了<br/>默认 prompt"] --> S3["📋 变更纪律"]
    P4["😱 同事想在<br/>手机上触发"] --> S4["📡 渠道投递"]
    style P1 fill:#fdeded
    style P2 fill:#fdeded
    style P3 fill:#fdeded
    style P4 fill:#fdeded
    style S1 fill:#eaf1ff
    style S2 fill:#eaf1ff
    style S3 fill:#eaf1ff
    style S4 fill:#eaf1ff
```

### Pain A · Two tasks overwrite each other’s files → workstation isolation

**Where it hurts**: two tasks run at once, both write `workspace/summary.md`—they overwrite each other, a mess.

**Fix**: when each task starts, issue a unique `run_id`, give it a **dedicated workstation**. One task, one directory; nobody steps on anybody.

```mermaid
flowchart TB
    RUN["新任务进来"] --> ID["发一个唯一 run_id<br/>run_id = 'r_' + 随机串"]
    ID --> WS["专属工位<br/>./workspaces/{run_id}/"]
    ID --> BR["（可选）专属 git 分支<br/>agent/{run_id}"]
    WS --> W1["任务A 只在自己工位里写"]
    WS --> W2["任务B 只在自己工位里写"]
    W1 -. 井水不犯河水 .- W2
    style WS fill:#eaf1ff
```

Kortix pushes this idea to the extreme—it has an **iron rule**: `session = sandbox = git branch`, three in one, and **once this identity is established it cannot be changed**. Its source comment is hard (`runtime-identity.ts`): a sandbox holding user data is an “immutable identity boundary”—even if it crashes, you can only stop it, never swap in a new one to take its identity. Why so strict? Because once identity can be stolen and swapped, it means “task A’s artifact may be pinned on task B”—fatal for a multi-tenant platform.

**Naming warning**: in Kortix “conversation id” and “workstation id” are two different things; do not mix them. From day one, log these two fields separately.

```text
验收：并行跑两个任务 → 它们的 workspace 路径不同；
     在 A 的工位里，找不到任何属于 B 的内容。
```

> 🚧 **One sentence**: one task, one workstation, one identity that cannot be stolen and swapped. That is the foundation of multi-task not fighting.

### Pain B · Secrets leak into logs → secret gateway

**Where it hurts**: the task has to call a third-party service that needs an API key (send Slack, query Notion). If you let the agent itself hold the key and call, that key will eventually appear in some log, some deliverable—**leak is only a matter of time**.

**Fix**: build a wall in front of the key. The agent **never gets the real key**; it can only knock on a gateway carrying a request “I want to call X”; the gateway secretly fills in the key on its side, calls, and hands the result back (redacted).

```mermaid
flowchart LR
    subgraph SBX["Agent（跑在沙箱里）· 拿不到真钥匙"]
        REQ["「帮我调 slack.send，<br/>参数是这些」"]
    end
    subgraph GW["网关（可信进程）· 保管钥匙"]
        SEC["从保险箱取出真钥匙"]
        CALL["拿钥匙调第三方"]
        RED["把结果脱敏后返回"]
    end
    REQ -->|"只带任务id + 请求，<br/>不带钥匙"| SEC
    SEC --> CALL --> RED
    RED -->|"干净的结果"| REQ
    style SBX fill:#fdeded
    style GW fill:#eaf1ff
```

Compare Kortix’s Executor: the real credential is attached only on the side of the **trusted API process**; the sandbox running the agent holds only a restricted temporary token (`executor/execute.ts`, `credentials.ts`).

This wall also brings a side benefit—**permission tiers**. Since every third-party call must pass the gateway, the gateway can incidentally judge “should this operation be allowed.” Kortix’s policy engine (`executor/policy.ts`) splits actions into three bins, and the default is set cleverly:

| Action type | Default treatment |
|---|---|
| Read (read) | Allow directly |
| Write / dangerous (write / destructive) | **Needs a human nod** |

This “read allowed, write needs approval” default (called `default_mode = risk` in source) is much safer than “allow everything,” and will not annoy you into confirming every read.

```text
验收：在所有日志和交付物里搜 'sk-' / 'api_key' / 'Bearer' → 零命中。
     网关单测：不带任务凭证去请求 → 返回 401。
```

> 🔐 **One sentence**: the agent only hands over a request; the key is locked in the gateway. If it can act, it can cause trouble—so dangerous actions by default need a human nod.

### Pain C · A broken prompt must be revertible → change discipline

**Where it hurts**: you (or the agent itself) changed the default manual, and the next day every task runs crooked. How do you go back?

**Fix**: you do not need to invent a complex system. Put **all manuals, configs, policy files into git**; if it is broken, `git revert` once and it is back.

- Manuals `prompts/`, configs `specs/`, policies `policies/` — all into git.
- The agent by default **may only write its own workstation**; changing a manual must go through a human PR.
- Add a “config format check” in CI; wrong format is a red light; bad config cannot enter the trunk.

Compare Kortix: it puts artifacts and config into git; merge walks a `change_requests` review process. Your poor-person version—one GitHub PR is enough; you need not invent a review UI. This step is pure process discipline; there is no mechanism worth drawing.

```text
验收：故意提交一个格式非法的配置 → CI 红灯。
     把一次改坏的说明书 revert 掉 → 剧本 α 恢复成功。
```

> 📋 **One sentence**: config goes into git; a bad change can roll back. Do not let “change one sentence of prompt” become an accident you cannot undo.

### Pain D · Trigger from phone / Slack → channel delivery

**Where it hurts**: the Gate 1 agent can only be started by typing a command in your own terminal. A colleague wants to use it, or you want to send a task from your phone. Then what?

**Fix**: wrap an HTTP entry around the outside of the loop. Every channel (web, Slack, phone) eventually becomes a call to the same `loop.run`—**the loop itself does not change at all**.

```mermaid
flowchart LR
    C1["💻 网页"] --> API
    C2["💬 Slack"] --> API
    C3["📱 手机"] --> API
    API["POST /v1/runs<br/>{message, spec_name}"] --> Q["建 run_id + 工位<br/>丢进后台队列"]
    Q --> LOOP["还是那个 loop.run<br/>（一行没改）"]
    LOOP --> POLL["GET /v1/runs/{id}<br/>查状态 + 下载产物"]
    style API fill:#eaf1ff
    style LOOP fill:#e7f8f1
```

Do not invent a complex queue yet—one SQLite + a single background worker is enough to verify “a colleague can send a task from a phone.”

```text
验收：用 curl 创建一个任务 → 轮询到「成功」→ 下载 summary.md → 通过剧本 α 的机检。
```

> 📡 **One sentence**: a thousand channels, one loop. Every entry finally converges on the same `loop.run`.

---

## Appendix 1 · Schedule: string these steps into a rhythm

This is only an **orchestration suggestion**; the real depth is in each step above. If you are in a hurry, 2–3 weeks finishing only Gate 1 (steps 1–10) is enough to hand in something usable.

| Week | Which steps | Deliverable |
|---|---|---|
| 1 | Steps 1–5 | A loop that can terminate + memory + logs |
| 2 | Steps 6–8 | Script α (URL → summary) runs stably |
| 3–4 | Thicken steps 7, 8 | Script β (city → itinerary) runs stably |
| 5 | Step 9 | Network-down / over-budget / stuck behavior all pass |
| 6 | Step 10 | Two specialists + graduation exam pass |
| 7+ | Gate 2 | **Only when** the corresponding pain has actually appeared |

---

## Appendix 2 · Golden tasks: the machine-check list before you ship

“Golden tasks” are a few scripts written dead. Every time you change code you run them once; green means you did not regress.

```bash
# α · 网址摘要
./golden.sh alpha https://example.com
#   期望：退出码 0；summary.md 存在；文件里含 'example.com'；日志含 terminate + success

# β · 城市行程
./golden.sh beta "京都"
#   期望：itinerary.html 存在；里面 ≥3 个 http 链接；日志里有 search

# γ · 断网（把网络代理指到一个死端口）
HTTP_PROXY=http://127.0.0.1:1 ./golden.sh alpha https://example.com
#   期望：非成功收工；日志里有 ok:false；不许步数跑满还在空转

# δ · 预算
MAX_STEPS=3 ./golden.sh alpha https://example.com
#   期望：结局是「预算耗尽」，工位里可以有半成品

# ε · 隔离（第二关）
./golden.sh parallel_beta
#   期望：两个任务的工位目录，文件互不交叉
```

---

## Appendix 3 · Ten ways it most easily blows up

Each row corresponds to some step above; if you step in it, go back and review that step.

| # | Blow-up | Which step it wounds |
|---|---|---|
| 1 | Pass tool arguments downstream as a raw string, no `json.loads` | Step 1 · brain |
| 2 | `write_file` does not validate paths; the model can write through the whole disk | Step 2 · hand |
| 3 | The tool `raise`s on first failure; one bad link blows up the whole task | Step 2 · hand |
| 4 | The loop has no step budget; a dead-end burns the budget empty | Step 3 · loop |
| 5 | Expect “the model went quiet = finished,” then empty-spin to exhaustion | Step 4 · terminate |
| 6 | Reuse an agent instance and forget `memory.clear()`; tasks taste of each other | Step 5 · memory |
| 7 | Every turn’s sticky note is stacked into memory, eating quota for nothing | Step 5 · memory |
| 8 | Week one you already put on “multi-agent dispatch,” drown in complexity | Step 10 · specialist |
| 9 | Take browser `disable_security=True` onto the public net | Step 7 · research |
| 10 | Secrets entered memory / logs / deliverables | Pain B · gateway |

**License reminder**: OpenManus is MIT (you may freely fork commercially; keeping attribution is recommended); Kortix/Suna is Elastic License 2.0 (internal use is usually fine, but you must read the terms before hosting it as a competing service).

---

## Appendix 4 · Comparative reading index

When you want to go deeper on a step, first read the matching analysis chapter, then open the real source.

| Step | Read the analysis first | Then read source (`参考项目/OpenManus/`) |
|---|---|---|
| Step 1 brain | *OpenManus source analysis* chapter 5 | `ask_tool` in `app/llm.py` |
| Step 2 hand | Chapter 7 | `tool/base.py`; `tool_collection.py` |
| Step 3 loop | Chapter 6 (the most detailed in the whole text) | `agent/base.py`, `react.py`, `toolcall.py` |
| Step 4 terminate | Section 6.4 | `tool/terminate.py`; `toolcall.py:210-227` |
| Step 5 memory | Chapters 5, 8 | Memory in `schema.py` |
| Step 6 manual | Section 5.2 | `prompt/manus.py` |
| Step 7 research | Section 7.3 | `tool/browser_use_tool.py` |
| Gate 2 · all | *Suna source analysis* chapters 3/6/7/10 | `runtime-identity.ts`; `executor/policy.ts`, `execute.ts` |

The “from-scratch build” zone in the learning site `openmanus-suna-learn/` corresponds one-to-one with this tutorial; you can click along.

---

## Close

> **Every step having a runnable acceptance assertion is building; a pile of slogans and a schedule is empty talk.**

Look back at the road you walked: a blank page that could do nothing, onto which you added a brain, a hand, a loop, memory, a manual, a clock-out bell—and it can fetch a page on its own, write a summary, and know how to close (Gate 1). When you really push it live, fill in workstations, a key vault, rollback, and multiple entries according to the pains (Gate 2).

**Next**: stop reading, open a repo. Today run through that smallest assertion—“let the model actively call terminate once.” The rest, one step at a time.

*End of text.*
