乐于分享
好东西不私藏

OpenClaw工具拆解之 sessions_send+sessions_spawn

OpenClaw工具拆解之 sessions_send+sessions_spawn

一、sessions_send 工具

1.1 工具概述

功能:发送消息到其他会话核心特性

  • • 支持 sessionKey 或 label 定位目标
  • • 支持指定 agentId(跨 agent 通信)
  • • Agent-to-Agent 策略检查
  • • 会话可见性检查
  • • 超时控制

1.2 Schema 定义

位置:第 109348 行

constSessionsSendToolSchema = Type.Object({sessionKeyType.Optional(Type.String()),labelType.Optional(Type.String({minLength1,maxLength512    })),agentIdType.Optional(Type.String({minLength1,maxLength64    })),messageType.String(),timeoutSecondsType.Optional(Type.Number({ minimum0 }))});

1.3 完整执行代码

位置:第 109358 行

functioncreateSessionsSendTool(opts) {return {label"Session Send",name"sessions_send",description"Send a message into another session. Use sessionKey or label to identify the target.",parametersSessionsSendToolSchema,executeasync (_toolCallId, args) => {const params = args;const gatewayCall = opts?.callGateway ?? callGateway;// 1. 解析消息(必填)const message = readStringParam$1(params, "message", { requiredtrue });// 2. 解析会话上下文const { cfg, mainKey, alias, effectiveRequesterKey, restrictToSpawned } = resolveSessionToolContext(opts);const a2aPolicy = createAgentToAgentPolicy(cfg);const sessionVisibility = resolveEffectiveSessionToolsVisibility({                cfg,sandboxed: opts?.sandboxed === true            });// 3. 解析目标参数const sessionKeyParam = readStringParam$1(params, "sessionKey");const labelParam = readStringParam$1(params, "label")?.trim() || void0;const labelAgentIdParam = readStringParam$1(params, "agentId")?.trim() || void0;// 4. 检查参数冲突if (sessionKeyParam && labelParam) {returnjsonResult({runId: crypto$1.randomUUID(),status"error",error"Provide either sessionKey or label (not both)."                });            }let sessionKey = sessionKeyParam;// 5. 通过 label 解析 sessionKeyif (!sessionKey && labelParam) {const requesterAgentId = resolveAgentIdFromSessionKey(effectiveRequesterKey);const requestedAgentId = labelAgentIdParam ? normalizeAgentId(labelAgentIdParam) : void0;// 沙盒限制if (restrictToSpawned && requestedAgentId && requestedAgentId !== requesterAgentId) {returnjsonResult({runId: crypto$1.randomUUID(),status"forbidden",error"Sandboxed sessions_send label lookup is limited to this agent"                    });                }// Agent-to-Agent 策略检查if (requesterAgentId && requestedAgentId && requestedAgentId !== requesterAgentId) {if (!a2aPolicy.enabled) {returnjsonResult({runId: crypto$1.randomUUID(),status"forbidden",error"Agent-to-agent messaging is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent sends."                        });                    }if (!a2aPolicy.isAllowed(requesterAgentId, requestedAgentId)) {returnjsonResult({runId: crypto$1.randomUUID(),status"forbidden",error"Agent-to-agent messaging denied by tools.agentToAgent.allow."                        });                    }                }// 解析 labelconst resolveParams = {label: labelParam,                    ...requestedAgentId ? { agentId: requestedAgentId } : {},                    ...restrictToSpawned ? { spawnedBy: effectiveRequesterKey } : {}                };let resolvedKey = "";try {const resolved = awaitgatewayCall({method"sessions.resolve",params: resolveParams,timeoutMs1e4                    });                    resolvedKey = typeof resolved?.key === "string" ? resolved.key.trim() : "";                } catch (err) {const msg = err instanceofError ? err.message : String(err);if (restrictToSpawned) {returnjsonResult({runId: crypto$1.randomUUID(),status"forbidden",error"Session not visible from this sandboxed agent session."                        });                    }returnjsonResult({runId: crypto$1.randomUUID(),status"error",error: msg || `No session found with label: ${labelParam}`                    });                }if (!resolvedKey) {if (restrictToSpawned) {returnjsonResult({runId: crypto$1.randomUUID(),status"forbidden",error"Session not visible from this sandboxed agent session."                        });                    }returnjsonResult({runId: crypto$1.randomUUID(),status"error",error`No session found with label: ${labelParam}`                    });                }                sessionKey = resolvedKey;            }// 6. 检查 sessionKey 是否存在if (!sessionKey) {returnjsonResult({runId: crypto$1.randomUUID(),status"error",error"Either sessionKey or label is required"                });            }// 7. 解析会话引用const resolvedSession = awaitresolveSessionReference({                sessionKey,                alias,                mainKey,requesterInternalKey: effectiveRequesterKey,                restrictToSpawned            });if (!resolvedSession.ok) {returnjsonResult({runId: crypto$1.randomUUID(),status: resolvedSession.status,error: resolvedSession.error                });            }// 8. 检查可见性const visibleSession = awaitresolveVisibleSessionReference({                resolvedSession,requesterSessionKey: effectiveRequesterKey,                restrictToSpawned,visibilitySessionKey: sessionKey            });if (!visibleSession.ok) {returnjsonResult({runId: crypto$1.randomUUID(),status: visibleSession.status,error: visibleSession.error,sessionKey: visibleSession.displayKey                });            }const resolvedKey = visibleSession.key;const displayKey = visibleSession.displayKey;// 9. 解析超时const timeoutSeconds = typeof params.timeoutSeconds === "number" && Number.isFinite(params.timeoutSeconds) ? Math.max(0Math.floor(params.timeoutSeconds)) : 30;const timeoutMs = timeoutSeconds * 1e3;const announceTimeoutMs = timeoutSeconds === 0 ? 3e4 : timeoutMs;// 10. 生成幂等键const idempotencyKey = crypto$1.randomUUID();let runId = idempotencyKey;// 11. 可见性检查const access = (awaitcreateSessionVisibilityGuard({action"send",requesterSessionKey: effectiveRequesterKey,visibility: sessionVisibility,                a2aPolicy            })).check(resolvedKey);if (!access.allowed) {returnjsonResult({runId: crypto$1.randomUUID(),status: access.status,error: access.error,sessionKey: displayKey                });            }// 12. 构建发送参数const sendParams = {                message,sessionKey: resolvedKey,                idempotencyKey,deliverfalse,  // 不直接发送到渠道channelINTERNAL_MESSAGE_CHANNEL,laneAGENT_LANE_NESTED,extraSystemPromptbuildAgentToAgentMessageContext({requesterSessionKey: opts?.agentSessionKey,requesterChannel: opts?.agentChannel,targetSessionKey: displayKey                }),inputProvenance: {kind"inter_session",sourceSessionKey: opts?.agentSessionKey,sourceChannel: opts?.agentChannel,sourceTool"sessions_send"                }            };// 13. 启动 A2A 流程const requesterSessionKey = opts?.agentSessionKey;const requesterChannel = opts?.agentChannel;const maxPingPongTurns = resolvePingPongTurns(cfg);const delivery = {status"pending",mode"announce"            };conststartA2AFlow = (roundOneReply, waitRunId) => {runSessionsSendA2AFlow({targetSessionKey: resolvedKey,                    displayKey,                    roundOneReply,                    waitRunId,                    requesterSessionKey,                    requesterChannel,                    maxPingPongTurns,                    gatewayCall,                    delivery                });            };// 14. 发送消息try {const result = awaitstartAgentRun({callGateway: gatewayCall,                    sendParams,                    runId,sessionKey: resolvedKey                });if (!result.okreturn result.result;                runId = result.runId;returnjsonResult({                    runId,status"sent",sessionKey: displayKey,                    delivery                });            } catch (err) {const msg = err instanceofError ? err.message : String(err);returnjsonResult({runId: crypto$1.randomUUID(),status"error",error: msg,sessionKey: displayKey                });            }        }    };}

1.4 Agent-to-Agent 策略检查

// 1. 检查是否启用if (!a2aPolicy.enabled) {returnjsonResult({status"forbidden",error"Agent-to-agent messaging is disabled."    });}// 2. 检查是否允许if (!a2aPolicy.isAllowed(requesterAgentId, requestedAgentId)) {returnjsonResult({status"forbidden",error"Agent-to-agent messaging denied by policy."    });}// 策略配置示例// tools.agentToAgent://   enabled: true//   allow://     - "agent-a" -> "agent-b"//     - "agent-a" -> "agent-c"

1.5 执行流程图

sessions_send 工具调用    ↓1. 解析消息(必填)    ↓2. 解析会话上下文    ↓3. 解析目标参数   ├─ sessionKey(直接指定)   └─ label(通过标签查找)    ↓4. 检查参数冲突(不能同时使用)    ↓5. 通过 label 解析 sessionKey   ├─ 沙盒限制检查   ├─ Agent-to-Agent 策略检查   └─ 调用 Gateway 解析    ↓6. 检查 sessionKey 是否存在    ↓7. 解析会话引用    ↓8. 检查可见性    ↓9. 解析超时    ↓10. 生成幂等键    ↓11. 可见性检查    ↓12. 构建发送参数    ↓13. 启动 A2A 流程    ↓14. 发送消息    ↓15. 返回结果

1.6 返回结果格式

成功

{"runId":"abc123","status":"sent","sessionKey":"main","delivery":{"status":"pending","mode":"announce"}}

失败(参数冲突)

{"runId":"abc123","status":"error","error":"Provide either sessionKey or label (not both)."}

失败(A2A 策略禁止)

{"runId":"abc123","status":"forbidden","error":"Agent-to-agent messaging is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent sends."}

二、sessions_spawn 工具

2.1 工具概述

功能:创建子 agent 会话核心特性

  • • 支持两种运行时(subagent / acp)
  • • 支持两种模式(run 单次 / session 持久)
  • • 支持恢复已有会话(仅 ACP)
  • • 支持附件(仅 subagent)
  • • 支持线程绑定(仅 ACP)
  • • 自动继承工作目录

2.2 Schema 定义

位置:第 112084 行

constSessionsSpawnToolSchema = Type.Object({taskType.String(),labelType.Optional(Type.String()),runtimeoptionalStringEnum$3(SESSIONS_SPAWN_RUNTIMES),  // "subagent" | "acp"agentIdType.Optional(Type.String()),resumeSessionIdType.Optional(Type.String({ description"Resume an existing agent session by its ID (e.g. a Codex session UUID). Requires runtime=\"acp\"."    })),modelType.Optional(Type.String()),thinkingType.Optional(Type.String()),cwdType.Optional(Type.String()),runTimeoutSecondsType.Optional(Type.Number({ minimum0 })),timeoutSecondsType.Optional(Type.Number({ minimum0 })),threadType.Optional(Type.Boolean()),modeoptionalStringEnum$3(SUBAGENT_SPAWN_MODES),  // "run" | "session"cleanupoptionalStringEnum$3(["delete""keep"]),sandboxoptionalStringEnum$3(SESSIONS_SPAWN_SANDBOX_MODES),  // "inherit" | "require"streamTooptionalStringEnum$3(ACP_SPAWN_STREAM_TARGETS),  // "parent"attachmentsType.Optional(Type.Array(Type.Object({nameType.String(),contentType.String(),encodingType.Optional(optionalStringEnum$3(["utf8""base64"])),mimeTypeType.Optional(Type.String())    }), { maxItems50 })),attachAsType.Optional(Type.Object({ mountPathType.Optional(Type.String())     }))});

2.3 完整执行代码

位置:第 112110 行

functioncreateSessionsSpawnTool(opts) {return {label"Sessions",name"sessions_spawn",description"Spawn an isolated session (runtime=\"subagent\" or runtime=\"acp\"). mode=\"run\" is one-shot and mode=\"session\" is persistent/thread-bound. Subagents inherit the parent workspace directory automatically.",parametersSessionsSpawnToolSchema,executeasync (_toolCallId, args) => {const params = args;// 1. 检查不支持的参数const unsupportedParam = UNSUPPORTED_SESSIONS_SPAWN_PARAM_KEYS.find((key) =>Object.hasOwn(params, key)            );if (unsupportedParam) {thrownewToolInputError(`sessions_spawn does not support "${unsupportedParam}". Use "message" or "sessions_send" for channel delivery.`                );            }// 2. 解析必填参数const task = readStringParam$1(params, "task", { requiredtrue });const label = typeof params.label === "string" ? params.label.trim() : "";// 3. 解析运行时const runtime = params.runtime === "acp" ? "acp" : "subagent";const requestedAgentId = readStringParam$1(params, "agentId");const resumeSessionId = readStringParam$1(params, "resumeSessionId");// 4. 解析模型和思考级别const modelOverride = readStringParam$1(params, "model");const thinkingOverrideRaw = readStringParam$1(params, "thinking");const cwd = readStringParam$1(params, "cwd");// 5. 解析模式const mode = params.mode === "run" || params.mode === "session" ? params.mode : void0;const cleanup = params.cleanup === "keep" || params.cleanup === "delete" ? params.cleanup : "keep";const sandbox = params.sandbox === "require" ? "require" : "inherit";const streamTo = params.streamTo === "parent" ? "parent" : void0;// 6. 解析超时const timeoutSecondsCandidate = typeof params.runTimeoutSeconds === "number" ?                 params.runTimeoutSeconds : typeof params.timeoutSeconds === "number" ? params.timeoutSeconds : void0;const runTimeoutSeconds = typeof timeoutSecondsCandidate === "number" && Number.isFinite(timeoutSecondsCandidate) ? Math.max(0Math.floor(timeoutSecondsCandidate)) : void0;// 7. 解析线程和附件const thread = params.thread === true;const attachments = Array.isArray(params.attachments) ? params.attachments : void0;// 8. 检查运行时特定限制if (streamTo && runtime !== "acp") {returnjsonResult({status"error",error`streamTo is only supported for runtime=acp; got runtime=${runtime}`                });            }if (resumeSessionId && runtime !== "acp") {returnjsonResult({status"error",error`resumeSessionId is only supported for runtime=acp; got runtime=${runtime}`                });            }// 9. ACP 运行时if (runtime === "acp") {if (Array.isArray(attachments) && attachments.length > 0) {returnjsonResult({status"error",error"attachments are currently unsupported for runtime=acp; use runtime=subagent or remove attachments"                    });                }returnjsonResult(awaitspawnAcpDirect({                    task,label: label || void0,agentId: requestedAgentId,                    resumeSessionId,                    cwd,mode: mode && ACP_SPAWN_MODES.includes(mode) ? mode : void0,                    thread,                    sandbox,                    streamTo                }, {agentSessionKey: opts?.agentSessionKey,agentChannel: opts?.agentChannel,agentAccountId: opts?.agentAccountId,agentTo: opts?.agentTo,agentThreadId: opts?.agentThreadId,sandboxed: opts?.sandboxed                }));            }// 10. Subagent 运行时returnjsonResult(awaitspawnSubagentDirect({                task,label: label || void0,agentId: requestedAgentId,model: modelOverride,thinking: thinkingOverrideRaw,                runTimeoutSeconds,                thread,                mode,                cleanup,                sandbox,expectsCompletionMessagetrue,                attachments,attachMountPath: params.attachAs && typeof params.attachAs === "object" ? readStringParam$1(params.attachAs"mountPath") : void0            }, {agentSessionKey: opts?.agentSessionKey,agentChannel: opts?.agentChannel,agentAccountId: opts?.agentAccountId,agentTo: opts?.agentTo,agentThreadId: opts?.agentThreadId,agentGroupId: opts?.agentGroupId,agentGroupChannel: opts?.agentGroupChannel,agentGroupSpace: opts?.agentGroupSpace,requesterAgentIdOverride: opts?.requesterAgentIdOverride,workspaceDir: opts?.workspaceDir            }));        }    };}

2.4 运行时对比

特性
subagent
acp
运行时类型
OpenClaw 子 agent
ACP 编码会话
附件支持
最多 50 个
不支持
恢复会话
不支持
resumeSessionId
流式输出
不支持
streamTo="parent"
线程绑定
支持
支持
模式
run / session
run / session

2.5 参数限制检查

// 1. streamTo 仅支持 ACPif (streamTo && runtime !== "acp") {returnjsonResult({status"error",error`streamTo is only supported for runtime=acp`    });}// 2. resumeSessionId 仅支持 ACPif (resumeSessionId && runtime !== "acp") {returnjsonResult({status"error",error`resumeSessionId is only supported for runtime=acp`    });}// 3. attachments 不支持 ACPif (runtime === "acp" && Array.isArray(attachments) && attachments.length > 0) {returnjsonResult({status"error",error"attachments are currently unsupported for runtime=acp"    });}

2.6 执行流程图

sessions_spawn 工具调用    ↓1. 检查不支持的参数    ↓2. 解析必填参数(task)    ↓3. 解析运行时(subagent / acp)    ↓4. 解析模型和思考级别    ↓5. 解析模式(run / session)    ↓6. 解析超时    ↓7. 解析线程和附件    ↓8. 检查运行时特定限制   ├─ streamTo 仅支持 ACP   ├─ resumeSessionId 仅支持 ACP   └─ attachments 不支持 ACP    ↓9. 根据运行时选择创建方式   ├─ ACP → spawnAcpDirect   └─ Subagent → spawnSubagentDirect    ↓10. 返回结果

2.7 返回结果格式

成功(Subagent)

{"status":"ok","runId":"abc123","childSessionKey":"subagent:abc123","label":"My Task"}

成功(ACP)

{"status":"ok","runId":"abc123","threadId":"thread_123","label":"My Task"}

失败(参数错误)

{"status":"error","error":"streamTo is only supported for runtime=acp; got runtime=subagent"}

三、关键机制对比

3.1 目标定位

特性
sessions_send
sessions_spawn
目标
已有会话
创建新会话
定位方式
sessionKey / label
不适用
A2A 策略
需要检查
不需要

3.2 运行时支持

特性
sessions_send
sessions_spawn
运行时
不适用
subagent / acp
附件
不支持
仅 subagent
恢复会话
不支持
仅 ACP

3.3 安全限制

限制类型
sessions_send
sessions_spawn
可见性检查
需要
不需要
A2A 策略
需要
不需要
沙盒限制
restrictToSpawned
sandbox 参数

四、使用示例

4.1 sessions_send 工具调用

用户发送消息到主会话

大模型返回

{"tool_call":{"name":"sessions_send","arguments":{"sessionKey":"main","message":"请处理这个任务"}}}

执行结果

{"runId":"abc123","status":"sent","sessionKey":"main","delivery":{"status":"pending","mode":"announce"}}

4.2 sessions_spawn 工具调用

用户创建一个子 agent 来分析股票数据

大模型返回

{"tool_call":{"name":"sessions_spawn","arguments":{"task":"分析 AAPL 股票数据","label":"股票分析","runtime":"subagent","mode":"run"}}}

执行结果

{"status":"ok","runId":"abc123","childSessionKey":"subagent:abc123","label":"股票分析"}

五、相关工具

5.1 sessions_yield

位置:第 112197 行

constSessionsYieldToolSchema = Type.Object({ messageType.Optional(Type.String()) });functioncreateSessionsYieldTool(opts) {return {label"Yield",name"sessions_yield",description"End your current turn. Use after spawning subagents to receive their results as the next message.",parametersSessionsYieldToolSchema,executeasync (_toolCallId, args) => {const message = readStringParam$1(args, "message") || "Turn yielded.";if (!opts?.sessionId) {returnjsonResult({ status"error"error"No session context" });            }if (!opts?.onYield) {returnjsonResult({ status"error"error"Yield not supported in this context" });            }await opts.onYield(message);returnjsonResult({status"yielded",                message            });        }    };}

用途:在 spawn 子 agent 后使用,结束当前回合,接收子 agent 结果作为下一条消息。