| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689 |
- /**
- * Helper for running forked agent query loops with usage tracking.
- *
- * This utility ensures forked agents:
- * 1. Share identical cache-critical params with the parent to guarantee prompt cache hits
- * 2. Track full usage metrics across the entire query loop
- * 3. Log metrics via the tengu_fork_agent_query event when complete
- * 4. Isolate mutable state to prevent interference with the main agent loop
- */
- import type { UUID } from 'crypto'
- import { randomUUID } from 'crypto'
- import type { PromptCommand } from '../commands.js'
- import type { QuerySource } from '../constants/querySource.js'
- import type { CanUseToolFn } from '../hooks/useCanUseTool.js'
- import { query } from '../query.js'
- import {
- type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- logEvent,
- } from '../services/analytics/index.js'
- import { accumulateUsage, updateUsage } from '../services/api/claude.js'
- import { EMPTY_USAGE, type NonNullableUsage } from '../services/api/logging.js'
- import type { ToolUseContext } from '../Tool.js'
- import type { AgentDefinition } from '../tools/AgentTool/loadAgentsDir.js'
- import type { AgentId } from '../types/ids.js'
- import type { Message } from '../types/message.js'
- import { createChildAbortController } from './abortController.js'
- import { logForDebugging } from './debug.js'
- import { cloneFileStateCache } from './fileStateCache.js'
- import type { REPLHookContext } from './hooks/postSamplingHooks.js'
- import {
- createUserMessage,
- extractTextContent,
- getLastAssistantMessage,
- } from './messages.js'
- import { createDenialTrackingState } from './permissions/denialTracking.js'
- import { parseToolListFromCLI } from './permissions/permissionSetup.js'
- import { recordSidechainTranscript } from './sessionStorage.js'
- import type { SystemPrompt } from './systemPromptType.js'
- import {
- type ContentReplacementState,
- cloneContentReplacementState,
- } from './toolResultStorage.js'
- import { createAgentId } from './uuid.js'
- /**
- * Parameters that must be identical between the fork and parent API requests
- * to share the parent's prompt cache. The Anthropic API cache key is composed of:
- * system prompt, tools, model, messages (prefix), and thinking config.
- *
- * CacheSafeParams carries the first five. Thinking config is derived from the
- * inherited toolUseContext.options.thinkingConfig — but can be inadvertently
- * changed if the fork sets maxOutputTokens, which clamps budget_tokens in
- * claude.ts (but only for older models that do not use adaptive thinking).
- * See the maxOutputTokens doc on ForkedAgentParams.
- */
- export type CacheSafeParams = {
- /** System prompt - must match parent for cache hits */
- systemPrompt: SystemPrompt
- /** User context - prepended to messages, affects cache */
- userContext: { [k: string]: string }
- /** System context - appended to system prompt, affects cache */
- systemContext: { [k: string]: string }
- /** Tool use context containing tools, model, and other options */
- toolUseContext: ToolUseContext
- /** Parent context messages for prompt cache sharing */
- forkContextMessages: Message[]
- }
- // Slot written by handleStopHooks after each turn so post-turn forks
- // (promptSuggestion, postTurnSummary, /btw) can share the main loop's
- // prompt cache without each caller threading params through.
- let lastCacheSafeParams: CacheSafeParams | null = null
- export function saveCacheSafeParams(params: CacheSafeParams | null): void {
- lastCacheSafeParams = params
- }
- export function getLastCacheSafeParams(): CacheSafeParams | null {
- return lastCacheSafeParams
- }
- export type ForkedAgentParams = {
- /** Messages to start the forked query loop with */
- promptMessages: Message[]
- /** Cache-safe parameters that must match the parent query */
- cacheSafeParams: CacheSafeParams
- /** Permission check function for the forked agent */
- canUseTool: CanUseToolFn
- /** Source identifier for tracking */
- querySource: QuerySource
- /** Label for analytics (e.g., 'session_memory', 'supervisor') */
- forkLabel: string
- /** Optional overrides for the subagent context (e.g., readFileState from setup phase) */
- overrides?: SubagentContextOverrides
- /**
- * Optional cap on output tokens. CAUTION: setting this changes both max_tokens
- * AND budget_tokens (via clamping in claude.ts). If the fork uses cacheSafeParams
- * to share the parent's prompt cache, a different budget_tokens will invalidate
- * the cache — thinking config is part of the cache key. Only set this when cache
- * sharing is not a goal (e.g., compact summaries).
- */
- maxOutputTokens?: number
- /** Optional cap on number of turns (API round-trips) */
- maxTurns?: number
- /** Optional callback invoked for each message as it arrives (for streaming UI) */
- onMessage?: (message: Message) => void
- /** Skip sidechain transcript recording (e.g., for ephemeral work like speculation) */
- skipTranscript?: boolean
- /** Skip writing new prompt cache entries on the last message. For
- * fire-and-forget forks where no future request will read from this prefix. */
- skipCacheWrite?: boolean
- }
- export type ForkedAgentResult = {
- /** All messages yielded during the query loop */
- messages: Message[]
- /** Accumulated usage across all API calls in the loop */
- totalUsage: NonNullableUsage
- }
- /**
- * Creates CacheSafeParams from REPLHookContext.
- * Use this helper when forking from a post-sampling hook context.
- *
- * To override specific fields (e.g., toolUseContext with cloned file state),
- * spread the result and override: `{ ...createCacheSafeParams(context), toolUseContext: clonedContext }`
- *
- * @param context - The REPLHookContext from the post-sampling hook
- */
- export function createCacheSafeParams(
- context: REPLHookContext,
- ): CacheSafeParams {
- return {
- systemPrompt: context.systemPrompt,
- userContext: context.userContext,
- systemContext: context.systemContext,
- toolUseContext: context.toolUseContext,
- forkContextMessages: context.messages,
- }
- }
- /**
- * Creates a modified getAppState that adds allowed tools to the permission context.
- * This is used by forked skill/command execution to grant tool permissions.
- */
- export function createGetAppStateWithAllowedTools(
- baseGetAppState: ToolUseContext['getAppState'],
- allowedTools: string[],
- ): ToolUseContext['getAppState'] {
- if (allowedTools.length === 0) return baseGetAppState
- return () => {
- const appState = baseGetAppState()
- return {
- ...appState,
- toolPermissionContext: {
- ...appState.toolPermissionContext,
- alwaysAllowRules: {
- ...appState.toolPermissionContext.alwaysAllowRules,
- command: [
- ...new Set([
- ...(appState.toolPermissionContext.alwaysAllowRules.command ||
- []),
- ...allowedTools,
- ]),
- ],
- },
- },
- }
- }
- }
- /**
- * Result from preparing a forked command context.
- */
- export type PreparedForkedContext = {
- /** Skill content with args replaced */
- skillContent: string
- /** Modified getAppState with allowed tools */
- modifiedGetAppState: ToolUseContext['getAppState']
- /** The general-purpose agent to use */
- baseAgent: AgentDefinition
- /** Initial prompt messages */
- promptMessages: Message[]
- }
- /**
- * Prepares the context for executing a forked command/skill.
- * This handles the common setup that both SkillTool and slash commands need.
- */
- export async function prepareForkedCommandContext(
- command: PromptCommand,
- args: string,
- context: ToolUseContext,
- ): Promise<PreparedForkedContext> {
- // Get skill content with $ARGUMENTS replaced
- const skillPrompt = await command.getPromptForCommand(args, context)
- const skillContent = skillPrompt
- .map(block => (block.type === 'text' ? block.text : ''))
- .join('\n')
- // Parse and prepare allowed tools
- const allowedTools = parseToolListFromCLI(command.allowedTools ?? [])
- // Create modified context with allowed tools
- const modifiedGetAppState = createGetAppStateWithAllowedTools(
- context.getAppState,
- allowedTools,
- )
- // Use command.agent if specified, otherwise 'general-purpose'
- const agentTypeName = command.agent ?? 'general-purpose'
- const agents = context.options.agentDefinitions.activeAgents
- const baseAgent =
- agents.find(a => a.agentType === agentTypeName) ??
- agents.find(a => a.agentType === 'general-purpose') ??
- agents[0]
- if (!baseAgent) {
- throw new Error('No agent available for forked execution')
- }
- // Prepare prompt messages
- const promptMessages = [createUserMessage({ content: skillContent })]
- return {
- skillContent,
- modifiedGetAppState,
- baseAgent,
- promptMessages,
- }
- }
- /**
- * Extracts result text from agent messages.
- */
- export function extractResultText(
- agentMessages: Message[],
- defaultText = 'Execution completed',
- ): string {
- const lastAssistantMessage = getLastAssistantMessage(agentMessages)
- if (!lastAssistantMessage) return defaultText
- const textContent = extractTextContent(
- Array.isArray(lastAssistantMessage.message.content) ? lastAssistantMessage.message.content : [],
- '\n',
- )
- return textContent || defaultText
- }
- /**
- * Options for creating a subagent context.
- *
- * By default, all mutable state is isolated to prevent interference with the parent.
- * Use these options to:
- * - Override specific fields (e.g., custom options, agentId, messages)
- * - Explicitly opt-in to sharing specific callbacks (for interactive subagents)
- */
- export type SubagentContextOverrides = {
- /** Override the options object (e.g., custom tools, model) */
- options?: ToolUseContext['options']
- /** Override the agentId (for subagents with their own ID) */
- agentId?: AgentId
- /** Override the agentType (for subagents with a specific type) */
- agentType?: string
- /** Override the messages array */
- messages?: Message[]
- /** Override the readFileState (e.g., fresh cache instead of clone) */
- readFileState?: ToolUseContext['readFileState']
- /** Override the abortController */
- abortController?: AbortController
- /** Override the getAppState function */
- getAppState?: ToolUseContext['getAppState']
- /**
- * Explicit opt-in to share parent's setAppState callback.
- * Use for interactive subagents that need to update shared state.
- * @default false (isolated no-op)
- */
- shareSetAppState?: boolean
- /**
- * Explicit opt-in to share parent's setResponseLength callback.
- * Use for subagents that contribute to parent's response metrics.
- * @default false (isolated no-op)
- */
- shareSetResponseLength?: boolean
- /**
- * Explicit opt-in to share parent's abortController.
- * Use for interactive subagents that should abort with parent.
- * Note: Only applies if abortController override is not provided.
- * @default false (new controller linked to parent)
- */
- shareAbortController?: boolean
- /** Critical system reminder to re-inject at every user turn */
- criticalSystemReminder_EXPERIMENTAL?: string
- /** When true, canUseTool must always be called even when hooks auto-approve.
- * Used by speculation for overlay file path rewriting. */
- requireCanUseTool?: boolean
- /** Override replacement state — used by resumeAgentBackground to thread
- * state reconstructed from the resumed sidechain so the same results
- * are re-replaced (prompt cache stability). */
- contentReplacementState?: ContentReplacementState
- }
- /**
- * Creates an isolated ToolUseContext for subagents.
- *
- * By default, ALL mutable state is isolated to prevent interference:
- * - readFileState: cloned from parent
- * - abortController: new controller linked to parent (parent abort propagates)
- * - getAppState: wrapped to set shouldAvoidPermissionPrompts
- * - All mutation callbacks (setAppState, etc.): no-op
- * - Fresh collections: nestedMemoryAttachmentTriggers, toolDecisions
- *
- * Callers can:
- * - Override specific fields via the overrides parameter
- * - Explicitly opt-in to sharing specific callbacks (shareSetAppState, etc.)
- *
- * @param parentContext - The parent's ToolUseContext to create subagent context from
- * @param overrides - Optional overrides and sharing options
- *
- * @example
- * // Full isolation (for background agents like session memory)
- * const ctx = createSubagentContext(parentContext)
- *
- * @example
- * // Custom options and agentId (for AgentTool async agents)
- * const ctx = createSubagentContext(parentContext, {
- * options: customOptions,
- * agentId: newAgentId,
- * messages: initialMessages,
- * })
- *
- * @example
- * // Interactive subagent that shares some state
- * const ctx = createSubagentContext(parentContext, {
- * options: customOptions,
- * agentId: newAgentId,
- * shareSetAppState: true,
- * shareSetResponseLength: true,
- * shareAbortController: true,
- * })
- */
- export function createSubagentContext(
- parentContext: ToolUseContext,
- overrides?: SubagentContextOverrides,
- ): ToolUseContext {
- // Determine abortController: explicit override > share parent's > new child
- const abortController =
- overrides?.abortController ??
- (overrides?.shareAbortController
- ? parentContext.abortController
- : createChildAbortController(parentContext.abortController))
- // Determine getAppState - wrap to set shouldAvoidPermissionPrompts unless sharing abortController
- // (if sharing abortController, it's an interactive agent that CAN show UI)
- const getAppState: ToolUseContext['getAppState'] = overrides?.getAppState
- ? overrides.getAppState
- : overrides?.shareAbortController
- ? parentContext.getAppState
- : () => {
- const state = parentContext.getAppState()
- if (state.toolPermissionContext.shouldAvoidPermissionPrompts) {
- return state
- }
- return {
- ...state,
- toolPermissionContext: {
- ...state.toolPermissionContext,
- shouldAvoidPermissionPrompts: true,
- },
- }
- }
- return {
- // Mutable state - cloned by default to maintain isolation
- // Clone overrides.readFileState if provided, otherwise clone from parent
- readFileState: cloneFileStateCache(
- overrides?.readFileState ?? parentContext.readFileState,
- ),
- nestedMemoryAttachmentTriggers: new Set<string>(),
- loadedNestedMemoryPaths: new Set<string>(),
- dynamicSkillDirTriggers: new Set<string>(),
- // Per-subagent: tracks skills surfaced by discovery for was_discovered telemetry (SkillTool.ts:116)
- discoveredSkillNames: new Set<string>(),
- toolDecisions: undefined,
- // Budget decisions: override > clone of parent > undefined (feature off).
- //
- // Clone by default (not fresh): cache-sharing forks process parent
- // messages containing parent tool_use_ids. A fresh state would see
- // them as unseen and make divergent replacement decisions → wire
- // prefix differs → cache miss. A clone makes identical decisions →
- // cache hit. For non-forking subagents the parent UUIDs never match
- // — clone is a harmless no-op.
- //
- // Override: AgentTool resume (reconstructed from sidechain records)
- // and inProcessRunner (per-teammate persistent loop state).
- contentReplacementState:
- overrides?.contentReplacementState ??
- (parentContext.contentReplacementState
- ? cloneContentReplacementState(parentContext.contentReplacementState)
- : undefined),
- // AbortController
- abortController,
- // AppState access
- getAppState,
- setAppState: overrides?.shareSetAppState
- ? parentContext.setAppState
- : () => {},
- // Task registration/kill must always reach the root store, even when
- // setAppState is a no-op — otherwise async agents' background bash tasks
- // are never registered and never killed (PPID=1 zombie).
- setAppStateForTasks:
- parentContext.setAppStateForTasks ?? parentContext.setAppState,
- // Async subagents whose setAppState is a no-op need local denial tracking
- // so the denial counter actually accumulates across retries.
- localDenialTracking: overrides?.shareSetAppState
- ? parentContext.localDenialTracking
- : createDenialTrackingState(),
- // Mutation callbacks - no-op by default
- setInProgressToolUseIDs: () => {},
- setResponseLength: overrides?.shareSetResponseLength
- ? parentContext.setResponseLength
- : () => {},
- pushApiMetricsEntry: overrides?.shareSetResponseLength
- ? parentContext.pushApiMetricsEntry
- : undefined,
- updateFileHistoryState: () => {},
- // Attribution is scoped and functional (prev => next) — safe to share even
- // when setAppState is stubbed. Concurrent calls compose via React's state queue.
- updateAttributionState: parentContext.updateAttributionState,
- // UI callbacks - undefined for subagents (can't control parent UI)
- addNotification: undefined,
- setToolJSX: undefined,
- setStreamMode: undefined,
- setSDKStatus: undefined,
- openMessageSelector: undefined,
- // Fields that can be overridden or copied from parent
- options: overrides?.options ?? parentContext.options,
- messages: overrides?.messages ?? parentContext.messages,
- // Generate new agentId for subagents (each subagent should have its own ID)
- agentId: overrides?.agentId ?? createAgentId(),
- agentType: overrides?.agentType,
- // Create new query tracking chain for subagent with incremented depth
- queryTracking: {
- chainId: randomUUID(),
- depth: (parentContext.queryTracking?.depth ?? -1) + 1,
- },
- fileReadingLimits: parentContext.fileReadingLimits,
- userModified: parentContext.userModified,
- criticalSystemReminder_EXPERIMENTAL:
- overrides?.criticalSystemReminder_EXPERIMENTAL,
- requireCanUseTool: overrides?.requireCanUseTool,
- }
- }
- /**
- * Runs a forked agent query loop and tracks cache hit metrics.
- *
- * This function:
- * 1. Uses identical cache-safe params from parent to enable prompt caching
- * 2. Accumulates usage across all query iterations
- * 3. Logs tengu_fork_agent_query with full usage when complete
- *
- * @example
- * ```typescript
- * const result = await runForkedAgent({
- * promptMessages: [createUserMessage({ content: userPrompt })],
- * cacheSafeParams: {
- * systemPrompt,
- * userContext,
- * systemContext,
- * toolUseContext: clonedToolUseContext,
- * forkContextMessages: messages,
- * },
- * canUseTool,
- * querySource: 'session_memory',
- * forkLabel: 'session_memory',
- * })
- * ```
- */
- export async function runForkedAgent({
- promptMessages,
- cacheSafeParams,
- canUseTool,
- querySource,
- forkLabel,
- overrides,
- maxOutputTokens,
- maxTurns,
- onMessage,
- skipTranscript,
- skipCacheWrite,
- }: ForkedAgentParams): Promise<ForkedAgentResult> {
- const startTime = Date.now()
- const outputMessages: Message[] = []
- let totalUsage: NonNullableUsage = { ...EMPTY_USAGE }
- const {
- systemPrompt,
- userContext,
- systemContext,
- toolUseContext,
- forkContextMessages,
- } = cacheSafeParams
- // Create isolated context to prevent mutation of parent state
- const isolatedToolUseContext = createSubagentContext(
- toolUseContext,
- overrides,
- )
- // Do NOT filterIncompleteToolCalls here — it drops the whole assistant on
- // partial tool batches, orphaning the paired results (API 400). Dangling
- // tool_uses are repaired downstream by ensureToolResultPairing in claude.ts,
- // same as the main thread — identical post-repair prefix keeps the cache hit.
- const initialMessages: Message[] = [...forkContextMessages, ...promptMessages]
- // Generate agent ID and record initial messages for transcript
- // When skipTranscript is set, skip agent ID creation and all transcript I/O
- const agentId = skipTranscript ? undefined : createAgentId(forkLabel)
- let lastRecordedUuid: UUID | null = null
- if (agentId) {
- await recordSidechainTranscript(initialMessages, agentId).catch(err =>
- logForDebugging(
- `Forked agent [${forkLabel}] failed to record initial transcript: ${err}`,
- ),
- )
- // Track the last recorded message UUID for parent chain continuity
- lastRecordedUuid =
- initialMessages.length > 0
- ? initialMessages[initialMessages.length - 1]!.uuid
- : null
- }
- // Run the query loop with isolated context (cache-safe params preserved)
- try {
- for await (const message of query({
- messages: initialMessages,
- systemPrompt,
- userContext,
- systemContext,
- canUseTool,
- toolUseContext: isolatedToolUseContext,
- querySource,
- maxOutputTokensOverride: maxOutputTokens,
- maxTurns,
- skipCacheWrite,
- })) {
- // Extract real usage from message_delta stream events (final usage per API call)
- if (message.type === 'stream_event') {
- if (
- 'event' in message &&
- (message as any).event?.type === 'message_delta' &&
- (message as any).event.usage
- ) {
- const turnUsage = updateUsage({ ...EMPTY_USAGE }, (message as any).event.usage)
- totalUsage = accumulateUsage(totalUsage, turnUsage)
- }
- continue
- }
- if (message.type === 'stream_request_start') {
- continue
- }
- logForDebugging(
- `Forked agent [${forkLabel}] received message: type=${message.type}`,
- )
- outputMessages.push(message as Message)
- onMessage?.(message as Message)
- // Record transcript for recordable message types (same pattern as runAgent.ts)
- const msg = message as Message
- if (
- agentId &&
- (msg.type === 'assistant' ||
- msg.type === 'user' ||
- msg.type === 'progress')
- ) {
- await recordSidechainTranscript([msg], agentId, lastRecordedUuid).catch(
- err =>
- logForDebugging(
- `Forked agent [${forkLabel}] failed to record transcript: ${err}`,
- ),
- )
- if (msg.type !== 'progress') {
- lastRecordedUuid = msg.uuid
- }
- }
- }
- } finally {
- // Release cloned file state cache memory (same pattern as runAgent.ts)
- isolatedToolUseContext.readFileState.clear()
- // Release the cloned fork context messages
- initialMessages.length = 0
- }
- logForDebugging(
- `Forked agent [${forkLabel}] finished: ${outputMessages.length} messages, types=[${outputMessages.map(m => m.type).join(', ')}], totalUsage: input=${totalUsage.input_tokens} output=${totalUsage.output_tokens} cacheRead=${totalUsage.cache_read_input_tokens} cacheCreate=${totalUsage.cache_creation_input_tokens}`,
- )
- const durationMs = Date.now() - startTime
- // Log the fork query metrics with full NonNullableUsage
- logForkAgentQueryEvent({
- forkLabel,
- querySource,
- durationMs,
- messageCount: outputMessages.length,
- totalUsage,
- queryTracking: toolUseContext.queryTracking,
- })
- return {
- messages: outputMessages,
- totalUsage,
- }
- }
- /**
- * Logs the tengu_fork_agent_query event with full NonNullableUsage fields.
- */
- function logForkAgentQueryEvent({
- forkLabel,
- querySource,
- durationMs,
- messageCount,
- totalUsage,
- queryTracking,
- }: {
- forkLabel: string
- querySource: QuerySource
- durationMs: number
- messageCount: number
- totalUsage: NonNullableUsage
- queryTracking?: { chainId: string; depth: number }
- }): void {
- // Calculate cache hit rate
- const totalInputTokens =
- totalUsage.input_tokens +
- totalUsage.cache_creation_input_tokens +
- totalUsage.cache_read_input_tokens
- const cacheHitRate =
- totalInputTokens > 0
- ? totalUsage.cache_read_input_tokens / totalInputTokens
- : 0
- logEvent('tengu_fork_agent_query', {
- // Metadata
- forkLabel:
- forkLabel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- querySource:
- querySource as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- durationMs,
- messageCount,
- // NonNullableUsage fields
- inputTokens: totalUsage.input_tokens,
- outputTokens: totalUsage.output_tokens,
- cacheReadInputTokens: totalUsage.cache_read_input_tokens,
- cacheCreationInputTokens: totalUsage.cache_creation_input_tokens,
- serviceTier:
- totalUsage.service_tier as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- cacheCreationEphemeral1hTokens:
- totalUsage.cache_creation.ephemeral_1h_input_tokens,
- cacheCreationEphemeral5mTokens:
- totalUsage.cache_creation.ephemeral_5m_input_tokens,
- // Derived metrics
- cacheHitRate,
- // Query tracking
- ...(queryTracking
- ? {
- queryChainId:
- queryTracking.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
- queryDepth: queryTracking.depth,
- }
- : {}),
- })
- }
|