1818// The endpoint is stateless: the caller supplies the full message history per request (no conversation store).
1919
2020import { PUBLIC_FIELD_BLOCKLIST } from "../track-record-summary.js" ;
21- import { resolveFirstConfiguredCodingAgentDriverName } from "./driver-factory.js" ;
21+ import { readAgentSdkResultUsage } from "./agent-sdk-driver.js" ;
22+ import { emitMinerAiGeneration } from "./ai-generation-sink.js" ;
23+ import { resolveCodingAgentTelemetryModel , resolveFirstConfiguredCodingAgentDriverName } from "./driver-factory.js" ;
2224
2325/**
2426 * The exact read-only tools this endpoint may call — one `server.registerTool(...)` call each in
@@ -42,6 +44,11 @@ export const CHAT_GROUNDING_TOOL_NAMES = Object.freeze([
4244/** The MCP server name the session registers the miner tools under. */
4345export const CHAT_GROUNDING_MCP_SERVER_NAME = "loopover-miner" ;
4446
47+ /** The only provider this endpoint can run on (boundary 1 above) — and therefore the provider id its
48+ * `$ai_generation` telemetry reports (#10200). One definition, consumed by both the fail-closed check and the
49+ * capture, so the two can never disagree about what is actually running. */
50+ export const CHAT_GROUNDING_PROVIDER = "agent-sdk" ;
51+
4552/** Ceiling on a single conversational session's tool-calling turns. */
4653const CHAT_MAX_TURNS = 12 ;
4754
@@ -201,7 +208,7 @@ export function resolveChatProviderError(
201208 "No coding-agent provider is configured. Chat requires the agent-sdk provider — set MINER_CODING_AGENT_PROVIDER=agent-sdk." ,
202209 } ;
203210 }
204- if ( provider !== "agent-sdk" ) {
211+ if ( provider !== CHAT_GROUNDING_PROVIDER ) {
205212 return {
206213 code : "chat_requires_agent_sdk_provider" ,
207214 message : `Chat requires the agent-sdk provider; the configured provider is ${ provider } , which is a single-turn, buffered coding driver.` ,
@@ -254,9 +261,35 @@ function* foldToolResultMessage(message: Record<string, unknown>): Generator<Cha
254261 }
255262}
256263
264+ /**
265+ * Why a completed session was not a successful generation, or `undefined` when it was. A stream that ends
266+ * without a `result` frame never reported completion, and one whose result frame is itself an error reported
267+ * failure — agent-sdk-driver.ts classifies exactly these two the same way (`agent_sdk_no_result` /
268+ * `agent_sdk_<subtype>`), and the telemetry must not call either of them a success.
269+ */
270+ function chatResultFailureReason ( resultMessage : Record < string , unknown > | null ) : string | undefined {
271+ if ( ! resultMessage ) return "chat_grounding_no_result" ;
272+ if ( resultMessage . is_error === true ) return "chat_grounding_errored" ;
273+ if ( resultMessage . subtype !== "success" ) {
274+ return `chat_grounding_${ typeof resultMessage . subtype === "string" ? resultMessage . subtype : "unknown" } ` ;
275+ }
276+ return undefined ;
277+ }
278+
257279/**
258280 * Drives one grounded conversational turn, yielding wire events. Never throws: an SDK failure becomes an `error`
259281 * event, and `done` always terminates the stream — including on the fail-closed provider paths.
282+ *
283+ * #10200: this drives a real `query()` session with a real turn budget, so it is real spend and reports an
284+ * `$ai_generation` through the host sink (ai-generation-sink.ts) on both the completed and the thrown path.
285+ * That also means folding the SDK's `result` frame, which this module previously discarded — it read only the
286+ * `assistant`/`user` messages it turns into wire events, so the session's own usage and cost were on the wire
287+ * and thrown away.
288+ *
289+ * The fail-closed provider path above deliberately emits NOTHING: no model was ever reached, so an
290+ * `$ai_generation` there would fabricate a generation that did not happen. That case is a request which produced
291+ * no generation — the shape the ORB side gives its own separate `selfhost_ai_degraded` event (#10186), which the
292+ * miner has no counterpart for yet.
260293 */
261294export async function * runChatGrounding (
262295 messages : ChatMessage [ ] ,
@@ -272,6 +305,9 @@ export async function* runChatGrounding(
272305
273306 const query = resolveChatQuery ( options ) ;
274307 const mcpServer = options . mcpServer ?? DEFAULT_MCP_SERVER ;
308+ const model = resolveCodingAgentTelemetryModel ( CHAT_GROUNDING_PROVIDER , env ) ;
309+ const startedAtMs = Date . now ( ) ;
310+ let resultMessage : Record < string , unknown > | null = null ;
275311 try {
276312 const stream = query ( {
277313 prompt : buildChatPrompt ( messages ) ,
@@ -289,9 +325,33 @@ export async function* runChatGrounding(
289325 }
290326 if ( message . type === "user" ) {
291327 yield * foldToolResultMessage ( message ) ;
328+ continue ;
292329 }
330+ // Kept, not re-emitted: the result frame carries usage/cost, never conversational content, so it feeds
331+ // the capture below and never becomes a wire event.
332+ if ( message . type === "result" ) resultMessage = message ;
293333 }
334+ const { tokens, costUsd } = readAgentSdkResultUsage ( resultMessage ) ;
335+ const failure = chatResultFailureReason ( resultMessage ) ;
336+ emitMinerAiGeneration ( {
337+ provider : CHAT_GROUNDING_PROVIDER ,
338+ model,
339+ latencyMs : Date . now ( ) - startedAtMs ,
340+ isError : failure !== undefined ,
341+ totalTokens : tokens . tokensUsed ,
342+ inputTokens : tokens . inputTokens ,
343+ outputTokens : tokens . outputTokens ,
344+ totalCostUsd : costUsd ,
345+ ...( failure === undefined ? { } : { error : new Error ( failure ) } ) ,
346+ } ) ;
294347 } catch ( error ) {
348+ emitMinerAiGeneration ( {
349+ provider : CHAT_GROUNDING_PROVIDER ,
350+ model,
351+ latencyMs : Date . now ( ) - startedAtMs ,
352+ isError : true ,
353+ error,
354+ } ) ;
295355 yield {
296356 type : "error" ,
297357 code : "chat_grounding_failed" ,
0 commit comments