Implementation:Openclaw Openclaw BuildEmbeddedRunPayloads
| Knowledge Sources | |
|---|---|
| Domains | Agent_Runtime, Messaging |
| Last Updated | 2026-02-06 12:00 GMT |
Overview
Concrete functions for constructing reply payloads from LLM output and dispatching them to channels, provided by the OpenClaw agent runtime.
Description
This implementation covers two complementary functions that together handle the reply delivery pipeline:
buildEmbeddedRunPayloads (in src/agents/pi-embedded-runner/run/payloads.ts) -- Transforms the raw outputs of an LLM inference run into an ordered array of reply payloads. It processes error text from the last assistant message, inline tool result metadata, reasoning/thinking text, assistant text segments (with reply directive parsing), and tool error information. The function applies deduplication to avoid surfacing raw API errors that are already covered by formatted error messages, and filters out silent replies and empty payloads.
createReplyDispatcher (in src/auto-reply/reply/reply-dispatcher.ts) -- Creates a serialized dispatch queue that channel handlers use to deliver reply payloads in order. The dispatcher provides three enqueue methods (sendToolResult, sendBlockReply, sendFinalReply) and a waitForIdle method. Each payload is normalized (response prefix applied, heartbeat tokens stripped, empty payloads filtered) before entering the promise chain for sequential delivery. Human-like delays are injected between block replies when configured.
Together, these functions form the complete path from raw LLM output to channel-delivered messages.
Usage
buildEmbeddedRunPayloads is called at the end of runEmbeddedPiAgent to construct the final payloads from accumulated assistant texts and tool metadata. createReplyDispatcher is called by channel handlers to set up the delivery pipeline before starting inference, and the dispatcher's methods are passed as callbacks to the inference engine.
Code Reference
Source Location
- Repository: openclaw
- File:
src/agents/pi-embedded-runner/run/payloads.ts(lines 23-255) andsrc/auto-reply/reply/reply-dispatcher.ts(lines 99-162)
Signature
// payloads.ts
export function buildEmbeddedRunPayloads(params: {
assistantTexts: string[];
toolMetas: ToolMetaEntry[];
lastAssistant: AssistantMessage | undefined;
lastToolError?: { toolName: string; meta?: string; error?: string };
config?: OpenClawConfig;
sessionKey: string;
verboseLevel?: VerboseLevel;
reasoningLevel?: ReasoningLevel;
toolResultFormat?: ToolResultFormat;
inlineToolResultsAllowed: boolean;
}): Array<{
text?: string;
mediaUrl?: string;
mediaUrls?: string[];
replyToId?: string;
isError?: boolean;
audioAsVoice?: boolean;
replyToTag?: boolean;
replyToCurrent?: boolean;
}>
// reply-dispatcher.ts
export function createReplyDispatcher(options: ReplyDispatcherOptions): ReplyDispatcher
Import
import { buildEmbeddedRunPayloads } from "../agents/pi-embedded-runner/run/payloads.js";
import { createReplyDispatcher } from "../auto-reply/reply/reply-dispatcher.js";
I/O Contract
Inputs (buildEmbeddedRunPayloads)
| Name | Type | Required | Description |
|---|---|---|---|
assistantTexts |
string[] |
Yes | Accumulated text segments from the assistant across all turns. |
toolMetas |
ToolMetaEntry[] |
Yes | Tool execution metadata entries ({ toolName, meta? }) for inline result formatting.
|
lastAssistant |
undefined | Yes | The final assistant message from the inference run, used for error text extraction and fallback text. |
lastToolError |
{ toolName: string; meta?: string; error?: string } |
No | The last tool error encountered, surfaced when no user-facing reply exists. |
config |
OpenClawConfig |
No | Runtime configuration for error formatting. |
sessionKey |
string |
Yes | Session key for error context. |
verboseLevel |
VerboseLevel |
No | Controls whether inline tool results are included ("off" suppresses them).
|
reasoningLevel |
ReasoningLevel |
No | When "on", includes reasoning/thinking text in the output.
|
toolResultFormat |
ToolResultFormat |
No | "markdown" or "plain" for tool result formatting.
|
inlineToolResultsAllowed |
boolean |
Yes | Whether inline tool results should be emitted (channel capability flag). |
Inputs (createReplyDispatcher)
| Name | Type | Required | Description |
|---|---|---|---|
deliver |
(payload: ReplyPayload, info: { kind: ReplyDispatchKind }) => Promise<void> |
Yes | The actual delivery function that sends a payload to the channel. |
responsePrefix |
string |
No | Text to prepend to the first reply payload. |
responsePrefixContext |
ResponsePrefixContext |
No | Static context for response prefix template interpolation. |
responsePrefixContextProvider |
() => ResponsePrefixContext |
No | Dynamic context provider called at normalization time. |
onHeartbeatStrip |
() => void |
No | Callback when a heartbeat token is stripped from a payload. |
onIdle |
() => void |
No | Callback when all pending deliveries have completed. |
onError |
(err, info) => void |
No | Error handler for failed deliveries. |
onSkip |
(payload, info) => void |
No | Callback when a payload is filtered during normalization. |
humanDelay |
HumanDelayConfig |
No | Configuration for human-like delays between block replies. |
Outputs (buildEmbeddedRunPayloads)
| Name | Type | Description |
|---|---|---|
| (return) | Array<{ text?, mediaUrl?, mediaUrls?, replyToId?, isError?, audioAsVoice?, replyToTag?, replyToCurrent? }> |
Ordered array of reply payloads ready for dispatch. |
Outputs (createReplyDispatcher)
| Name | Type | Description |
|---|---|---|
sendToolResult |
(payload: ReplyPayload) => boolean |
Enqueues a tool result payload. Returns true if enqueued, false if filtered.
|
sendBlockReply |
(payload: ReplyPayload) => boolean |
Enqueues a block (streaming chunk) reply. Returns true if enqueued, false if filtered.
|
sendFinalReply |
(payload: ReplyPayload) => boolean |
Enqueues the final reply payload. Returns true if enqueued, false if filtered.
|
waitForIdle |
() => Promise<void> |
Resolves when all pending deliveries have completed. |
getQueuedCounts |
() => Record<ReplyDispatchKind, number> |
Returns counts of enqueued payloads by kind (tool, block, final).
|
Usage Examples
Building Payloads
import { buildEmbeddedRunPayloads } from "../agents/pi-embedded-runner/run/payloads.js";
const payloads = buildEmbeddedRunPayloads({
assistantTexts: ["Here is the file listing:\n- README.md\n- package.json"],
toolMetas: [{ toolName: "exec", meta: "ls -la" }],
lastAssistant: assistantMessage,
sessionKey: "agent:default:telegram:dm:12345",
verboseLevel: "on",
toolResultFormat: "markdown",
inlineToolResultsAllowed: true,
});
for (const payload of payloads) {
console.log(payload.text);
}
Creating a Dispatcher
import { createReplyDispatcher } from "../auto-reply/reply/reply-dispatcher.js";
const dispatcher = createReplyDispatcher({
deliver: async (payload, { kind }) => {
await telegramBot.sendMessage(chatId, payload.text ?? "");
},
responsePrefix: "[Bot] ",
onIdle: () => {
console.log("All replies delivered");
},
onError: (err, { kind }) => {
console.error(`Failed to deliver ${kind} reply:`, err);
},
humanDelay: { mode: "on" },
});
// During inference, enqueue replies as they arrive
dispatcher.sendToolResult({ text: "Ran `ls`: 5 files found" });
dispatcher.sendFinalReply({ text: "Here are the files in your directory." });
// Wait for all deliveries to complete
await dispatcher.waitForIdle();