feat(web): stream tool summaries
This commit is contained in:
@@ -32,7 +32,7 @@ read_when:
|
|||||||
- Levels: `on|full` or `off` (default).
|
- 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.
|
- 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.
|
- 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
|
## Heartbeats
|
||||||
- Heartbeat probe body is `HEARTBEAT`. Inline directives in a heartbeat message apply as usual (but avoid changing session defaults from 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;
|
text?: string;
|
||||||
mediaUrls?: string[];
|
mediaUrls?: string[];
|
||||||
}) => void | Promise<void>;
|
}) => void | Promise<void>;
|
||||||
|
onToolResult?: (payload: {
|
||||||
|
text?: string;
|
||||||
|
mediaUrls?: string[];
|
||||||
|
}) => void | Promise<void>;
|
||||||
onAgentEvent?: (evt: {
|
onAgentEvent?: (evt: {
|
||||||
stream: string;
|
stream: string;
|
||||||
data: Record<string, unknown>;
|
data: Record<string, unknown>;
|
||||||
@@ -415,6 +419,24 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
isError,
|
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") {
|
if (evt.type === "message_update") {
|
||||||
@@ -566,6 +588,7 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
const inlineToolResults =
|
const inlineToolResults =
|
||||||
params.verboseLevel === "on" &&
|
params.verboseLevel === "on" &&
|
||||||
!params.onPartialReply &&
|
!params.onPartialReply &&
|
||||||
|
!params.onToolResult &&
|
||||||
toolMetas.length > 0;
|
toolMetas.length > 0;
|
||||||
if (inlineToolResults) {
|
if (inlineToolResults) {
|
||||||
for (const { toolName, meta } of toolMetas) {
|
for (const { toolName, meta } of toolMetas) {
|
||||||
|
|||||||
@@ -760,6 +760,13 @@ export async function getReplyFromConfig(
|
|||||||
mediaUrls: payload.mediaUrls,
|
mediaUrls: payload.mediaUrls,
|
||||||
})
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
|
onToolResult: opts?.onToolResult
|
||||||
|
? (payload) =>
|
||||||
|
opts.onToolResult?.({
|
||||||
|
text: payload.text,
|
||||||
|
mediaUrls: payload.mediaUrls,
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const payloadArray = runResult.payloads ?? [];
|
const payloadArray = runResult.payloads ?? [];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export type GetReplyOptions = {
|
|||||||
onReplyStart?: () => Promise<void> | void;
|
onReplyStart?: () => Promise<void> | void;
|
||||||
isHeartbeat?: boolean;
|
isHeartbeat?: boolean;
|
||||||
onPartialReply?: (payload: ReplyPayload) => Promise<void> | void;
|
onPartialReply?: (payload: ReplyPayload) => Promise<void> | void;
|
||||||
|
onToolResult?: (payload: ReplyPayload) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReplyPayload = {
|
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)(
|
const replyResult = await (replyResolver ?? getReplyFromConfig)(
|
||||||
{
|
{
|
||||||
Body: combinedBody,
|
Body: combinedBody,
|
||||||
@@ -905,6 +954,7 @@ export async function monitorWebProvider(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onReplyStart: msg.sendComposing,
|
onReplyStart: msg.sendComposing,
|
||||||
|
onToolResult: sendToolResult,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -919,8 +969,7 @@ export async function monitorWebProvider(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply response prefix if configured (skip for HEARTBEAT_OK to preserve exact match)
|
await toolSendChain;
|
||||||
const responsePrefix = cfg.inbound?.responsePrefix;
|
|
||||||
|
|
||||||
for (const replyPayload of replyList) {
|
for (const replyPayload of replyList) {
|
||||||
if (
|
if (
|
||||||
|
|||||||
Reference in New Issue
Block a user