feat(web): stream tool summaries
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ?? [];
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user