| |
| import type { |
| ToolResultBlockParam, |
| ToolUseBlock, |
| } from '@anthropic-ai/sdk/resources/index.mjs' |
| import type { CanUseToolFn } from './hooks/useCanUseTool.js' |
| import { FallbackTriggeredError } from './services/api/withRetry.js' |
| import { |
| calculateTokenWarningState, |
| isAutoCompactEnabled, |
| type AutoCompactTrackingState, |
| } from './services/compact/autoCompact.js' |
| import { buildPostCompactMessages } from './services/compact/compact.js' |
| |
| const reactiveCompact = feature('REACTIVE_COMPACT') |
| ? (require('./services/compact/reactiveCompact.js') as typeof import('./services/compact/reactiveCompact.js')) |
| : null |
| const contextCollapse = feature('CONTEXT_COLLAPSE') |
| ? (require('./services/contextCollapse/index.js') as typeof import('./services/contextCollapse/index.js')) |
| : null |
| |
| import { |
| logEvent, |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| } from 'src/services/analytics/index.js' |
| import { ImageSizeError } from './utils/imageValidation.js' |
| import { ImageResizeError } from './utils/imageResizer.js' |
| import { findToolByName, type ToolUseContext } from './Tool.js' |
| import { asSystemPrompt, type SystemPrompt } from './utils/systemPromptType.js' |
| import type { |
| AssistantMessage, |
| AttachmentMessage, |
| Message, |
| RequestStartEvent, |
| StreamEvent, |
| ToolUseSummaryMessage, |
| UserMessage, |
| TombstoneMessage, |
| } from './types/message.js' |
| import { logError } from './utils/log.js' |
| import { |
| PROMPT_TOO_LONG_ERROR_MESSAGE, |
| isPromptTooLongMessage, |
| } from './services/api/errors.js' |
| import { logAntError, logForDebugging } from './utils/debug.js' |
| import { |
| createUserMessage, |
| createUserInterruptionMessage, |
| normalizeMessagesForAPI, |
| createSystemMessage, |
| createAssistantAPIErrorMessage, |
| getMessagesAfterCompactBoundary, |
| createToolUseSummaryMessage, |
| createMicrocompactBoundaryMessage, |
| stripSignatureBlocks, |
| } from './utils/messages.js' |
| import { generateToolUseSummary } from './services/toolUseSummary/toolUseSummaryGenerator.js' |
| import { prependUserContext, appendSystemContext } from './utils/api.js' |
| import { |
| createAttachmentMessage, |
| filterDuplicateMemoryAttachments, |
| getAttachmentMessages, |
| startRelevantMemoryPrefetch, |
| } from './utils/attachments.js' |
| |
| const skillPrefetch = feature('EXPERIMENTAL_SKILL_SEARCH') |
| ? (require('./services/skillSearch/prefetch.js') as typeof import('./services/skillSearch/prefetch.js')) |
| : null |
| const jobClassifier = feature('TEMPLATES') |
| ? (require('./jobs/classifier.js') as typeof import('./jobs/classifier.js')) |
| : null |
| |
| import { |
| remove as removeFromQueue, |
| getCommandsByMaxPriority, |
| isSlashCommand, |
| } from './utils/messageQueueManager.js' |
| import { notifyCommandLifecycle } from './utils/commandLifecycle.js' |
| import { headlessProfilerCheckpoint } from './utils/headlessProfiler.js' |
| import { |
| getRuntimeMainLoopModel, |
| renderModelName, |
| } from './utils/model/model.js' |
| import { |
| doesMostRecentAssistantMessageExceed200k, |
| finalContextTokensFromLastResponse, |
| tokenCountWithEstimation, |
| } from './utils/tokens.js' |
| import { ESCALATED_MAX_TOKENS } from './utils/context.js' |
| import { getFeatureValue_CACHED_MAY_BE_STALE } from './services/analytics/growthbook.js' |
| import { SLEEP_TOOL_NAME } from './tools/SleepTool/prompt.js' |
| import { executePostSamplingHooks } from './utils/hooks/postSamplingHooks.js' |
| import { executeStopFailureHooks } from './utils/hooks.js' |
| import type { QuerySource } from './constants/querySource.js' |
| import { createDumpPromptsFetch } from './services/api/dumpPrompts.js' |
| import { StreamingToolExecutor } from './services/tools/StreamingToolExecutor.js' |
| import { queryCheckpoint } from './utils/queryProfiler.js' |
| import { runTools } from './services/tools/toolOrchestration.js' |
| import { applyToolResultBudget } from './utils/toolResultStorage.js' |
| import { recordContentReplacement } from './utils/sessionStorage.js' |
| import { handleStopHooks } from './query/stopHooks.js' |
| import { buildQueryConfig } from './query/config.js' |
| import { productionDeps, type QueryDeps } from './query/deps.js' |
| import type { Terminal, Continue } from './query/transitions.js' |
| import { feature } from 'bun:bundle' |
| import { |
| getCurrentTurnTokenBudget, |
| getTurnOutputTokens, |
| incrementBudgetContinuationCount, |
| } from './bootstrap/state.js' |
| import { createBudgetTracker, checkTokenBudget } from './query/tokenBudget.js' |
| import { count } from './utils/array.js' |
|
|
| |
| const snipModule = feature('HISTORY_SNIP') |
| ? (require('./services/compact/snipCompact.js') as typeof import('./services/compact/snipCompact.js')) |
| : null |
| const taskSummaryModule = feature('BG_SESSIONS') |
| ? (require('./utils/taskSummary.js') as typeof import('./utils/taskSummary.js')) |
| : null |
| |
|
|
| function* yieldMissingToolResultBlocks( |
| assistantMessages: AssistantMessage[], |
| errorMessage: string, |
| ) { |
| for (const assistantMessage of assistantMessages) { |
| |
| const toolUseBlocks = assistantMessage.message.content.filter( |
| content => content.type === 'tool_use', |
| ) as ToolUseBlock[] |
|
|
| |
| for (const toolUse of toolUseBlocks) { |
| yield createUserMessage({ |
| content: [ |
| { |
| type: 'tool_result', |
| content: errorMessage, |
| is_error: true, |
| tool_use_id: toolUse.id, |
| }, |
| ], |
| toolUseResult: errorMessage, |
| sourceToolAssistantUUID: assistantMessage.uuid, |
| }) |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3 |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function isWithheldMaxOutputTokens( |
| msg: Message | StreamEvent | undefined, |
| ): msg is AssistantMessage { |
| return msg?.type === 'assistant' && msg.apiError === 'max_output_tokens' |
| } |
|
|
| export type QueryParams = { |
| messages: Message[] |
| systemPrompt: SystemPrompt |
| userContext: { [k: string]: string } |
| systemContext: { [k: string]: string } |
| canUseTool: CanUseToolFn |
| toolUseContext: ToolUseContext |
| fallbackModel?: string |
| querySource: QuerySource |
| maxOutputTokensOverride?: number |
| temperatureOverride?: number |
| maxTurns?: number |
| skipCacheWrite?: boolean |
| |
| |
| |
| |
| taskBudget?: { total: number } |
| deps?: QueryDeps |
| } |
|
|
| |
|
|
| |
| type State = { |
| messages: Message[] |
| toolUseContext: ToolUseContext |
| autoCompactTracking: AutoCompactTrackingState | undefined |
| maxOutputTokensRecoveryCount: number |
| hasAttemptedReactiveCompact: boolean |
| maxOutputTokensOverride: number | undefined |
| pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined |
| stopHookActive: boolean | undefined |
| turnCount: number |
| |
| |
| transition: Continue | undefined |
| } |
|
|
| export async function* query( |
| params: QueryParams, |
| ): AsyncGenerator< |
| | StreamEvent |
| | RequestStartEvent |
| | Message |
| | TombstoneMessage |
| | ToolUseSummaryMessage, |
| Terminal |
| > { |
| const consumedCommandUuids: string[] = [] |
| const terminal = yield* queryLoop(params, consumedCommandUuids) |
| |
| |
| |
| |
| for (const uuid of consumedCommandUuids) { |
| notifyCommandLifecycle(uuid, 'completed') |
| } |
| return terminal |
| } |
|
|
| async function* queryLoop( |
| params: QueryParams, |
| consumedCommandUuids: string[], |
| ): AsyncGenerator< |
| | StreamEvent |
| | RequestStartEvent |
| | Message |
| | TombstoneMessage |
| | ToolUseSummaryMessage, |
| Terminal |
| > { |
| |
| const { |
| systemPrompt, |
| userContext, |
| systemContext, |
| canUseTool, |
| fallbackModel, |
| querySource, |
| temperatureOverride, |
| maxTurns, |
| skipCacheWrite, |
| } = params |
| const deps = params.deps ?? productionDeps() |
|
|
| |
| |
| |
| let state: State = { |
| messages: params.messages, |
| toolUseContext: params.toolUseContext, |
| maxOutputTokensOverride: params.maxOutputTokensOverride, |
| autoCompactTracking: undefined, |
| stopHookActive: undefined, |
| maxOutputTokensRecoveryCount: 0, |
| hasAttemptedReactiveCompact: false, |
| turnCount: 1, |
| pendingToolUseSummary: undefined, |
| transition: undefined, |
| } |
| const budgetTracker = feature('TOKEN_BUDGET') ? createBudgetTracker() : null |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let taskBudgetRemaining: number | undefined = undefined |
|
|
| |
| |
| const config = buildQueryConfig() |
|
|
| |
| |
| |
| |
| using pendingMemoryPrefetch = startRelevantMemoryPrefetch( |
| state.messages, |
| state.toolUseContext, |
| ) |
|
|
| |
| while (true) { |
| |
| |
| |
| let { toolUseContext } = state |
| const { |
| messages, |
| autoCompactTracking, |
| maxOutputTokensRecoveryCount, |
| hasAttemptedReactiveCompact, |
| maxOutputTokensOverride, |
| pendingToolUseSummary, |
| stopHookActive, |
| turnCount, |
| } = state |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const pendingSkillPrefetch = skillPrefetch?.startSkillDiscoveryPrefetch( |
| null, |
| messages, |
| toolUseContext, |
| ) |
|
|
| yield { type: 'stream_request_start' } |
|
|
| queryCheckpoint('query_fn_entry') |
|
|
| |
| if (!toolUseContext.agentId) { |
| headlessProfilerCheckpoint('query_started') |
| } |
|
|
| |
| const queryTracking = toolUseContext.queryTracking |
| ? { |
| chainId: toolUseContext.queryTracking.chainId, |
| depth: toolUseContext.queryTracking.depth + 1, |
| } |
| : { |
| chainId: deps.uuid(), |
| depth: 0, |
| } |
|
|
| const queryChainIdForAnalytics = |
| queryTracking.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS |
|
|
| toolUseContext = { |
| ...toolUseContext, |
| queryTracking, |
| } |
|
|
| let messagesForQuery = [...getMessagesAfterCompactBoundary(messages)] |
|
|
| let tracking = autoCompactTracking |
|
|
| |
| |
| |
| |
| |
| |
| |
| const persistReplacements = |
| querySource.startsWith('agent:') || |
| querySource.startsWith('repl_main_thread') |
| messagesForQuery = await applyToolResultBudget( |
| messagesForQuery, |
| toolUseContext.contentReplacementState, |
| persistReplacements |
| ? records => |
| void recordContentReplacement( |
| records, |
| toolUseContext.agentId, |
| ).catch(logError) |
| : undefined, |
| new Set( |
| toolUseContext.options.tools |
| .filter(t => !Number.isFinite(t.maxResultSizeChars)) |
| .map(t => t.name), |
| ), |
| ) |
|
|
| |
| |
| |
| |
| let snipTokensFreed = 0 |
| if (feature('HISTORY_SNIP')) { |
| queryCheckpoint('query_snip_start') |
| const snipResult = snipModule!.snipCompactIfNeeded(messagesForQuery) |
| messagesForQuery = snipResult.messages |
| snipTokensFreed = snipResult.tokensFreed |
| if (snipResult.boundaryMessage) { |
| yield snipResult.boundaryMessage |
| } |
| queryCheckpoint('query_snip_end') |
| } |
|
|
| |
| queryCheckpoint('query_microcompact_start') |
| const microcompactResult = await deps.microcompact( |
| messagesForQuery, |
| toolUseContext, |
| querySource, |
| ) |
| messagesForQuery = microcompactResult.messages |
| |
| |
| |
| const pendingCacheEdits = feature('CACHED_MICROCOMPACT') |
| ? microcompactResult.compactionInfo?.pendingCacheEdits |
| : undefined |
| queryCheckpoint('query_microcompact_end') |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if (feature('CONTEXT_COLLAPSE') && contextCollapse) { |
| const collapseResult = await contextCollapse.applyCollapsesIfNeeded( |
| messagesForQuery, |
| toolUseContext, |
| querySource, |
| ) |
| messagesForQuery = collapseResult.messages |
| } |
|
|
| const fullSystemPrompt = asSystemPrompt( |
| appendSystemContext(systemPrompt, systemContext), |
| ) |
|
|
| queryCheckpoint('query_autocompact_start') |
| const { compactionResult, consecutiveFailures } = await deps.autocompact( |
| messagesForQuery, |
| toolUseContext, |
| { |
| systemPrompt, |
| userContext, |
| systemContext, |
| toolUseContext, |
| forkContextMessages: messagesForQuery, |
| }, |
| querySource, |
| tracking, |
| snipTokensFreed, |
| ) |
| queryCheckpoint('query_autocompact_end') |
|
|
| if (compactionResult) { |
| const { |
| preCompactTokenCount, |
| postCompactTokenCount, |
| truePostCompactTokenCount, |
| compactionUsage, |
| } = compactionResult |
|
|
| logEvent('tengu_auto_compact_succeeded', { |
| originalMessageCount: messages.length, |
| compactedMessageCount: |
| compactionResult.summaryMessages.length + |
| compactionResult.attachments.length + |
| compactionResult.hookResults.length, |
| preCompactTokenCount, |
| postCompactTokenCount, |
| truePostCompactTokenCount, |
| compactionInputTokens: compactionUsage?.input_tokens, |
| compactionOutputTokens: compactionUsage?.output_tokens, |
| compactionCacheReadTokens: |
| compactionUsage?.cache_read_input_tokens ?? 0, |
| compactionCacheCreationTokens: |
| compactionUsage?.cache_creation_input_tokens ?? 0, |
| compactionTotalTokens: compactionUsage |
| ? compactionUsage.input_tokens + |
| (compactionUsage.cache_creation_input_tokens ?? 0) + |
| (compactionUsage.cache_read_input_tokens ?? 0) + |
| compactionUsage.output_tokens |
| : 0, |
|
|
| queryChainId: queryChainIdForAnalytics, |
| queryDepth: queryTracking.depth, |
| }) |
|
|
| |
| |
| |
| |
| if (params.taskBudget) { |
| const preCompactContext = |
| finalContextTokensFromLastResponse(messagesForQuery) |
| taskBudgetRemaining = Math.max( |
| 0, |
| (taskBudgetRemaining ?? params.taskBudget.total) - preCompactContext, |
| ) |
| } |
|
|
| |
| |
| |
| |
| tracking = { |
| compacted: true, |
| turnId: deps.uuid(), |
| turnCounter: 0, |
| consecutiveFailures: 0, |
| } |
|
|
| const postCompactMessages = buildPostCompactMessages(compactionResult) |
|
|
| for (const message of postCompactMessages) { |
| yield message |
| } |
|
|
| |
| messagesForQuery = postCompactMessages |
| } else if (consecutiveFailures !== undefined) { |
| |
| |
| tracking = { |
| ...(tracking ?? { compacted: false, turnId: '', turnCounter: 0 }), |
| consecutiveFailures, |
| } |
| } |
|
|
| |
| toolUseContext = { |
| ...toolUseContext, |
| messages: messagesForQuery, |
| } |
|
|
| const assistantMessages: AssistantMessage[] = [] |
| const toolResults: (UserMessage | AttachmentMessage)[] = [] |
| |
| |
| |
| |
| const toolUseBlocks: ToolUseBlock[] = [] |
| let needsFollowUp = false |
|
|
| queryCheckpoint('query_setup_start') |
| const useStreamingToolExecution = config.gates.streamingToolExecution |
| let streamingToolExecutor = useStreamingToolExecution |
| ? new StreamingToolExecutor( |
| toolUseContext.options.tools, |
| canUseTool, |
| toolUseContext, |
| ) |
| : null |
|
|
| const appState = toolUseContext.getAppState() |
| const permissionMode = appState.toolPermissionContext.mode |
| let currentModel = getRuntimeMainLoopModel({ |
| permissionMode, |
| mainLoopModel: toolUseContext.options.mainLoopModel, |
| exceeds200kTokens: |
| permissionMode === 'plan' && |
| doesMostRecentAssistantMessageExceed200k(messagesForQuery), |
| }) |
|
|
| queryCheckpoint('query_setup_end') |
|
|
| |
| |
| |
| |
| |
| |
| const dumpPromptsFetch = config.gates.isAnt |
| ? createDumpPromptsFetch(toolUseContext.agentId ?? config.sessionId) |
| : undefined |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let collapseOwnsIt = false |
| if (feature('CONTEXT_COLLAPSE')) { |
| collapseOwnsIt = |
| (contextCollapse?.isContextCollapseEnabled() ?? false) && |
| isAutoCompactEnabled() |
| } |
| |
| |
| |
| |
| |
| const mediaRecoveryEnabled = |
| reactiveCompact?.isReactiveCompactEnabled() ?? false |
| if ( |
| !compactionResult && |
| querySource !== 'compact' && |
| querySource !== 'session_memory' && |
| !( |
| reactiveCompact?.isReactiveCompactEnabled() && isAutoCompactEnabled() |
| ) && |
| !collapseOwnsIt |
| ) { |
| const { isAtBlockingLimit } = calculateTokenWarningState( |
| tokenCountWithEstimation(messagesForQuery) - snipTokensFreed, |
| toolUseContext.options.mainLoopModel, |
| ) |
| if (isAtBlockingLimit) { |
| yield createAssistantAPIErrorMessage({ |
| content: PROMPT_TOO_LONG_ERROR_MESSAGE, |
| error: 'invalid_request', |
| }) |
| return { reason: 'blocking_limit' } |
| } |
| } |
|
|
| let attemptWithFallback = true |
|
|
| queryCheckpoint('query_api_loop_start') |
| try { |
| while (attemptWithFallback) { |
| attemptWithFallback = false |
| try { |
| let streamingFallbackOccured = false |
| queryCheckpoint('query_api_streaming_start') |
| for await (const message of deps.callModel({ |
| messages: prependUserContext(messagesForQuery, userContext), |
| systemPrompt: fullSystemPrompt, |
| thinkingConfig: toolUseContext.options.thinkingConfig, |
| tools: toolUseContext.options.tools, |
| signal: toolUseContext.abortController.signal, |
| options: { |
| async getToolPermissionContext() { |
| const appState = toolUseContext.getAppState() |
| return appState.toolPermissionContext |
| }, |
| model: currentModel, |
| ...(config.gates.fastModeEnabled && { |
| fastMode: appState.fastMode, |
| }), |
| toolChoice: undefined, |
| isNonInteractiveSession: |
| toolUseContext.options.isNonInteractiveSession, |
| fallbackModel, |
| onStreamingFallback: () => { |
| streamingFallbackOccured = true |
| }, |
| querySource, |
| agents: toolUseContext.options.agentDefinitions.activeAgents, |
| allowedAgentTypes: |
| toolUseContext.options.agentDefinitions.allowedAgentTypes, |
| hasAppendSystemPrompt: |
| !!toolUseContext.options.appendSystemPrompt, |
| maxOutputTokensOverride, |
| temperatureOverride, |
| fetchOverride: dumpPromptsFetch, |
| mcpTools: appState.mcp.tools, |
| hasPendingMcpServers: appState.mcp.clients.some( |
| c => c.type === 'pending', |
| ), |
| queryTracking, |
| effortValue: appState.effortValue, |
| advisorModel: appState.advisorModel, |
| skipCacheWrite, |
| agentId: toolUseContext.agentId, |
| addNotification: toolUseContext.addNotification, |
| ...(params.taskBudget && { |
| taskBudget: { |
| total: params.taskBudget.total, |
| ...(taskBudgetRemaining !== undefined && { |
| remaining: taskBudgetRemaining, |
| }), |
| }, |
| }), |
| }, |
| })) { |
| |
| |
| |
| if (streamingFallbackOccured) { |
| |
| |
| |
| for (const msg of assistantMessages) { |
| yield { type: 'tombstone' as const, message: msg } |
| } |
| logEvent('tengu_orphaned_messages_tombstoned', { |
| orphanedMessageCount: assistantMessages.length, |
| queryChainId: queryChainIdForAnalytics, |
| queryDepth: queryTracking.depth, |
| }) |
|
|
| assistantMessages.length = 0 |
| toolResults.length = 0 |
| toolUseBlocks.length = 0 |
| needsFollowUp = false |
|
|
| |
| |
| |
| if (streamingToolExecutor) { |
| streamingToolExecutor.discard() |
| streamingToolExecutor = new StreamingToolExecutor( |
| toolUseContext.options.tools, |
| canUseTool, |
| toolUseContext, |
| ) |
| } |
| } |
| |
| |
| |
| |
| |
| let yieldMessage: typeof message = message |
| if (message.type === 'assistant') { |
| let clonedContent: typeof message.message.content | undefined |
| for (let i = 0; i < message.message.content.length; i++) { |
| const block = message.message.content[i]! |
| if ( |
| block.type === 'tool_use' && |
| typeof block.input === 'object' && |
| block.input !== null |
| ) { |
| const tool = findToolByName( |
| toolUseContext.options.tools, |
| block.name, |
| ) |
| if (tool?.backfillObservableInput) { |
| const originalInput = block.input as Record<string, unknown> |
| const inputCopy = { ...originalInput } |
| tool.backfillObservableInput(inputCopy) |
| |
| |
| |
| |
| |
| |
| const addedFields = Object.keys(inputCopy).some( |
| k => !(k in originalInput), |
| ) |
| if (addedFields) { |
| clonedContent ??= [...message.message.content] |
| clonedContent[i] = { ...block, input: inputCopy } |
| } |
| } |
| } |
| } |
| if (clonedContent) { |
| yieldMessage = { |
| ...message, |
| message: { ...message.message, content: clonedContent }, |
| } |
| } |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let withheld = false |
| if (feature('CONTEXT_COLLAPSE')) { |
| if ( |
| contextCollapse?.isWithheldPromptTooLong( |
| message, |
| isPromptTooLongMessage, |
| querySource, |
| ) |
| ) { |
| withheld = true |
| } |
| } |
| if (reactiveCompact?.isWithheldPromptTooLong(message)) { |
| withheld = true |
| } |
| if ( |
| mediaRecoveryEnabled && |
| reactiveCompact?.isWithheldMediaSizeError(message) |
| ) { |
| withheld = true |
| } |
| if (isWithheldMaxOutputTokens(message)) { |
| withheld = true |
| } |
| if (!withheld) { |
| yield yieldMessage |
| } |
| if (message.type === 'assistant') { |
| assistantMessages.push(message) |
|
|
| const msgToolUseBlocks = message.message.content.filter( |
| content => content.type === 'tool_use', |
| ) as ToolUseBlock[] |
| if (msgToolUseBlocks.length > 0) { |
| toolUseBlocks.push(...msgToolUseBlocks) |
| needsFollowUp = true |
| } |
|
|
| if ( |
| streamingToolExecutor && |
| !toolUseContext.abortController.signal.aborted |
| ) { |
| for (const toolBlock of msgToolUseBlocks) { |
| streamingToolExecutor.addTool(toolBlock, message) |
| } |
| } |
| } |
|
|
| if ( |
| streamingToolExecutor && |
| !toolUseContext.abortController.signal.aborted |
| ) { |
| for (const result of streamingToolExecutor.getCompletedResults()) { |
| if (result.message) { |
| yield result.message |
| toolResults.push( |
| ...normalizeMessagesForAPI( |
| [result.message], |
| toolUseContext.options.tools, |
| ).filter(_ => _.type === 'user'), |
| ) |
| } |
| } |
| } |
| } |
| queryCheckpoint('query_api_streaming_end') |
|
|
| |
| |
| |
| |
| if (feature('CACHED_MICROCOMPACT') && pendingCacheEdits) { |
| const lastAssistant = assistantMessages.at(-1) |
| |
| |
| const usage = lastAssistant?.message.usage |
| const cumulativeDeleted = usage |
| ? ((usage as unknown as Record<string, number>) |
| .cache_deleted_input_tokens ?? 0) |
| : 0 |
| const deletedTokens = Math.max( |
| 0, |
| cumulativeDeleted - pendingCacheEdits.baselineCacheDeletedTokens, |
| ) |
| if (deletedTokens > 0) { |
| yield createMicrocompactBoundaryMessage( |
| pendingCacheEdits.trigger, |
| 0, |
| deletedTokens, |
| pendingCacheEdits.deletedToolIds, |
| [], |
| ) |
| } |
| } |
| } catch (innerError) { |
| if (innerError instanceof FallbackTriggeredError && fallbackModel) { |
| |
| currentModel = fallbackModel |
| attemptWithFallback = true |
|
|
| |
| yield* yieldMissingToolResultBlocks( |
| assistantMessages, |
| 'Model fallback triggered', |
| ) |
| assistantMessages.length = 0 |
| toolResults.length = 0 |
| toolUseBlocks.length = 0 |
| needsFollowUp = false |
|
|
| |
| |
| |
| if (streamingToolExecutor) { |
| streamingToolExecutor.discard() |
| streamingToolExecutor = new StreamingToolExecutor( |
| toolUseContext.options.tools, |
| canUseTool, |
| toolUseContext, |
| ) |
| } |
|
|
| |
| toolUseContext.options.mainLoopModel = fallbackModel |
|
|
| |
| |
| |
| if (process.env.USER_TYPE === 'ant') { |
| messagesForQuery = stripSignatureBlocks(messagesForQuery) |
| } |
|
|
| |
| logEvent('tengu_model_fallback_triggered', { |
| original_model: |
| innerError.originalModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| fallback_model: |
| fallbackModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| entrypoint: |
| 'cli' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| queryChainId: queryChainIdForAnalytics, |
| queryDepth: queryTracking.depth, |
| }) |
|
|
| |
| |
| yield createSystemMessage( |
| `Switched to ${renderModelName(innerError.fallbackModel)} due to high demand for ${renderModelName(innerError.originalModel)}`, |
| 'warning', |
| ) |
|
|
| continue |
| } |
| throw innerError |
| } |
| } |
| } catch (error) { |
| logError(error) |
| const errorMessage = |
| error instanceof Error ? error.message : String(error) |
| logEvent('tengu_query_error', { |
| assistantMessages: assistantMessages.length, |
| toolUses: assistantMessages.flatMap(_ => |
| _.message.content.filter(content => content.type === 'tool_use'), |
| ).length, |
|
|
| queryChainId: queryChainIdForAnalytics, |
| queryDepth: queryTracking.depth, |
| }) |
|
|
| |
| if ( |
| error instanceof ImageSizeError || |
| error instanceof ImageResizeError |
| ) { |
| yield createAssistantAPIErrorMessage({ |
| content: error.message, |
| }) |
| return { reason: 'image_error' } |
| } |
|
|
| |
| |
| |
| |
| yield* yieldMissingToolResultBlocks(assistantMessages, errorMessage) |
|
|
| |
| |
| |
| |
| yield createAssistantAPIErrorMessage({ |
| content: errorMessage, |
| }) |
|
|
| |
| logAntError('Query error', error) |
| return { reason: 'model_error', error } |
| } |
|
|
| |
| if (assistantMessages.length > 0) { |
| void executePostSamplingHooks( |
| [...messagesForQuery, ...assistantMessages], |
| systemPrompt, |
| userContext, |
| systemContext, |
| toolUseContext, |
| querySource, |
| ) |
| } |
|
|
| |
| |
| |
| |
| if (toolUseContext.abortController.signal.aborted) { |
| if (streamingToolExecutor) { |
| |
| |
| for await (const update of streamingToolExecutor.getRemainingResults()) { |
| if (update.message) { |
| yield update.message |
| } |
| } |
| } else { |
| yield* yieldMissingToolResultBlocks( |
| assistantMessages, |
| 'Interrupted by user', |
| ) |
| } |
| |
| |
| |
| if (feature('CHICAGO_MCP') && !toolUseContext.agentId) { |
| try { |
| const { cleanupComputerUseAfterTurn } = await import( |
| './utils/computerUse/cleanup.js' |
| ) |
| await cleanupComputerUseAfterTurn(toolUseContext) |
| } catch { |
| |
| } |
| } |
|
|
| |
| |
| if (toolUseContext.abortController.signal.reason !== 'interrupt') { |
| yield createUserInterruptionMessage({ |
| toolUse: false, |
| }) |
| } |
| return { reason: 'aborted_streaming' } |
| } |
|
|
| |
| if (pendingToolUseSummary) { |
| const summary = await pendingToolUseSummary |
| if (summary) { |
| yield summary |
| } |
| } |
|
|
| if (!needsFollowUp) { |
| const lastMessage = assistantMessages.at(-1) |
|
|
| |
| |
| |
| |
| |
| const isWithheld413 = |
| lastMessage?.type === 'assistant' && |
| lastMessage.isApiErrorMessage && |
| isPromptTooLongMessage(lastMessage) |
| |
| |
| |
| |
| |
| |
| |
| |
| const isWithheldMedia = |
| mediaRecoveryEnabled && |
| reactiveCompact?.isWithheldMediaSizeError(lastMessage) |
| if (isWithheld413) { |
| |
| |
| |
| if ( |
| feature('CONTEXT_COLLAPSE') && |
| contextCollapse && |
| state.transition?.reason !== 'collapse_drain_retry' |
| ) { |
| const drained = contextCollapse.recoverFromOverflow( |
| messagesForQuery, |
| querySource, |
| ) |
| if (drained.committed > 0) { |
| const next: State = { |
| messages: drained.messages, |
| toolUseContext, |
| autoCompactTracking: tracking, |
| maxOutputTokensRecoveryCount, |
| hasAttemptedReactiveCompact, |
| maxOutputTokensOverride: undefined, |
| pendingToolUseSummary: undefined, |
| stopHookActive: undefined, |
| turnCount, |
| transition: { |
| reason: 'collapse_drain_retry', |
| committed: drained.committed, |
| }, |
| } |
| state = next |
| continue |
| } |
| } |
| } |
| if ((isWithheld413 || isWithheldMedia) && reactiveCompact) { |
| const compacted = await reactiveCompact.tryReactiveCompact({ |
| hasAttempted: hasAttemptedReactiveCompact, |
| querySource, |
| aborted: toolUseContext.abortController.signal.aborted, |
| messages: messagesForQuery, |
| cacheSafeParams: { |
| systemPrompt, |
| userContext, |
| systemContext, |
| toolUseContext, |
| forkContextMessages: messagesForQuery, |
| }, |
| }) |
|
|
| if (compacted) { |
| |
| |
| |
| if (params.taskBudget) { |
| const preCompactContext = |
| finalContextTokensFromLastResponse(messagesForQuery) |
| taskBudgetRemaining = Math.max( |
| 0, |
| (taskBudgetRemaining ?? params.taskBudget.total) - |
| preCompactContext, |
| ) |
| } |
|
|
| const postCompactMessages = buildPostCompactMessages(compacted) |
| for (const msg of postCompactMessages) { |
| yield msg |
| } |
| const next: State = { |
| messages: postCompactMessages, |
| toolUseContext, |
| autoCompactTracking: undefined, |
| maxOutputTokensRecoveryCount, |
| hasAttemptedReactiveCompact: true, |
| maxOutputTokensOverride: undefined, |
| pendingToolUseSummary: undefined, |
| stopHookActive: undefined, |
| turnCount, |
| transition: { reason: 'reactive_compact_retry' }, |
| } |
| state = next |
| continue |
| } |
|
|
| |
| |
| |
| |
| |
| yield lastMessage |
| void executeStopFailureHooks(lastMessage, toolUseContext) |
| return { reason: isWithheldMedia ? 'image_error' : 'prompt_too_long' } |
| } else if (feature('CONTEXT_COLLAPSE') && isWithheld413) { |
| |
| |
| |
| yield lastMessage |
| void executeStopFailureHooks(lastMessage, toolUseContext) |
| return { reason: 'prompt_too_long' } |
| } |
|
|
| |
| |
| |
| if (isWithheldMaxOutputTokens(lastMessage)) { |
| |
| |
| |
| |
| |
| |
| const capEnabled = getFeatureValue_CACHED_MAY_BE_STALE( |
| 'tengu_otk_slot_v1', |
| false, |
| ) |
| if ( |
| capEnabled && |
| maxOutputTokensOverride === undefined && |
| !process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS |
| ) { |
| logEvent('tengu_max_tokens_escalate', { |
| escalatedTo: ESCALATED_MAX_TOKENS, |
| }) |
| const next: State = { |
| messages: messagesForQuery, |
| toolUseContext, |
| autoCompactTracking: tracking, |
| maxOutputTokensRecoveryCount, |
| hasAttemptedReactiveCompact, |
| maxOutputTokensOverride: ESCALATED_MAX_TOKENS, |
| pendingToolUseSummary: undefined, |
| stopHookActive: undefined, |
| turnCount, |
| transition: { reason: 'max_output_tokens_escalate' }, |
| } |
| state = next |
| continue |
| } |
|
|
| if (maxOutputTokensRecoveryCount < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT) { |
| const recoveryMessage = createUserMessage({ |
| content: |
| `Output token limit hit. Resume directly — no apology, no recap of what you were doing. ` + |
| `Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.`, |
| isMeta: true, |
| }) |
|
|
| const next: State = { |
| messages: [ |
| ...messagesForQuery, |
| ...assistantMessages, |
| recoveryMessage, |
| ], |
| toolUseContext, |
| autoCompactTracking: tracking, |
| maxOutputTokensRecoveryCount: maxOutputTokensRecoveryCount + 1, |
| hasAttemptedReactiveCompact, |
| maxOutputTokensOverride: undefined, |
| pendingToolUseSummary: undefined, |
| stopHookActive: undefined, |
| turnCount, |
| transition: { |
| reason: 'max_output_tokens_recovery', |
| attempt: maxOutputTokensRecoveryCount + 1, |
| }, |
| } |
| state = next |
| continue |
| } |
|
|
| |
| yield lastMessage |
| } |
|
|
| |
| |
| |
| |
| if (lastMessage?.isApiErrorMessage) { |
| void executeStopFailureHooks(lastMessage, toolUseContext) |
| return { reason: 'completed' } |
| } |
|
|
| const stopHookResult = yield* handleStopHooks( |
| messagesForQuery, |
| assistantMessages, |
| systemPrompt, |
| userContext, |
| systemContext, |
| toolUseContext, |
| querySource, |
| stopHookActive, |
| ) |
|
|
| if (stopHookResult.preventContinuation) { |
| return { reason: 'stop_hook_prevented' } |
| } |
|
|
| if (stopHookResult.blockingErrors.length > 0) { |
| const next: State = { |
| messages: [ |
| ...messagesForQuery, |
| ...assistantMessages, |
| ...stopHookResult.blockingErrors, |
| ], |
| toolUseContext, |
| autoCompactTracking: tracking, |
| maxOutputTokensRecoveryCount: 0, |
| |
| |
| |
| |
| |
| hasAttemptedReactiveCompact, |
| maxOutputTokensOverride: undefined, |
| pendingToolUseSummary: undefined, |
| stopHookActive: true, |
| turnCount, |
| transition: { reason: 'stop_hook_blocking' }, |
| } |
| state = next |
| continue |
| } |
|
|
| if (feature('TOKEN_BUDGET')) { |
| const decision = checkTokenBudget( |
| budgetTracker!, |
| toolUseContext.agentId, |
| getCurrentTurnTokenBudget(), |
| getTurnOutputTokens(), |
| ) |
|
|
| if (decision.action === 'continue') { |
| incrementBudgetContinuationCount() |
| logForDebugging( |
| `Token budget continuation #${decision.continuationCount}: ${decision.pct}% (${decision.turnTokens.toLocaleString()} / ${decision.budget.toLocaleString()})`, |
| ) |
| state = { |
| messages: [ |
| ...messagesForQuery, |
| ...assistantMessages, |
| createUserMessage({ |
| content: decision.nudgeMessage, |
| isMeta: true, |
| }), |
| ], |
| toolUseContext, |
| autoCompactTracking: tracking, |
| maxOutputTokensRecoveryCount: 0, |
| hasAttemptedReactiveCompact: false, |
| maxOutputTokensOverride: undefined, |
| pendingToolUseSummary: undefined, |
| stopHookActive: undefined, |
| turnCount, |
| transition: { reason: 'token_budget_continuation' }, |
| } |
| continue |
| } |
|
|
| if (decision.completionEvent) { |
| if (decision.completionEvent.diminishingReturns) { |
| logForDebugging( |
| `Token budget early stop: diminishing returns at ${decision.completionEvent.pct}%`, |
| ) |
| } |
| logEvent('tengu_token_budget_completed', { |
| ...decision.completionEvent, |
| queryChainId: queryChainIdForAnalytics, |
| queryDepth: queryTracking.depth, |
| }) |
| } |
| } |
|
|
| return { reason: 'completed' } |
| } |
|
|
| let shouldPreventContinuation = false |
| let updatedToolUseContext = toolUseContext |
|
|
| queryCheckpoint('query_tool_execution_start') |
|
|
|
|
| if (streamingToolExecutor) { |
| logEvent('tengu_streaming_tool_execution_used', { |
| tool_count: toolUseBlocks.length, |
| queryChainId: queryChainIdForAnalytics, |
| queryDepth: queryTracking.depth, |
| }) |
| } else { |
| logEvent('tengu_streaming_tool_execution_not_used', { |
| tool_count: toolUseBlocks.length, |
| queryChainId: queryChainIdForAnalytics, |
| queryDepth: queryTracking.depth, |
| }) |
| } |
|
|
| const toolUpdates = streamingToolExecutor |
| ? streamingToolExecutor.getRemainingResults() |
| : runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext) |
|
|
| for await (const update of toolUpdates) { |
| if (update.message) { |
| yield update.message |
|
|
| if ( |
| update.message.type === 'attachment' && |
| update.message.attachment.type === 'hook_stopped_continuation' |
| ) { |
| shouldPreventContinuation = true |
| } |
|
|
| toolResults.push( |
| ...normalizeMessagesForAPI( |
| [update.message], |
| toolUseContext.options.tools, |
| ).filter(_ => _.type === 'user'), |
| ) |
| } |
| if (update.newContext) { |
| updatedToolUseContext = { |
| ...update.newContext, |
| queryTracking, |
| } |
| } |
| } |
| queryCheckpoint('query_tool_execution_end') |
|
|
| |
| let nextPendingToolUseSummary: |
| | Promise<ToolUseSummaryMessage | null> |
| | undefined |
| if ( |
| config.gates.emitToolUseSummaries && |
| toolUseBlocks.length > 0 && |
| !toolUseContext.abortController.signal.aborted && |
| !toolUseContext.agentId |
| ) { |
| |
| const lastAssistantMessage = assistantMessages.at(-1) |
| let lastAssistantText: string | undefined |
| if (lastAssistantMessage) { |
| const textBlocks = lastAssistantMessage.message.content.filter( |
| block => block.type === 'text', |
| ) |
| if (textBlocks.length > 0) { |
| const lastTextBlock = textBlocks.at(-1) |
| if (lastTextBlock && 'text' in lastTextBlock) { |
| lastAssistantText = lastTextBlock.text |
| } |
| } |
| } |
|
|
| |
| const toolUseIds = toolUseBlocks.map(block => block.id) |
| const toolInfoForSummary = toolUseBlocks.map(block => { |
| |
| const toolResult = toolResults.find( |
| result => |
| result.type === 'user' && |
| Array.isArray(result.message.content) && |
| result.message.content.some( |
| content => |
| content.type === 'tool_result' && |
| content.tool_use_id === block.id, |
| ), |
| ) |
| const resultContent = |
| toolResult?.type === 'user' && |
| Array.isArray(toolResult.message.content) |
| ? toolResult.message.content.find( |
| (c): c is ToolResultBlockParam => |
| c.type === 'tool_result' && c.tool_use_id === block.id, |
| ) |
| : undefined |
| return { |
| name: block.name, |
| input: block.input, |
| output: |
| resultContent && 'content' in resultContent |
| ? resultContent.content |
| : null, |
| } |
| }) |
|
|
| |
| nextPendingToolUseSummary = generateToolUseSummary({ |
| tools: toolInfoForSummary, |
| signal: toolUseContext.abortController.signal, |
| isNonInteractiveSession: toolUseContext.options.isNonInteractiveSession, |
| lastAssistantText, |
| }) |
| .then(summary => { |
| if (summary) { |
| return createToolUseSummaryMessage(summary, toolUseIds) |
| } |
| return null |
| }) |
| .catch(() => null) |
| } |
|
|
| |
| if (toolUseContext.abortController.signal.aborted) { |
| |
| |
| |
| if (feature('CHICAGO_MCP') && !toolUseContext.agentId) { |
| try { |
| const { cleanupComputerUseAfterTurn } = await import( |
| './utils/computerUse/cleanup.js' |
| ) |
| await cleanupComputerUseAfterTurn(toolUseContext) |
| } catch { |
| |
| } |
| } |
| |
| |
| if (toolUseContext.abortController.signal.reason !== 'interrupt') { |
| yield createUserInterruptionMessage({ |
| toolUse: true, |
| }) |
| } |
| |
| const nextTurnCountOnAbort = turnCount + 1 |
| if (maxTurns && nextTurnCountOnAbort > maxTurns) { |
| yield createAttachmentMessage({ |
| type: 'max_turns_reached', |
| maxTurns, |
| turnCount: nextTurnCountOnAbort, |
| }) |
| } |
| return { reason: 'aborted_tools' } |
| } |
|
|
| |
| if (shouldPreventContinuation) { |
| return { reason: 'hook_stopped' } |
| } |
|
|
| if (tracking?.compacted) { |
| tracking.turnCounter++ |
| logEvent('tengu_post_autocompact_turn', { |
| turnId: |
| tracking.turnId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| turnCounter: tracking.turnCounter, |
|
|
| queryChainId: queryChainIdForAnalytics, |
| queryDepth: queryTracking.depth, |
| }) |
| } |
|
|
| |
| |
|
|
| |
| logEvent('tengu_query_before_attachments', { |
| messagesForQueryCount: messagesForQuery.length, |
| assistantMessagesCount: assistantMessages.length, |
| toolResultsCount: toolResults.length, |
| queryChainId: queryChainIdForAnalytics, |
| queryDepth: queryTracking.depth, |
| }) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const sleepRan = toolUseBlocks.some(b => b.name === SLEEP_TOOL_NAME) |
| const isMainThread = |
| querySource.startsWith('repl_main_thread') || querySource === 'sdk' |
| const currentAgentId = toolUseContext.agentId |
| const queuedCommandsSnapshot = getCommandsByMaxPriority( |
| sleepRan ? 'later' : 'next', |
| ).filter(cmd => { |
| if (isSlashCommand(cmd)) return false |
| if (isMainThread) return cmd.agentId === undefined |
| |
| |
| return cmd.mode === 'task-notification' && cmd.agentId === currentAgentId |
| }) |
|
|
| for await (const attachment of getAttachmentMessages( |
| null, |
| updatedToolUseContext, |
| null, |
| queuedCommandsSnapshot, |
| [...messagesForQuery, ...assistantMessages, ...toolResults], |
| querySource, |
| )) { |
| yield attachment |
| toolResults.push(attachment) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| if ( |
| pendingMemoryPrefetch && |
| pendingMemoryPrefetch.settledAt !== null && |
| pendingMemoryPrefetch.consumedOnIteration === -1 |
| ) { |
| const memoryAttachments = filterDuplicateMemoryAttachments( |
| await pendingMemoryPrefetch.promise, |
| toolUseContext.readFileState, |
| ) |
| for (const memAttachment of memoryAttachments) { |
| const msg = createAttachmentMessage(memAttachment) |
| yield msg |
| toolResults.push(msg) |
| } |
| pendingMemoryPrefetch.consumedOnIteration = turnCount - 1 |
| } |
|
|
|
|
| |
| |
| |
| if (skillPrefetch && pendingSkillPrefetch) { |
| const skillAttachments = |
| await skillPrefetch.collectSkillDiscoveryPrefetch(pendingSkillPrefetch) |
| for (const att of skillAttachments) { |
| const msg = createAttachmentMessage(att) |
| yield msg |
| toolResults.push(msg) |
| } |
| } |
|
|
| |
| |
| const consumedCommands = queuedCommandsSnapshot.filter( |
| cmd => cmd.mode === 'prompt' || cmd.mode === 'task-notification', |
| ) |
| if (consumedCommands.length > 0) { |
| for (const cmd of consumedCommands) { |
| if (cmd.uuid) { |
| consumedCommandUuids.push(cmd.uuid) |
| notifyCommandLifecycle(cmd.uuid, 'started') |
| } |
| } |
| removeFromQueue(consumedCommands) |
| } |
|
|
| |
| const fileChangeAttachmentCount = count( |
| toolResults, |
| tr => |
| tr.type === 'attachment' && tr.attachment.type === 'edited_text_file', |
| ) |
|
|
| logEvent('tengu_query_after_attachments', { |
| totalToolResultsCount: toolResults.length, |
| fileChangeAttachmentCount, |
| queryChainId: queryChainIdForAnalytics, |
| queryDepth: queryTracking.depth, |
| }) |
|
|
| |
| if (updatedToolUseContext.options.refreshTools) { |
| const refreshedTools = updatedToolUseContext.options.refreshTools() |
| if (refreshedTools !== updatedToolUseContext.options.tools) { |
| updatedToolUseContext = { |
| ...updatedToolUseContext, |
| options: { |
| ...updatedToolUseContext.options, |
| tools: refreshedTools, |
| }, |
| } |
| } |
| } |
|
|
| const toolUseContextWithQueryTracking = { |
| ...updatedToolUseContext, |
| queryTracking, |
| } |
|
|
| |
| const nextTurnCount = turnCount + 1 |
|
|
| |
| |
| |
| |
| if (feature('BG_SESSIONS')) { |
| if ( |
| !toolUseContext.agentId && |
| taskSummaryModule!.shouldGenerateTaskSummary() |
| ) { |
| taskSummaryModule!.maybeGenerateTaskSummary({ |
| systemPrompt, |
| userContext, |
| systemContext, |
| toolUseContext, |
| forkContextMessages: [ |
| ...messagesForQuery, |
| ...assistantMessages, |
| ...toolResults, |
| ], |
| }) |
| } |
| } |
|
|
| |
| if (maxTurns && nextTurnCount > maxTurns) { |
| yield createAttachmentMessage({ |
| type: 'max_turns_reached', |
| maxTurns, |
| turnCount: nextTurnCount, |
| }) |
| return { reason: 'max_turns', turnCount: nextTurnCount } |
| } |
|
|
| queryCheckpoint('query_recursive_call') |
| const next: State = { |
| messages: [...messagesForQuery, ...assistantMessages, ...toolResults], |
| toolUseContext: toolUseContextWithQueryTracking, |
| autoCompactTracking: tracking, |
| turnCount: nextTurnCount, |
| maxOutputTokensRecoveryCount: 0, |
| hasAttemptedReactiveCompact: false, |
| pendingToolUseSummary: nextPendingToolUseSummary, |
| maxOutputTokensOverride: undefined, |
| stopHookActive, |
| transition: { reason: 'next_turn' }, |
| } |
| state = next |
| } |
| } |
|
|