feat: Thread usage data through reply pipeline to message_sent hook
- Add ReplyUsage type and onUsageAvailable callback to GetReplyOptions - Call onUsageAvailable in agent-runner.ts when usage is computed - Capture usage in dispatch-from-config.ts via callback ref - Pass usage data to message_sent hook for presence updates This completes the usage threading so Discord presence can show accurate per-message token counts.
This commit is contained in:
parent
f8db360d06
commit
6c0b886afe
@ -418,41 +418,58 @@ export async function runReplyAgent(params: {
|
|||||||
|
|
||||||
await signalTypingIfNeeded(replyPayloads, typingSignals);
|
await signalTypingIfNeeded(replyPayloads, typingSignals);
|
||||||
|
|
||||||
if (isDiagnosticsEnabled(cfg) && hasNonzeroUsage(usage)) {
|
// Compute usage stats and notify via callback if available
|
||||||
|
if (hasNonzeroUsage(usage)) {
|
||||||
const input = usage.input ?? 0;
|
const input = usage.input ?? 0;
|
||||||
const output = usage.output ?? 0;
|
const output = usage.output ?? 0;
|
||||||
const cacheRead = usage.cacheRead ?? 0;
|
const cacheRead = usage.cacheRead ?? 0;
|
||||||
const cacheWrite = usage.cacheWrite ?? 0;
|
const cacheWrite = usage.cacheWrite ?? 0;
|
||||||
const promptTokens = input + cacheRead + cacheWrite;
|
const promptTokens = input + cacheRead + cacheWrite;
|
||||||
const totalTokens = usage.total ?? promptTokens + output;
|
const totalTokens = usage.total ?? promptTokens + output;
|
||||||
const costConfig = resolveModelCostConfig({
|
const durationMs = Date.now() - runStartedAt;
|
||||||
provider: providerUsed,
|
|
||||||
|
// Notify usage callback (for presence updates, analytics, etc.)
|
||||||
|
opts?.onUsageAvailable?.({
|
||||||
|
inputTokens: input,
|
||||||
|
outputTokens: output,
|
||||||
|
totalTokens,
|
||||||
|
cacheReadTokens: cacheRead,
|
||||||
|
cacheWriteTokens: cacheWrite,
|
||||||
model: modelUsed,
|
model: modelUsed,
|
||||||
config: cfg,
|
|
||||||
});
|
|
||||||
const costUsd = estimateUsageCost({ usage, cost: costConfig });
|
|
||||||
emitDiagnosticEvent({
|
|
||||||
type: "model.usage",
|
|
||||||
sessionKey,
|
|
||||||
sessionId: followupRun.run.sessionId,
|
|
||||||
channel: replyToChannel,
|
|
||||||
provider: providerUsed,
|
provider: providerUsed,
|
||||||
model: modelUsed,
|
durationMs,
|
||||||
usage: {
|
|
||||||
input,
|
|
||||||
output,
|
|
||||||
cacheRead,
|
|
||||||
cacheWrite,
|
|
||||||
promptTokens,
|
|
||||||
total: totalTokens,
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
limit: contextTokensUsed,
|
|
||||||
used: totalTokens,
|
|
||||||
},
|
|
||||||
costUsd,
|
|
||||||
durationMs: Date.now() - runStartedAt,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isDiagnosticsEnabled(cfg)) {
|
||||||
|
const costConfig = resolveModelCostConfig({
|
||||||
|
provider: providerUsed,
|
||||||
|
model: modelUsed,
|
||||||
|
config: cfg,
|
||||||
|
});
|
||||||
|
const costUsd = estimateUsageCost({ usage, cost: costConfig });
|
||||||
|
emitDiagnosticEvent({
|
||||||
|
type: "model.usage",
|
||||||
|
sessionKey,
|
||||||
|
sessionId: followupRun.run.sessionId,
|
||||||
|
channel: replyToChannel,
|
||||||
|
provider: providerUsed,
|
||||||
|
model: modelUsed,
|
||||||
|
usage: {
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
cacheRead,
|
||||||
|
cacheWrite,
|
||||||
|
promptTokens,
|
||||||
|
total: totalTokens,
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
limit: contextTokensUsed,
|
||||||
|
used: totalTokens,
|
||||||
|
},
|
||||||
|
costUsd,
|
||||||
|
durationMs,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseUsageRaw =
|
const responseUsageRaw =
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||||
import { getReplyFromConfig } from "../reply.js";
|
import { getReplyFromConfig } from "../reply.js";
|
||||||
import type { FinalizedMsgContext } from "../templating.js";
|
import type { FinalizedMsgContext } from "../templating.js";
|
||||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
import type { GetReplyOptions, ReplyPayload, ReplyUsage } from "../types.js";
|
||||||
import { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";
|
import { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";
|
||||||
import { shouldSkipDuplicateInbound } from "./inbound-dedupe.js";
|
import { shouldSkipDuplicateInbound } from "./inbound-dedupe.js";
|
||||||
import type { ReplyDispatcher, ReplyDispatchKind } from "./reply-dispatcher.js";
|
import type { ReplyDispatcher, ReplyDispatchKind } from "./reply-dispatcher.js";
|
||||||
@ -266,10 +266,18 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
return { queuedFinal, counts };
|
return { queuedFinal, counts };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mutable ref to capture usage stats from the agent run
|
||||||
|
const usageRef: { value?: ReplyUsage } = {};
|
||||||
|
|
||||||
const replyResult = await (params.replyResolver ?? getReplyFromConfig)(
|
const replyResult = await (params.replyResolver ?? getReplyFromConfig)(
|
||||||
ctx,
|
ctx,
|
||||||
{
|
{
|
||||||
...params.replyOptions,
|
...params.replyOptions,
|
||||||
|
onUsageAvailable: (usage) => {
|
||||||
|
usageRef.value = usage;
|
||||||
|
// Also call the original callback if provided
|
||||||
|
params.replyOptions?.onUsageAvailable?.(usage);
|
||||||
|
},
|
||||||
onBlockReply: (payload: ReplyPayload, context) => {
|
onBlockReply: (payload: ReplyPayload, context) => {
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
const ttsPayload = await maybeApplyTtsToPayload({
|
const ttsPayload = await maybeApplyTtsToPayload({
|
||||||
@ -332,13 +340,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
const counts = dispatcher.getQueuedCounts();
|
const counts = dispatcher.getQueuedCounts();
|
||||||
counts.final += routedFinalCount;
|
counts.final += routedFinalCount;
|
||||||
|
|
||||||
// Trigger message_sent hook for final replies
|
// Trigger message_sent hook for final replies with usage stats
|
||||||
// NOTE: Usage stats (tokens, model, etc.) are not currently available here.
|
|
||||||
// They are computed in agent-runner.ts but not returned through the reply pipeline.
|
|
||||||
// To enable full usage tracking, getReplyFromConfig/replyResolver would need to
|
|
||||||
// return usage data alongside the reply text. For now, hooks receive the message
|
|
||||||
// content without usage stats - presence managers can track cumulative tokens
|
|
||||||
// via other means (e.g., diagnostic events or session metadata).
|
|
||||||
if (hookRunner?.hasHooks("message_sent") && replies.length > 0) {
|
if (hookRunner?.hasHooks("message_sent") && replies.length > 0) {
|
||||||
const targetSessionKey =
|
const targetSessionKey =
|
||||||
ctx.CommandSource === "native" ? ctx.CommandTargetSessionKey?.trim() : undefined;
|
ctx.CommandSource === "native" ? ctx.CommandTargetSessionKey?.trim() : undefined;
|
||||||
@ -349,6 +351,9 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
const channelId = (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "").toLowerCase();
|
const channelId = (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider ?? "").toLowerCase();
|
||||||
const conversationId = ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? undefined;
|
const conversationId = ctx.OriginatingTo ?? ctx.To ?? ctx.From ?? undefined;
|
||||||
|
|
||||||
|
// Get captured usage stats from the agent run
|
||||||
|
const usage = usageRef.value;
|
||||||
|
|
||||||
for (const reply of replies) {
|
for (const reply of replies) {
|
||||||
void hookRunner
|
void hookRunner
|
||||||
.runMessageSent(
|
.runMessageSent(
|
||||||
@ -356,6 +361,19 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
to: conversationId ?? "",
|
to: conversationId ?? "",
|
||||||
content: reply.text ?? "",
|
content: reply.text ?? "",
|
||||||
success: queuedFinal,
|
success: queuedFinal,
|
||||||
|
// Include usage stats if available
|
||||||
|
usage: usage
|
||||||
|
? {
|
||||||
|
inputTokens: usage.inputTokens,
|
||||||
|
outputTokens: usage.outputTokens,
|
||||||
|
totalTokens: usage.totalTokens,
|
||||||
|
cacheReadTokens: usage.cacheReadTokens,
|
||||||
|
cacheCreationTokens: usage.cacheWriteTokens,
|
||||||
|
model: usage.model,
|
||||||
|
provider: usage.provider,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
durationMs: usage?.durationMs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
channelId,
|
channelId,
|
||||||
|
|||||||
@ -32,6 +32,8 @@ export type GetReplyOptions = {
|
|||||||
/** Called when the actual model is selected (including after fallback).
|
/** Called when the actual model is selected (including after fallback).
|
||||||
* Use this to get model/provider/thinkLevel for responsePrefix template interpolation. */
|
* Use this to get model/provider/thinkLevel for responsePrefix template interpolation. */
|
||||||
onModelSelected?: (ctx: ModelSelectedContext) => void;
|
onModelSelected?: (ctx: ModelSelectedContext) => void;
|
||||||
|
/** Called when usage statistics are available after the agent run completes. */
|
||||||
|
onUsageAvailable?: (usage: ReplyUsage) => void;
|
||||||
disableBlockStreaming?: boolean;
|
disableBlockStreaming?: boolean;
|
||||||
/** Timeout for block reply delivery (ms). */
|
/** Timeout for block reply delivery (ms). */
|
||||||
blockReplyTimeoutMs?: number;
|
blockReplyTimeoutMs?: number;
|
||||||
@ -55,3 +57,21 @@ export type ReplyPayload = {
|
|||||||
/** Channel-specific payload data (per-channel envelope). */
|
/** Channel-specific payload data (per-channel envelope). */
|
||||||
channelData?: Record<string, unknown>;
|
channelData?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Usage statistics from an agent run. */
|
||||||
|
export type ReplyUsage = {
|
||||||
|
inputTokens?: number;
|
||||||
|
outputTokens?: number;
|
||||||
|
totalTokens?: number;
|
||||||
|
cacheReadTokens?: number;
|
||||||
|
cacheWriteTokens?: number;
|
||||||
|
model?: string;
|
||||||
|
provider?: string;
|
||||||
|
durationMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Result from getReplyFromConfig including reply payload(s) and usage stats. */
|
||||||
|
export type ReplyResult = {
|
||||||
|
replies: ReplyPayload[];
|
||||||
|
usage?: ReplyUsage;
|
||||||
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user