feat(web): stream tool summaries

This commit is contained in:
Peter Steinberger
2025-12-20 13:47:07 +00:00
parent 63b63cd66d
commit 70faa4ff36
5 changed files with 83 additions and 3 deletions

View File

@@ -32,7 +32,7 @@ read_when:
- Levels: `on|full` or `off` (default).
- Directive-only message toggles session verbose and replies `Verbose logging enabled.` / `Verbose logging disabled.`; invalid levels return a hint without changing state.
- Inline directive affects only that message; session/global defaults apply otherwise.
- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool result back as its own metadata-only message, prefixed with `[🛠️ <tool-name> <arg>]` when available (path/command); the tool output itself is not forwarded.
- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool result back as its own metadata-only message, prefixed with `[🛠️ <tool-name> <arg>]` when available (path/command); the tool output itself is not forwarded. These tool summaries are sent as soon as each tool finishes (separate bubbles), not as streaming deltas.
## Heartbeats
- Heartbeat probe body is `HEARTBEAT`. Inline directives in a heartbeat message apply as usual (but avoid changing session defaults from heartbeats).

View File

@@ -221,6 +221,10 @@ export async function runEmbeddedPiAgent(params: {
text?: string;
mediaUrls?: string[];
}) => void | Promise<void>;
onToolResult?: (payload: {
text?: string;
mediaUrls?: string[];
}) => void | Promise<void>;
onAgentEvent?: (evt: {
stream: string;
data: Record<string, unknown>;
@@ -415,6 +419,24 @@ export async function runEmbeddedPiAgent(params: {
isError,
},
});
if (params.verboseLevel === "on" && params.onToolResult) {
const agg = formatToolAggregate(
toolName,
meta ? [meta] : undefined,
);
const { text: cleanedText, mediaUrls } =
splitMediaFromOutput(agg);
if (cleanedText || (mediaUrls && mediaUrls.length > 0)) {
try {
void params.onToolResult({
text: cleanedText,
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
});
} catch {
// ignore tool result delivery failures
}
}
}
}
if (evt.type === "message_update") {
@@ -566,6 +588,7 @@ export async function runEmbeddedPiAgent(params: {
const inlineToolResults =
params.verboseLevel === "on" &&
!params.onPartialReply &&
!params.onToolResult &&
toolMetas.length > 0;
if (inlineToolResults) {
for (const { toolName, meta } of toolMetas) {

View File

@@ -760,6 +760,13 @@ export async function getReplyFromConfig(
mediaUrls: payload.mediaUrls,
})
: undefined,
onToolResult: opts?.onToolResult
? (payload) =>
opts.onToolResult?.({
text: payload.text,
mediaUrls: payload.mediaUrls,
})
: undefined,
});
const payloadArray = runResult.payloads ?? [];

View File

@@ -2,6 +2,7 @@ export type GetReplyOptions = {
onReplyStart?: () => Promise<void> | void;
isHeartbeat?: boolean;
onPartialReply?: (payload: ReplyPayload) => Promise<void> | void;
onToolResult?: (payload: ReplyPayload) => Promise<void> | void;
};
export type ReplyPayload = {

View File

@@ -887,6 +887,55 @@ export async function monitorWebProvider(
}
}
const responsePrefix = cfg.inbound?.responsePrefix;
let toolSendChain: Promise<void> = Promise.resolve();
const sendToolResult = (payload: ReplyPayload) => {
if (
!payload?.text &&
!payload?.mediaUrl &&
!(payload?.mediaUrls?.length ?? 0)
) {
return;
}
const toolPayload: ReplyPayload = { ...payload };
if (
responsePrefix &&
toolPayload.text &&
toolPayload.text.trim() !== HEARTBEAT_TOKEN &&
!toolPayload.text.startsWith(responsePrefix)
) {
toolPayload.text = `${responsePrefix} ${toolPayload.text}`;
}
toolSendChain = toolSendChain
.then(async () => {
await deliverWebReply({
replyResult: toolPayload,
msg,
maxMediaBytes,
replyLogger,
runtime,
connectionId,
skipLog: true,
});
if (toolPayload.text) {
recentlySent.add(toolPayload.text);
if (recentlySent.size > MAX_RECENT_MESSAGES) {
const firstKey = recentlySent.values().next().value;
if (firstKey) recentlySent.delete(firstKey);
}
}
})
.catch((err) => {
console.error(
danger(
`Failed sending web tool update to ${msg.from ?? conversationId}: ${String(
err,
)}`,
),
);
});
};
const replyResult = await (replyResolver ?? getReplyFromConfig)(
{
Body: combinedBody,
@@ -905,6 +954,7 @@ export async function monitorWebProvider(
},
{
onReplyStart: msg.sendComposing,
onToolResult: sendToolResult,
},
);
@@ -919,8 +969,7 @@ export async function monitorWebProvider(
return;
}
// Apply response prefix if configured (skip for HEARTBEAT_OK to preserve exact match)
const responsePrefix = cfg.inbound?.responsePrefix;
await toolSendChain;
for (const replyPayload of replyList) {
if (