Series Outline · Hand Rubbing AI Agent (Node.js / TypeScript)
This series starts from scratchGeneral tool Agent: Can have conversations, call tools, plan tasks, collaborate with multiple agents, and finally be connected to the MCP ecosystem. Technology stack lock-in Node.js 22 + TypeScriptthe model access layer uses a set of OpenAI Compatible AbstractionsTake all OpenAI / DeepSeek / Tongyi / OpenRouter – change
.envYou can change the model without any changes to the core. Every piece of code is runnable, testable, andgit checkout post-NNGo back to the starting point of any article, and the pictures and texts will be published simultaneously on multiple platforms.
General catalog (14 main lines + 1 ending)
- Beginning: What is Agent, why not use the framework directly?
- Engineering foundation: Node22 + TS + unified LLM client
- Heart: Agent Loop and ReAct
- Growing arms and legs: Tool system design
- The first real tool: search/calculate/read files
- Multi-tool collaboration: serial orchestration and parallel execution (This article)
- Two forms of memory
- Will planning: Plan-and-Execute
- Multi-Agent collaboration
- Connecting to the outside world: MCP in action
- Make the Agent trustworthy: permissions/sandbox/manual intervention
- Persistence and recovery: session, state, crash continuation
- Online and observable
- Ending: This series of hand-rubbed vs. industry ADK/Mastra
Current article: Part 5 (corresponding to item 6 of the general catalog·Multi-tool collaboration) – Based on the four real tools (calculator / readFile / searchFiles / webFetch) in Part 4, let the Agent work in a taskAutonomously connect multiple tools in series and parallel. The focus is on two forms of collaboration: the dependent type must be arranged serially across rounds, and the independent type can be executed in parallel in the same round; and how the code layer can truly speed up parallelism without damaging it.
tool_call_idcorresponding.Supporting code warehouse: (Each article corresponds to one
post-NNtag,git checkout post-05You can return to the starting point of this code)
Part 4 equips the Agent with four real tools, which can “count, read, search, and capture”. But “having tools” and “being able to use tools to collaborate” are two different things. Give the model a calculatorit can calculate 96; given the model four tools, it can also calculateCalled continuously in a conversationthey. But once the task becomes complicated – “search for the agent in the information, read out the hit plan file, and tell me what it says” – it can no longer be solved by “adjusting a tool”: the task is broken down intoA series of sequential and dependent actionsand some of these actions are independent of each other and could haveDo it at the same time。
What this article wants to solve is exactly the word “collaboration”:Who decides the order in which tools are called? Can multiple independent calls be run together? You will find that there are very few codes that really need to be changed (more than a dozen lines). The difficult thing is to get the “parallelism” thing right without destroying the existing error isolation and context structure.
00 Introduction: Why “knowing how to adjust tools” is not enough
First distinguish two levels:
- Single tool call: The model determines “who to call in this step and what parameters to pass”. Parts 2, 3, and 4 have been solved – the loop is responsible for turning, the registry is responsible for verification and execution, and error isolation is responsible for not crashing.
- Multi-tool collaboration: The model determines “who to adjust first, who to adjust later, and which ones can be adjusted together.” The order and concurrency of this step are not hard-coded in the code;The model determines by itself at each roundthe circular “turning in circles” just puts its judgment on the ground.
Why does this deserve its own article? Because once multiple tools are involved,order and dependenciesIt becomes a top priority:
- “First
searchFilesFind the file name and thenreadFileRead it” – these two steps have dependencies and must be done one after the other; let the model be issued in parallel, and the second step will be called before the file name is obtained.readFilewill inevitably fail. - “Catch at the same time
example.comandexample.org“Two web pages” – these two steps are performed separately. It doesn’t matter which one comes first. Publishing them together can save half the time.
Confusing these two categories is either slow (obviously it can be parallelized but waits one by one), or wrong (obviously there are dependencies but it is messed up in parallel). Next, nail the form first, and then change the code.
01 Concept: Two forms of multi-tool collaboration
Multiple tool calls in a task are divided into two categories according to “whether they have dependencies”:
1.1 Serial arrangement (across rounds, because there are dependencies)
whenThe latter tool requires the return of the previous onethey can only occur in different model rounds. The reason is: when the model makes a tool call, it doesn’t yet know what the previous tool will return – it must first see the first round of observations (such as “hit docs/plan.md“) can be decided in the second round” Then I read docs/plan.md“。
This kind of arrangementNo need to write any code: It is the natural behavior of Agent Loop. The first round of model adjustment searchFiles → Observation is written back to the context → The second round of model reads the observation → Call readFile → Observe and write back → The third round of information is enough, give the answer. Every time the loop turns around, there is one more observation in the context, and the model’s decision-making is pushed forward one step. All the code has to do is “don’t stop it from spinning”, and the loop in Part 2 already supports it.
1.2 Parallel execution (same round, because independent)
When multiple tools callindependent of each otherWhen, the model can beSend multiple messages at once in the same reply tool_calls. The OpenAI / DeepSeek interface originally supports one reply with multiple tool_calls——The problem is in the execution of the old code: it uses for cycleserial one by oneFor ground running, two grabs that each take 1 second will be strung together into 2 seconds.
The core change in this article is to replace this serial execution withParallel execution。
1.3 Who decides “serial or parallel”
Key insights:The orchestrator is the model and the engine is the loop. The code does not predict tasks, nor does it write a “search first, read later” process; the code only guarantees two things –
- Dependent calls, because the observation has to wait for the previous round, are naturally split into two rounds by the loop (serialization is guaranteed by the data structure);
- Independent call, if multiple models are sent at one time, the code will be executed in parallel (concurrency is controlled by
Promise.allensure).
you just needTell the model the likelihood of this happening(Written in the system prompt), it will select parallelism on the appropriate tasks.
02 Code: Parallel multiple tool calls in the same round
The changes are only in src/agent/Agent.ts and only touch the short section of “Execute Tool Call”.
2.1 Old logic: serial for loop
What’s left in Part 4 is this:
// 旧:一次回复里的多个 tool_calls,逐个串行执行
for (const call of result.toolCalls) {
const observation = await this.runTool(call); // 等这一个跑完,才跑下一个
this.conversation.push({ role: "tool", content: observation, tool_call_id: call.id });
}
Two 1 second grabs each will be strung together into 2 seconds. What’s worse is that the model clearly said “I’ll catch these two at the same time” in a message, but the execution end secretly queued up.
2.2 New logic: Promise.all parallelism
// src/agent/Agent.ts(节选)
// 一次回复可能要求调用多个工具。彼此独立的调用(如并行抓取两个网页)
// 用 Promise.all 同时跑,省时间;彼此依赖的调用模型会自动拆成两轮,
// 依赖关系的编排由上面的 for 循环自然完成。registry.run 永不抛异常
// (失败转成观察文本),所以 Promise.all 不会因单个工具失败而短路。
const executed = await Promise.all(
result.toolCalls.map(async (call) => {
const actionText = `${call.function.name}(${JSON.stringify(call.function.arguments)})`;
const observation = await this.runTool(call);
return { id: call.id, actionText, observation };
}),
);
for (const { id, actionText, observation } of executed) {
steps.push({ kind: "action", text: actionText });
steps.push({ kind: "observation", text: observation });
this.conversation.push({
role: "tool",
content: observation,
tool_call_id: id,
});
}
Three points should be made clear:
- Concurrency by
Promise.allFinish. MultiplerunToolsimultaneouslyawaitwhoever returns first will finish the calculation first. The wall clock time is approximately equal to the slowest one, rather than adding up. tool_call_idThe correspondence is not broken. Each observation carries its own correspondingidwhen writing back the context, hang up to the correcttool_call_idsuperior. The next time the model sees is “call a1 to get x, call a2 to get y”, there will be no aliasing – this is a rigid requirement of the OpenAI function calling protocol, and it is especially easy to write errors in parallel. Here, “collect first, then write back in the original order” is used to avoid the race condition.- No short circuit due to a single failure。
registry.run()internaltry/catchConvert any exceptions into observation text, soPromise.allWhat you get is always the parsed value, and there is no situation where a tool throws a reject and causes the remaining observations to be lost. The error isolation article still holds true.
2.3 System prompt: Give the model “parallel” permission
The code can be parallelized, but the model may not be issued multiple times. tool_calls. Add an arrangement guide to the default system prompt in Part 4 to clearly state “when to run in parallel and when to run in series”:
// src/agent/Agent.ts(DEFAULT_SYSTEM_PROMPT 追加)
"关于多工具协作:如果多个工具调用彼此独立(例如同时抓取两个网页、同时读两个文件)," +
"请在同一次回复里一次性发出,我会并行执行它们;如果后一个工具需要前一个的返回结果" +
"(例如先 searchFiles 找到文件名、再 readFile 读它),就分两轮,先拿到观察再决定下一步。";
This tip does not change any code paths, it just reduces the tendency of the model to “default one by one”. After adding it in the actual test, when encountering an apparently independent task such as “grabbing two web pages at the same time”, the model will actively send two messages in the same message. webFetch。
03 Data flow: The loop shape has not changed, it just rotates faster
The tools are still the same four, and the registry is still the same. The only change is “how to implement multiple calls in a model reply”:

- serial path: The first round of model release
searchFiles→registry.runExecute → observe writeback → second round of model reading and observationreadFile→ Write back → Give the answer in the third round. The “next step” of each step is determined by the model based on the previous round of observations, and the loop is just called repeatedlychatWithTools。 - parallel paths: The model sends two messages in one message
webFetch→Promise.allExecute simultaneously → two observation buttonsidWrite back separately → The next round of model will get two results and summarize them together.
Two paths share the same set registry.run() Isolated from errors, the only new addition is the “parallel” accelerator.
How to run the code in this chapter
All supporting codes have been pushed to GitHub, one for each article post-NN tag. Following this chapter, first cut to post-05 The code status, and then run step by step:
# 1. 克隆仓库
git clone .git
cd agent-from-scratch
# 2. 切到本篇(第 5 篇)对应的代码快照
git checkout post-05
# 3. 安装依赖(需要 Node.js >= 22,否则内置 fetch 不可用)
npm install
# 4. 配置密钥:复制模板,填入你的 API Key
cp .env.example .env
# 然后编辑 .env,填入:
# OPENAI_API_KEY=sk-xxxxxxxx(填入申请的 API Key)
# BASE_URL= # DeepSeek 兼容地址,注意【不带 /v1】
# MODEL=deepseek-flash
# 5. 启动 Agent 对话(输入一句,Agent 可能连续调用多个工具,输入 exit 退出)
npm run dev
# 6. 跑单元测试(mock,无需 key / 外网)
npm test
# 7. 类型检查
npm run typecheck
illustrate:.env has been .gitignore Ignored, the commit will not be entered and the key will only stay on the local machine. If you want to intuitively see the collaboration effect, you can try these two sentences (the former triggers serial orchestration, the latter triggers parallelization): “Search for the word agent in the data directory, find the mentioned file, read docs/plan.md, and tell me what it mainly talks about”, “Please crawl two web pages in parallel in the same round, and tell me to verify their page title text.” like npm run dev Connection timeout reported, please confirm first .env inside BASE_URL Not written in tape /v1 address (DeepSeek does not carry /v1)。
04 Verification: What does the code in this chapter do and what effect is achieved?
Code location:src/agent/Agent.ts(run() Within the tool execution is performed by the serial for Change to Promise.all parallel;DEFAULT_SYSTEM_PROMPT Additional arrangement guide),tests/collaboration.test.ts(3 new use cases added).
what did: Change “multiple tool calls in the same round of model reply” from serial execution to parallel execution, and thoroughly explain the two collaboration forms of “serial orchestration/parallel execution”. There are no new abstractions outside of the Agent core – parallelism just adds runTool Bao Jin Promise.allthe order is the same as tool_call_id Correspondence is guaranteed by “collect first and then write back”. The understanding that “the model is the orchestrator and the loop is the engine” buried in Part 4 is explicitly realized here.
What effect was achievedthree sets of evidence:

- Unit test 36/36 passed(
npm test5 files total 36 Use cases: Part 1llm.test.ts6 cases, Part 2agent.test.ts6 cases, Part 3tools.test.ts13 Examples, Chapter 4tools-real.test.ts8 cases, new in this articlecollaboration.test.ts3 examples). The new use case specifically covers collaboration: each observation presstool_call_idCorrespondingly; two fake tools that each take 50ms are called in the same round to assert their time intervals.overlappingAnd the total time consumption is far less than 100ms (which proves that it is indeed concurrency rather than queuing); when dependent tools are serialized across rounds, the parameters obtained by the latter tool do come from the observations of the previous tool. - Type checking 0 errors(
npm run typecheck,tsc --noEmit,strict+noUncheckedIndexedAccessFully open).Promise.allcollected is{ id, actionText, observation }Structure, type closed when written back. - Real conversations run end-to-end(DeepSeek
deepseek-flashsee below):

The real operation shows two rounds of dialogue: in the first round, the user asked to “search agent, read plan.md, and summarize”, and the model walked out independently. searchFiles → readFile → answeredserial arrangementsecond step readFile The path is exactly what is given by the first step of observation docs/plan.md;The second round of users asked for “grabbing two web pages in parallel”, and the model wasSend two in the same message webFetch,Depend on Promise.all Capture them at the same time and summarize the titles together. Both serial and parallel modalities were verified in the same session.
Records of three engineering pits(Both are handled in the code):
- Parallel must be used
Promise.allAfter collecting, write back in order。runToolThere is insideawaitif while runningpushto step logs and context, logs for multiple calls will be interleaved, andtool_call_idIt’s easy to make a mistake. First collect them into arrays and then unify themforWrite-back ensures both sequential readability and ID correspondence. - Parallelism is not short-circuited by a single failure because
registry.runNever throw exception. This is the bonus of Part 3 “Error Isolation”: tool execution exceptions are converted into observation text,Promise.allWhat I got were normal values. If one day someone takesrunChange tothrowparallelism will throw away all remaining observations due to an error in one tool. - Parallel calls in the step log look like “executed one after another”。
[行动]/[观察]They are printed in order in the terminal. This is just the log display order and does not represent the execution order on the wall clock – twowebFetchIn fact, they were issued at the same time. If you understand this, you will not mistakenly think that “parallelism does not take effect”.
05 The minefield buried in this article
This chapter allows the Agent to autonomously connect and parallel multiple tools in a task, but three issues are still pending, stringing together subsequent chapters:
- The model may not choose “serial or parallel” correctly every time . The prompt states the rules, but the model may still be emitted all at once when there are dependencies (for example, without waiting
searchFilesReturn to parallelreadFile), as a result, the second tool gets the empty parameter and triggersvalidationObserve and correct by looping and retrying. The more secure “plan first and then execute” is the theme of Part 7 (Plan-and-Execute). maxStepsWill reach the top earlier in complex tasks . It used to be that one tool lasted one round, but now a task may require several steps of “search→read→calculate→summarize”. The original 8-step budget will not be enough in long tasks, triggering a cryptic answer. Whether the upper limit of the number of steps should be given dynamically according to the task is something that must be considered when planning to split the sub-tasks in Part 7.- The more tools there are, the easier it is for the model to be “wrong selected and messed up”. . The four tool models in this article can still be controlled, but after the registry is filled with twenty tools, it may be entangled in “should use searchFiles or webFetch”, or issue a bunch of unrelated calls in parallel. This involves how to write tool descriptions to be more differentiated, and whether a planning layer of “think clearly about the steps first” should be introduced – which also points to Part 7.
Judgment: Multi-tool collaboration allows the Agent to move from “can adjust a single tool” to “can sequence tasks by itself and can speed up in parallel”, but the three problems of dependence on misjudgment, step budget, and tool inflation have not yet been eradicated. Highlights: The next article is to equip the Agent with “memory” – short-term memory of the current round’s context, and long-term memory of cross-session preferences and facts, otherwise it will be like amnesia every time it opens a conversation.
Focus on Internet product development and share tool experience. Do everything well, big or small.