From 34f62e9edb8d1d5511d0c3697227ad5be870f3c0 Mon Sep 17 00:00:00 2001 From: Roomote Date: Fri, 28 Aug 2026 14:46:43 +0000 Subject: [PATCH 1/2] refactor: centralize Fast inference attempt diagnostics --- .../__tests__/fast-agent-service.test.ts | 22 ++- .../fast-agent-turn-diagnostics.test.ts | 136 ++++++++++++-- .../fast-agent-context-telemetry.ts | 12 +- .../server/fast-agent/fast-agent-service.ts | 170 ++++++------------ .../fast-agent/fast-agent-turn-diagnostics.ts | 160 ++++++++++++++++- 5 files changed, 358 insertions(+), 142 deletions(-) diff --git a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-service.test.ts b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-service.test.ts index 5a3e84534..b7d4c7d10 100644 --- a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-service.test.ts +++ b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-service.test.ts @@ -2483,7 +2483,12 @@ describe('answerFastAgentQuestion native OpenCode tools', () => { }); it('posts an error closeout when the native OpenCode prompt fails', async () => { - mocks.generateText.mockRejectedValue(new Error('OpenCode unavailable')); + mocks.generateText.mockImplementation( + async (_params, _session, options) => { + options.onModelResolved?.('openrouter/openai/gpt-5.4'); + throw new Error('OpenCode unavailable'); + }, + ); const adapter = callbacks(); await expect( @@ -2493,6 +2498,13 @@ describe('answerFastAgentQuestion native OpenCode tools', () => { expect.objectContaining({ purpose: 'closeout' }), ); expect(mocks.invalidateSession).toHaveBeenCalledWith('conversation-1'); + expect(mocks.captureInferenceAttemptOutcome).toHaveBeenCalledWith( + expect.objectContaining({ + outcome: 'failure', + stage: 'opencode_setup', + resolvedModel: 'openrouter/openai/gpt-5.4', + }), + ); }); it('retries a gateway block from a clean compatibility bootstrap', async () => { @@ -3012,6 +3024,7 @@ describe('answerFastAgentQuestion native OpenCode tools', () => { it('reports OpenCode internal provider retries while the prompt is pending', async () => { mocks.generateText.mockImplementationOnce( async (params, _session, options) => { + options.onModelResolved?.('openrouter/openai/gpt-5.4'); await options.onSessionReady('opencode-session-1'); options.onPromptStarted?.(); await params.onProviderRetry?.({ @@ -3053,6 +3066,13 @@ describe('answerFastAgentQuestion native OpenCode tools', () => { providerRetryAttempt: 1, }), ); + expect(mocks.captureInferenceAttemptOutcome).toHaveBeenCalledWith( + expect.objectContaining({ + outcome: 'success', + stage: 'model_generation', + providerRetryEventCount: 1, + }), + ); }); it('bounds an initial prompt after OpenCode enters provider recovery', async () => { diff --git a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-turn-diagnostics.test.ts b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-turn-diagnostics.test.ts index 6df1ab19d..3eb2fbdd1 100644 --- a/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-turn-diagnostics.test.ts +++ b/packages/cloud-agents/src/server/fast-agent/__tests__/fast-agent-turn-diagnostics.test.ts @@ -1,3 +1,14 @@ +const telemetry = vi.hoisted(() => ({ + captureInferenceContext: vi.fn(), + captureInferenceAttemptOutcome: vi.fn(), +})); + +vi.mock('../fast-agent-context-telemetry', () => ({ + captureFastAgentInferenceContext: telemetry.captureInferenceContext, + captureFastAgentInferenceAttemptOutcome: + telemetry.captureInferenceAttemptOutcome, +})); + import { FastAgentTurnDiagnostics } from '../fast-agent-turn-diagnostics'; const conversation = { @@ -7,6 +18,34 @@ const conversation = { replyTarget: { channelId: 'channel-1', threadId: 'thread-1' }, }; +const inferenceContext = { + userId: 'user-1', + sessionId: 'session-1', + turnId: 'turn-1', + systemPrompt: 'private system prompt', + surface: 'slack' as const, + turnSource: 'human' as const, + platformEventHandling: 'default' as const, + platformEventKind: 'delegated_task' as const, + sessionPath: 'cold_rebuild' as const, + promptKind: 'bootstrap' as const, + releasePresent: true, + environmentCount: 1, + taskModelCount: 1, + activeTaskCount: 0, + integrationCount: 0, + integrationToolCount: 0, + memoryIntegrationCount: 0, + compatibilityMessageCount: 0, + suppliedThreadMessageCount: 0, + threadContextAttached: false, + senderContextPresent: true, + agentContextPresent: false, + inputImageCount: 0, + attachedImageCount: 0, + degradedComponents: [], +}; + function createTestDiagnostics(now: () => number) { const logger = { error: vi.fn(), @@ -35,6 +74,10 @@ function createTestDiagnostics(now: () => number) { } describe('FastAgentTurnDiagnostics', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it('separates session queue wait from provider inference time', () => { let currentTime = 1_000; const { diagnostics, logger } = createTestDiagnostics(() => currentTime); @@ -44,13 +87,15 @@ describe('FastAgentTurnDiagnostics', () => { diagnostics.markInferenceQueued(); currentTime = 1_040; diagnostics.markInferenceSetupStarted(); + const attempt = diagnostics.beginInferenceAttempt(inferenceContext); currentTime = 1_060; - diagnostics.markInferenceStarted(); - currentTime = 1_075; - diagnostics.recordSessionPath('cold_rebuild'); + attempt.recordModelResolved('openrouter/openai/gpt-test'); + attempt.recordPromptStarted(); diagnostics.recordOpenCodeSessionReady('opencode-session-1'); - diagnostics.recordOpenCodeProviderRetry(2, 'temporary upstream failure'); + currentTime = 1_075; + attempt.recordProviderRetry(2, 'temporary upstream failure'); currentTime = 1_100; + attempt.recordSuccess(); diagnostics.markInferenceFinished(); currentTime = 1_110; diagnostics.finish(); @@ -71,6 +116,23 @@ describe('FastAgentTurnDiagnostics', () => { expect(logMessage).toContain('sessionPath="cold_rebuild"'); expect(logMessage).toContain('openCodeSessionId="opencode-session-1"'); expect(logMessage).toContain('recoveredAfterOpenCodeProviderRetry=true'); + expect(telemetry.captureInferenceContext).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + attemptNumber: 1, + attemptScope: 'provider_retry', + providerRetryAttempt: 2, + }), + ); + expect(telemetry.captureInferenceAttemptOutcome).toHaveBeenCalledWith( + expect.objectContaining({ + attemptNumber: 1, + outcome: 'success', + stage: 'model_generation', + elapsedMs: 60, + providerRetryEventCount: 1, + }), + ); }); it('records bounded redacted context for each failed inference attempt', () => { @@ -78,16 +140,11 @@ describe('FastAgentTurnDiagnostics', () => { const secret = 'sk-provider-secret-1234567890'; diagnostics.setCanonicalConversationId('canonical-1'); - diagnostics.recordSessionPath('cold_rebuild'); - diagnostics.recordModelResolved('openrouter/openai/gpt-test'); - diagnostics.recordInferenceAttemptFailure({ - attemptNumber: 1, - promptKind: 'bootstrap', - stage: 'opencode_setup', - elapsedMs: 654, + const attempt = diagnostics.beginInferenceAttempt(inferenceContext); + attempt.recordModelResolved('openrouter/openai/gpt-test'); + attempt.recordFailure({ reason: 'endpoint_unreachable', retryable: true, - providerRetryEventCount: 0, error: new Error(`authorization: Bearer ${secret}`), }); @@ -100,6 +157,61 @@ describe('FastAgentTurnDiagnostics', () => { expect(logMessage).toContain('providerRetryEventCount=0'); expect(logMessage).toContain('[redacted]'); expect(logMessage).not.toContain(secret); + expect(telemetry.captureInferenceAttemptOutcome).toHaveBeenCalledWith( + expect.objectContaining({ + attemptNumber: 1, + outcome: 'failure', + stage: 'opencode_setup', + failureReason: 'endpoint_unreachable', + }), + ); + }); + + it('derives model-resolution and generation failure stages per attempt', () => { + let currentTime = 6_000; + const { diagnostics } = createTestDiagnostics(() => currentTime); + + const modelResolutionAttempt = + diagnostics.beginInferenceAttempt(inferenceContext); + currentTime = 6_010; + modelResolutionAttempt.recordFailure({ + reason: 'model_unavailable', + retryable: false, + error: new Error('model unavailable'), + }); + + const generationAttempt = diagnostics.beginInferenceAttempt({ + ...inferenceContext, + sessionPath: 'warm', + promptKind: 'turn_delta', + }); + generationAttempt.recordModelResolved('openrouter/openai/gpt-test'); + generationAttempt.recordPromptStarted(); + currentTime = 6_025; + generationAttempt.recordFailure({ + reason: 'provider_error', + retryable: true, + error: new Error('provider error'), + }); + + expect(telemetry.captureInferenceAttemptOutcome).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + attemptNumber: 1, + stage: 'model_resolution', + elapsedMs: 10, + }), + ); + expect(telemetry.captureInferenceAttemptOutcome).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + attemptNumber: 2, + sessionPath: 'warm', + promptKind: 'turn_delta', + stage: 'model_generation', + elapsedMs: 15, + }), + ); }); it('records completed and still-active native tools without their payloads', () => { diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-context-telemetry.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-context-telemetry.ts index df56c1932..9a58cc63e 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-context-telemetry.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-context-telemetry.ts @@ -11,7 +11,7 @@ import type { const FAST_AGENT_CONTEXT_MANIFEST_VERSION = 1; -type FastAgentSessionPath = +export type FastAgentSessionPath = | 'warm' | 'cold_resume' | 'cold_rebuild' @@ -35,7 +35,7 @@ const REQUIRED_SYSTEM_COMPONENTS = [ 'task_model_catalog', ] as const; -type CaptureFastAgentInferenceContextInput = { +export type CaptureFastAgentInferenceContextInput = { userId: string; sessionId: string; turnId: string; @@ -151,7 +151,7 @@ export function captureFastAgentInferenceContext( }); } -export function captureFastAgentInferenceAttemptOutcome(input: { +export type CaptureFastAgentInferenceAttemptOutcomeInput = { userId: string; sessionId: string; turnId: string; @@ -166,7 +166,11 @@ export function captureFastAgentInferenceAttemptOutcome(input: { failureRetryable?: boolean; resolvedModel?: string; providerRetryEventCount: number; -}): void { +}; + +export function captureFastAgentInferenceAttemptOutcome( + input: CaptureFastAgentInferenceAttemptOutcomeInput, +): void { void captureEvent('fast_agent_inference_attempt_outcome', { userId: input.userId, properties: { diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts index 4f8781bb0..7758dbebe 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-service.ts @@ -84,11 +84,7 @@ import { } from './fast-agent-tasks'; import { getFastAgentUserIdentity } from './fast-agent-user-identity'; import { FastAgentTurnDiagnostics } from './fast-agent-turn-diagnostics'; -import { - captureFastAgentInferenceAttemptOutcome, - captureFastAgentInferenceContext, - type FastAgentPromptKind, -} from './fast-agent-context-telemetry'; +import type { FastAgentPromptKind } from './fast-agent-context-telemetry'; import { RemoteFastAgentRepositorySkillSource } from './fast-agent-repository-skill-source'; import { FastAgentSkillStore } from './fast-agent-skill-store'; import { @@ -1229,7 +1225,6 @@ export async function answerFastAgentQuestion({ const reportProviderRetryEvent = async ( event: NonTaskProviderRetryEvent, ) => { - diagnostics.recordOpenCodeProviderRetry(event.attempt, event.message); await reportInferenceRetry({ failure: classifyNonTaskInferenceError(new Error(event.message)), attemptNumber: event.attempt, @@ -1813,7 +1808,38 @@ export async function answerFastAgentQuestion({ const serializedTurnPrompt = serializeFastAgentMessages(turnMessages); const serializedBootstrapPrompt = serializeFastAgentMessages(bootstrapMessages); - let inferenceAttemptNumber = 0; + const inferenceContext = { + userId, + sessionId: session.id, + turnId, + systemPrompt: system, + surface: conversation.surface, + turnSource, + platformEventHandling, + platformEventKind, + releasePresent: Boolean(releaseVersion), + environmentCount: availableEnvironments.length, + taskModelCount: taskModelOptions.models.length, + activeTaskCount: resolvedActiveTasks.length, + integrationCount: availableIntegrations.length, + integrationToolCount: availableIntegrations.reduce( + (count, integration) => count + integration.tools.length, + 0, + ), + memoryIntegrationCount: availableIntegrations.filter((integration) => + isMemoryMcpServer(integration.id), + ).length, + compatibilityMessageCount: session.compatibilityMessages.length, + suppliedThreadMessageCount: threadContext.length, + senderContextPresent: Boolean( + currentMessageSender?.slackUserId || + currentMessageSender?.displayName || + currentMessageSender?.githubLogin, + ), + agentContextPresent: Boolean(currentMessageAgentContext), + inputImageCount: imageFiles.length, + degradedComponents: [...degradedContextComponents], + }; const persistOpenCodeSession = async (openCodeSessionId: string) => { if (durableOpenCodeSessionId === openCodeSessionId) return; await setFastAgentOpenCodeSession({ @@ -1867,57 +1893,6 @@ export async function answerFastAgentQuestion({ : 'bootstrap'; let attemptSessionPath = sessionPath; let promptTimeoutMs: number | null = null; - let resolvedInferenceModel: string | undefined; - const captureInferenceContext = ( - attemptScope: 'prompt_submission' | 'provider_retry', - providerRetryAttempt?: number, - ) => { - captureFastAgentInferenceContext({ - userId, - sessionId: session.id, - turnId, - systemPrompt: system, - surface: conversation.surface, - turnSource, - platformEventHandling, - platformEventKind, - sessionPath: attemptSessionPath, - promptKind, - attemptNumber: inferenceAttemptNumber, - attemptScope, - providerRetryAttempt, - releasePresent: Boolean(releaseVersion), - environmentCount: availableEnvironments.length, - taskModelCount: taskModelOptions.models.length, - activeTaskCount: resolvedActiveTasks.length, - integrationCount: availableIntegrations.length, - integrationToolCount: availableIntegrations.reduce( - (count, integration) => count + integration.tools.length, - 0, - ), - memoryIntegrationCount: availableIntegrations.filter( - (integration) => isMemoryMcpServer(integration.id), - ).length, - compatibilityMessageCount: session.compatibilityMessages.length, - suppliedThreadMessageCount: threadContext.length, - threadContextAttached: - promptKind === 'bootstrap' || - promptKind === 'clean_retry_bootstrap' - ? bootstrapThreadContextPresent - : promptKind === 'turn_delta' - ? turnThreadContextPresent - : false, - senderContextPresent: Boolean( - currentMessageSender?.slackUserId || - currentMessageSender?.displayName || - currentMessageSender?.githubLogin, - ), - agentContextPresent: Boolean(currentMessageAgentContext), - inputImageCount: imageFiles.length, - attachedImageCount: imageFilesForAttempt.length, - degradedComponents: [...degradedContextComponents], - }); - }; const unbindMcpExecutor = bindFastAgentMcpToolExecutor( nativeRuntime.mcpCapability, executeMcpTool, @@ -1932,13 +1907,20 @@ export async function answerFastAgentQuestion({ let providerRetryTimeout: | ReturnType | undefined; - const attemptStartedAt = Date.now(); - let promptStarted = false; - let providerRetryEventCount = 0; + const attemptDiagnostics = diagnostics.beginInferenceAttempt({ + ...inferenceContext, + sessionPath: attemptSessionPath, + promptKind, + threadContextAttached: + promptKind === 'bootstrap' || + promptKind === 'clean_retry_bootstrap' + ? bootstrapThreadContextPresent + : promptKind === 'turn_delta' + ? turnThreadContextPresent + : false, + attachedImageCount: imageFilesForAttempt.length, + }); try { - inferenceAttemptNumber += 1; - resolvedInferenceModel = undefined; - captureInferenceContext('prompt_submission'); const resultPromise = generateTrackedNonTaskTextInOpenCodeSession( { @@ -1954,10 +1936,9 @@ export async function answerFastAgentQuestion({ system, prompt: promptForAttempt, onProviderRetry: async (event) => { - providerRetryEventCount += 1; - captureInferenceContext( - 'provider_retry', + attemptDiagnostics.recordProviderRetry( event.attempt, + event.message, ); // Initial turns stay unbounded unless the provider enters // recovery. Start this deadline once so repeated provider @@ -1999,15 +1980,13 @@ export async function answerFastAgentQuestion({ ), ), onModelResolved: (model) => { - resolvedInferenceModel = model; - diagnostics.recordModelResolved(model); + attemptDiagnostics.recordModelResolved(model); }, onMessageCompleted: (message) => { completedOpenCodeMessage = message; }, onPromptStarted: () => { - promptStarted = true; - diagnostics.markInferenceStarted(); + attemptDiagnostics.recordPromptStarted(); }, onSessionReady: async (openCodeSessionID) => { activeOpenCodeSessionId = openCodeSessionID; @@ -2055,57 +2034,13 @@ export async function answerFastAgentQuestion({ }, ); const result = await resultPromise; - captureFastAgentInferenceAttemptOutcome({ - userId, - sessionId: session.id, - turnId, - surface: conversation.surface, - sessionPath: attemptSessionPath, - promptKind, - attemptNumber: inferenceAttemptNumber, - outcome: 'success', - stage: !resolvedInferenceModel - ? 'model_resolution' - : promptStarted - ? 'model_generation' - : 'opencode_setup', - elapsedMs: Date.now() - attemptStartedAt, - resolvedModel: resolvedInferenceModel, - providerRetryEventCount, - }); + attemptDiagnostics.recordSuccess(); return result; } catch (error) { const failure = classifyNonTaskInferenceError(error); - const attemptStage = !resolvedInferenceModel - ? 'model_resolution' - : promptStarted - ? 'model_generation' - : 'opencode_setup'; - const attemptElapsedMs = Date.now() - attemptStartedAt; - captureFastAgentInferenceAttemptOutcome({ - userId, - sessionId: session.id, - turnId, - surface: conversation.surface, - sessionPath: attemptSessionPath, - promptKind, - attemptNumber: inferenceAttemptNumber, - outcome: 'failure', - stage: attemptStage, - elapsedMs: attemptElapsedMs, - failureReason: failure.reason, - failureRetryable: failure.retryable, - resolvedModel: resolvedInferenceModel, - providerRetryEventCount, - }); - diagnostics.recordInferenceAttemptFailure({ - attemptNumber: inferenceAttemptNumber, - promptKind, - stage: attemptStage, - elapsedMs: attemptElapsedMs, + attemptDiagnostics.recordFailure({ reason: failure.reason, retryable: failure.retryable, - providerRetryEventCount, error, }); throw error; @@ -2141,7 +2076,6 @@ export async function answerFastAgentQuestion({ imageFilesForAttempt = imageFiles; promptKind = 'clean_retry_bootstrap'; attemptSessionPath = 'cold_rebuild'; - diagnostics.recordSessionPath(attemptSessionPath); } // Keep every recovery attempt bounded so it cannot hold the // conversation lock forever if the provider stalls again. diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-turn-diagnostics.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-turn-diagnostics.ts index e76274b99..b62d27553 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-turn-diagnostics.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-turn-diagnostics.ts @@ -10,6 +10,11 @@ import type { FastAgentConversation, FastAgentTurnSource, } from './fast-agent-conversation'; +import { + captureFastAgentInferenceAttemptOutcome, + captureFastAgentInferenceContext, + type CaptureFastAgentInferenceContextInput, +} from './fast-agent-context-telemetry'; const MAX_TERMINAL_ERROR_LENGTH = 4_000; @@ -35,6 +40,23 @@ type NativeToolStats = { maxDurationMs: number; }; +type FastAgentInferenceAttemptContext = Omit< + CaptureFastAgentInferenceContextInput, + 'attemptNumber' | 'attemptScope' | 'providerRetryAttempt' +>; + +export type FastAgentInferenceAttemptDiagnostics = { + recordModelResolved: (model: string) => void; + recordPromptStarted: () => void; + recordProviderRetry: (attempt: number, error?: unknown) => void; + recordSuccess: () => void; + recordFailure: (input: { + reason: string; + retryable: boolean; + error: unknown; + }) => void; +}; + function formatTerminalError(error: unknown): string { const redacted = redactSecrets(formatErrorForLog(error)); return redacted.length <= MAX_TERMINAL_ERROR_LENGTH @@ -71,6 +93,7 @@ export class FastAgentTurnDiagnostics { private firstOpenCodeProviderRetryElapsedMs: number | undefined; private lastOpenCodeProviderRetryElapsedMs: number | undefined; private lastOpenCodeProviderRetryAttempt: number | undefined; + private inferenceAttemptCount = 0; private roomoteInferenceRetryCount = 0; private nativeToolCallCount = 0; private readonly nativeToolStats: Partial< @@ -103,10 +126,6 @@ export class FastAgentTurnDiagnostics { this.visibleReplyCount += 1; } - recordModelResolved(model: string): void { - this.resolvedModel = model; - } - recordSessionPath(path: string): void { this.sessionPath = path; } @@ -125,7 +144,7 @@ export class FastAgentTurnDiagnostics { this.inferenceSetupStartedAt ??= setupStartedAt; } - markInferenceStarted(): void { + private markInferenceStarted(): void { const inferenceStartedAt = this.now(); this.inferenceQueuedAt ??= inferenceStartedAt; this.inferenceSetupStartedAt ??= inferenceStartedAt; @@ -138,7 +157,134 @@ export class FastAgentTurnDiagnostics { } } - recordOpenCodeProviderRetry(attempt: number, error?: unknown): void { + beginInferenceAttempt( + context: FastAgentInferenceAttemptContext, + ): FastAgentInferenceAttemptDiagnostics { + const attemptNumber = ++this.inferenceAttemptCount; + const attemptStartedAt = this.now(); + let resolvedModel: string | undefined; + let promptStarted = false; + let providerRetryEventCount = 0; + let finished = false; + + this.sessionPath = context.sessionPath; + this.captureInferenceContext(context, attemptNumber, 'prompt_submission'); + + const finish = ( + outcome: + | { type: 'success' } + | { + type: 'failure'; + reason: string; + retryable: boolean; + error: unknown; + }, + ) => { + if (finished) return; + finished = true; + + const stage = !resolvedModel + ? 'model_resolution' + : promptStarted + ? 'model_generation' + : 'opencode_setup'; + const elapsedMs = this.now() - attemptStartedAt; + this.captureDiagnostic(() => + captureFastAgentInferenceAttemptOutcome({ + userId: context.userId, + sessionId: context.sessionId, + turnId: context.turnId, + surface: context.surface, + sessionPath: context.sessionPath, + promptKind: context.promptKind, + attemptNumber, + outcome: outcome.type, + stage, + elapsedMs, + ...(outcome.type === 'failure' + ? { + failureReason: outcome.reason, + failureRetryable: outcome.retryable, + } + : {}), + resolvedModel, + providerRetryEventCount, + }), + ); + + if (outcome.type === 'failure') { + this.captureDiagnostic(() => + this.writeInferenceAttemptFailure({ + attemptNumber, + promptKind: context.promptKind, + stage, + elapsedMs, + reason: outcome.reason, + retryable: outcome.retryable, + providerRetryEventCount, + error: outcome.error, + }), + ); + } + }; + + return { + recordModelResolved: (model) => { + resolvedModel = model; + this.resolvedModel = model; + }, + recordPromptStarted: () => { + promptStarted = true; + this.markInferenceStarted(); + }, + recordProviderRetry: (attempt, error) => { + providerRetryEventCount += 1; + this.captureInferenceContext( + context, + attemptNumber, + 'provider_retry', + attempt, + ); + this.captureDiagnostic(() => + this.writeOpenCodeProviderRetry(attempt, error), + ); + }, + recordSuccess: () => finish({ type: 'success' }), + recordFailure: (input) => finish({ type: 'failure', ...input }), + }; + } + + private captureInferenceContext( + context: FastAgentInferenceAttemptContext, + attemptNumber: number, + attemptScope: 'prompt_submission' | 'provider_retry', + providerRetryAttempt?: number, + ): void { + this.captureDiagnostic(() => + captureFastAgentInferenceContext({ + ...context, + attemptNumber, + attemptScope, + providerRetryAttempt, + }), + ); + } + + private captureDiagnostic(capture: () => void): void { + try { + capture(); + } catch (error) { + try { + this.logger.warn( + `[Fast Agent] Failed to record inference diagnostics: ${formatTerminalError(error)}`, + ); + } catch { + // Diagnostics must never replace the Fast turn's product result. + } + } + } + + private writeOpenCodeProviderRetry(attempt: number, error?: unknown): void { this.openCodeProviderRetryEventCount += 1; this.lastOpenCodeProviderRetryAttempt = attempt; @@ -166,7 +312,7 @@ export class FastAgentTurnDiagnostics { ); } - recordInferenceAttemptFailure(input: { + private writeInferenceAttemptFailure(input: { attemptNumber: number; promptKind: string; stage: string; From f38c710d13f5e0065e7241a519e16c9f90f7fc1c Mon Sep 17 00:00:00 2001 From: Roomote Date: Fri, 28 Aug 2026 14:48:28 +0000 Subject: [PATCH 2/2] chore: keep Fast diagnostics types internal --- .../src/server/fast-agent/fast-agent-context-telemetry.ts | 2 +- .../src/server/fast-agent/fast-agent-turn-diagnostics.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-context-telemetry.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-context-telemetry.ts index 9a58cc63e..9a98a9714 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-context-telemetry.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-context-telemetry.ts @@ -151,7 +151,7 @@ export function captureFastAgentInferenceContext( }); } -export type CaptureFastAgentInferenceAttemptOutcomeInput = { +type CaptureFastAgentInferenceAttemptOutcomeInput = { userId: string; sessionId: string; turnId: string; diff --git a/packages/cloud-agents/src/server/fast-agent/fast-agent-turn-diagnostics.ts b/packages/cloud-agents/src/server/fast-agent/fast-agent-turn-diagnostics.ts index b62d27553..097e30901 100644 --- a/packages/cloud-agents/src/server/fast-agent/fast-agent-turn-diagnostics.ts +++ b/packages/cloud-agents/src/server/fast-agent/fast-agent-turn-diagnostics.ts @@ -45,7 +45,7 @@ type FastAgentInferenceAttemptContext = Omit< 'attemptNumber' | 'attemptScope' | 'providerRetryAttempt' >; -export type FastAgentInferenceAttemptDiagnostics = { +type FastAgentInferenceAttemptDiagnostics = { recordModelResolved: (model: string) => void; recordPromptStarted: () => void; recordProviderRetry: (attempt: number, error?: unknown) => void;