如果一間公司收到一個需求,最怕的不是沒人做,而是大家都以為自己該做。
老闆看到信就自己回、工程師看到信就自己改、行政看到信就自己排、每個人都想直接把事情處理掉。短時間看起來很勤快,長時間就是災難。
OpenClaw 的做法剛好相反。
它不是先問「我能不能做」,而是先問:
第 3 天我們看的是入口怎麼接住任務。第 4 天就要再往下一層,看它怎麼把任務真正分派出去,而且不是靠想像,而是看 source code 怎麼做。
這一篇我會把設定當成引子,但主體放在實作本身。
allowAgents 只是設定,還是會真的影響執行?sessions_spawn 在 source code 裡怎麼收件、過濾、放行?OpenClaw 的分派不是一個動作,而是兩段流程:
所以如果只看設定檔,你只會知道有名單。
但如果看 source code,你才會知道:
也就是說,設定只是入口,真正的分派邏輯在 code 裡。
如果把這一段拆成白話版,大概是這樣:
使用者需求
-> main 接住
-> 決定要不要分派
-> subagent spawn code 檢查誰可被叫出來
-> delivery / routing code 決定工作要送去哪裡
-> 任務執行
-> 結果回傳或送達
這裡最重要的觀念是:
allowAgents 不是裝飾品subagents 不是說「有這些人」而已route / delivery 不是純輸出,它們是在決定結果要落到哪個 session所以第 4 天真正要看的不是「有沒有設定」,而是「設定怎麼被程式吃進去,然後變成規則」。
openclaw.json 裡面,main 的確有一段子代理允許清單:
📄 文件:
docs/tools/pdf.md:170-181
{
"id": "main",
"model": "openai/gpt-5.4-mini",
"subagents": {
"allowAgents": [
"research",
"kaijin",
"diablo",
"shion",
"gobta",
"souei",
"luminous",
"adalman",
"benimaru"
]
}
}
這段本身只是在描述政策,還不夠。
真正關鍵的是,這個 policy 會怎麼被 source code 讀進去、整理、驗證。
allowAgents 怎麼被整理成可用的目標下面這段來自 /opt/homebrew/lib/node_modules/openclaw/dist/subagent-spawn-plan-CsAYyEim.js,核心是把 allowAgents 轉成可比較的 policy。
📄 原始碼:
src/agents/subagent-target-policy.ts:32-78
function normalizeAllowAgents(allowAgents) {
if (!Array.isArray(allowAgents)) return {
configured: false,
allowAny: false,
allowedIds: []
};
const allowedIds = allowAgents.map((value) => value.trim()).filter((value) => value && value !== "*").map((value) => normalizeAgentId(value)).filter(Boolean);
return {
configured: true,
allowAny: allowAgents.some((value) => value.trim() === "*"),
allowedIds: sortUniqueStrings(allowedIds)
};
}
function resolveSubagentAllowedTargetIds(params) {
const requesterAgentId = normalizeAgentId(params.requesterAgentId);
const policy = normalizeAllowAgents(params.allowAgents);
if (!policy.configured) return {
allowAny: false,
allowedIds: requesterAgentId ? [requesterAgentId] : []
};
if (policy.allowAny) {
const configuredIds = Array.from(normalizeConfiguredAgentIds(params.configuredAgentIds));
if (requesterAgentId) configuredIds.push(requesterAgentId);
return {
allowAny: true,
allowedIds: sortUniqueStrings(configuredIds)
};
}
return {
allowAny: false,
allowedIds: filterConfiguredAllowedIds({
allowedIds: policy.allowedIds,
configuredAgentIds: params.configuredAgentIds
}).toSorted((a, b) => a.localeCompare(b))
};
}
這段在做的事,其實很直接:
allowAgents 標準化*,才代表放寬成 allow any這代表 OpenClaw 並不是看到 allowAgents 就直接照單全收。
它會先清理輸入,再跟實際 registry 比對,最後才決定誰真的能接。
這就是「設定有寫,不代表能執行」的第一層。
同一個檔案裡,下一段是驗證單一目標是否真的能派出去:
📄 原始碼:
src/agents/subagent-target-policy.ts:57-106
function resolveSubagentTargetPolicy(params) {
const requesterAgentId = normalizeAgentId(params.requesterAgentId);
const targetAgentId = normalizeAgentId(params.targetAgentId);
if (!params.requestedAgentId?.trim() && targetAgentId === requesterAgentId) return { ok: true };
const allowed = resolveSubagentAllowedTargetIds({
requesterAgentId,
allowAgents: params.allowAgents,
configuredAgentIds: params.configuredAgentIds
});
if (allowed.allowedIds.includes(targetAgentId)) return { ok: true };
const allowedText = allowed.allowedIds.length > 0 ? allowed.allowedIds.join(", ") : "none";
const policy = normalizeAllowAgents(params.allowAgents);
if (allowed.allowAny || policy.allowedIds.includes(targetAgentId)) return {
ok: false,
allowedText,
error: `agentId "${targetAgentId}" is not in the configured agent registry (allowed: ${allowedText})`
};
return {
ok: false,
allowedText,
error: `agentId is not allowed for sessions_spawn (allowed: ${allowedText})`
};
}
這段就是實際的 gate。
它會先看:
如果不行,就直接回 error。
這裡很重要,因為它把「政策」變成「執行時檢查」了。
不是設定看起來可以,而是跑到這一關真的可以。
接下來看 /opt/homebrew/lib/node_modules/openclaw/dist/agent-delivery-BE_Ki_cN.js 裡的 delivery / routing 邏輯。
📄 原始碼:
src/infra/outbound/agent-delivery.ts:86-163
function resolveAgentDeliveryPlan(params) {
const requestedRaw = normalizeOptionalString(params.requestedChannel) ?? "";
const requestedChannel = (requestedRaw ? normalizeMessageChannel(requestedRaw) : void 0) || "last";
const explicitTo = normalizeOptionalString(params.explicitTo) ?? void 0;
const normalizedTurnSource = params.turnSourceChannel ? normalizeMessageChannel(params.turnSourceChannel) : void 0;
const turnSourceChannel = normalizedTurnSource && isDeliverableMessageChannel(normalizedTurnSource) ? normalizedTurnSource : void 0;
const turnSourceTo = normalizeOptionalString(params.turnSourceTo) ?? void 0;
const turnSourceAccountId = normalizeAccountId(params.turnSourceAccountId);
const turnSourceThreadId = params.turnSourceThreadId != null && params.turnSourceThreadId !== "" ? params.turnSourceThreadId : void 0;
const baseDelivery = resolveSessionDeliveryTarget({
entry: params.sessionEntry,
requestedChannel: requestedChannel === "webchat" ? "last" : requestedChannel,
explicitTo,
explicitThreadId: params.explicitThreadId,
turnSourceChannel,
turnSourceTo,
turnSourceAccountId,
turnSourceThreadId
});
const resolvedChannel = (() => {
if (requestedChannel === "webchat") return INTERNAL_MESSAGE_CHANNEL;
if (requestedChannel === "last") {
if (baseDelivery.channel && baseDelivery.channel !== "webchat") return baseDelivery.channel;
return INTERNAL_MESSAGE_CHANNEL;
}
if (isGatewayMessageChannel(requestedChannel)) return requestedChannel;
if (baseDelivery.channel && baseDelivery.channel !== "webchat") return baseDelivery.channel;
return INTERNAL_MESSAGE_CHANNEL;
})();
const deliveryTargetMode = explicitTo ? "explicit" : isDeliverableMessageChannel(resolvedChannel) ? "implicit" : void 0;
const resolvedAccountId = normalizeAccountId(params.accountId) ?? (deliveryTargetMode === "implicit" ? baseDelivery.accountId : void 0);
let resolvedTo = explicitTo;
if (!resolvedTo && isDeliverableMessageChannel(resolvedChannel) && resolvedChannel === baseDelivery.lastChannel) resolvedTo = baseDelivery.lastTo;
return {
baseDelivery,
resolvedChannel,
resolvedTo,
resolvedAccountId,
resolvedThreadId: baseDelivery.threadId,
deliveryTargetMode
};
}
這段 code 看起來很像在處理一堆聊天系統的雜事,但本質只有一件事:
工作做完以後,要送到哪一條通道、哪個 account、哪個 thread。
它會先整理:
to
這就是「接收任務」之後的另一半:送達。
如果前面的 subagent-spawn-plan 解的是「誰可接」,這裡解的是「結果往哪裡回」。
同一個檔案後面還有一段更重的處理:
📄 原始碼:
src/infra/outbound/agent-delivery.ts:176-325
async function resolveAgentDeliveryPlanWithSessionRoute(params) {
const plan = resolveAgentDeliveryPlan(params);
const { resolvedChannel, resolvedTo } = plan;
if (!params.wantsDelivery || !resolvedTo || !isDeliverableMessageChannel(resolvedChannel)) return plan;
const plugin = resolveOutboundChannelPlugin({
channel: resolvedChannel,
cfg: params.cfg,
allowBootstrap: true
});
const hasPluginSessionRoute = Boolean(plugin?.messaging?.resolveOutboundSessionRoute);
if (!hasPluginSessionRoute && params.sessionRouteMode !== "allow-fallback") return plan;
const resolvedAccountId = plan.resolvedAccountId ?? (plugin && params.sessionRouteMode === "allow-fallback" ? resolveChannelDefaultAccountId({
plugin,
cfg: params.cfg
}) : void 0);
const normalizedTarget = resolveOutboundTarget({
channel: resolvedChannel,
to: resolvedTo,
cfg: params.cfg,
accountId: routedPlan.resolvedAccountId,
mode: routedPlan.deliveryTargetMode ?? "explicit"
});
...
return {
...routedPlan,
resolvedSessionKey: selectedRoute.sessionKey,
resolvedTo: hasPluginSessionRoute ? selectedRoute.to : resolvedSessionRouteTarget?.to ?? sessionRouteTarget,
resolvedThreadId: selectedRoute.threadId ?? (routedPlan.deliveryTargetMode === "explicit" ? explicitThreadId : routedPlan.resolvedThreadId)
};
}
這段的重點是:OpenClaw 不只知道「送到哪個 channel」,還會進一步找對應的 session route。
也就是說,它不是簡單把訊息丟出去而已,而是在問:
to 要不要重新標準化?這就是完整的「接收 -> 處理 -> 路由 -> 送達」鏈條。
我把這段想成一間公司真的在派工。
allowAgents 是名單,不是命令很多人看設定會想說:
「既然寫了 allowAgents,那是不是就代表可以叫這些人?」
不完全是。
因為 source code 會再做兩層事情:
所以這不是「有寫就算」,而是「有寫,還要真的存在,還要真的被允許」。
sessions_spawn 像人事窗口你可以把 resolveSubagentTargetPolicy 想成一個人事窗口。
窗口先看:
如果不符合規則,它不會勉強幫你接單,而是直接退回。
這很像成熟的組織,不會因為「想要快」就跳過權限。
任務接了之後,真正麻煩的是結果怎麼回去。
這時候不是只看一個「訊息要發出去」就完事,而是要考慮:
這就像物流出貨。
不是包裹交出去就好了,還要知道地址、路線、倉儲、收件人、最後一哩路。
因為「誰可以做」跟「做完怎麼送」是兩件不同的事。
如果把兩件事混在一起,系統會很難維護:
OpenClaw 這裡拆得很乾淨,所以後面比較好擴充。
這就是我這次想修正的地方。
只看設定會很像「有這些角色」,但看 source code 才會知道「怎麼真的派出去」。
allowAgents 會先被 normalize,再跟實際 registry 做交集sessions_spawn 會真的檢查 target 是否可被允許,不合法就直接回錯resolveAgentDeliveryPlan 與 resolveAgentDeliveryPlanWithSessionRoute 負責把結果送到對的 channel / account / session第 5 天我會接著看另一件很重要的事:工具怎麼變成 Agent 的手腳。
因為當分派已經搞清楚之後,下一個問題就是:
任務接到人手上以後,他到底是怎麼把事情做完的?
那就是下一篇要看的重點。