02: Execute Tool Call
Validate, gate, dispatch, and record one tool call from the provider.
File: src/utils/chat/toolLoop.ts:197-331
Mission
Section titled “Mission”Given a function_call stream result, run the named tool and return a
discriminated outcome that tells the outer loop whether to restart (context was
enriched), abort (fatal condition), or append a history entry and continue.
The stage enforces the deliberate-tool-mode allowlist before reaching the
registry, retains the affordance window on success, emits a hidden trigger
notice when a deliberate-mode trigger matched, and delegates context-enrichment
restarts to
stage 03 — handleEnhancedContextRestart.
params: ToolLoopParams— full loop context.streamResult: StreamResult— thefunction_callresult from stage 01;streamResult.datacarries the raw function-call payload.iteration: number— current loop iteration index (used to setshowKillHintinToolContextonce the soft-warn threshold is reached).
Output
Section titled “Output”A discriminated union:
| { kind: "restart" }| { kind: "abort"; status: GenerationTurnResult["status"] }| { kind: "history"; functionName: string; success: boolean; endTurn: boolean; historyEntry: ToolHistoryEntry; }"restart"—handleEnhancedContextRestartconsumed the result; the outer loop doescontinuewithout pushing tofunctionHistory."abort"— a fatal condition was hit (malformed call, stop request, consecutive error cap); outer loop callsbuildResult(status)immediately."history"— normal outcome; outer loop pusheshistoryEntrytofunctionHistoryand checksendTurn/shouldEndAfterPreToolText.
Side effects
Section titled “Side effects”Steps in execution order:
-
Validate function-call data — if
streamResult.datais missing or has noname, returns{kind: "abort", status: "error"}and logs the malformed result. -
Stop-request check — calls
StreamOrchestrator.hasStopRequest(params.context.channel.id). If a stop is pending, returns{kind: "abort", status: "stopped_by_user"}before any tool runs. -
Build
ToolContext— assembles the context object passed to every tool. Key fields derived fromChatTurnContext:channel,client,message,userId,guildIdtomoriState,locale,provider(provider name string)streamContext(the liveStreamingContext)webhook,personaUsername,personaAvatarUrl(fromresponseTarget)activePersonaId,isUserImpersonation,impersonatedUserIdsuppressProgressNotices— set whenshouldSurfaceUserErrorsis falsecontextItems,messageIdMap— live references (tools may read these)showKillHint— true onceiteration >= SOFT_WARN_ITERATION_THRESHOLDabortSignal— the turn-levelAbortSignalfromgetChannelTurnAbortSignal. Tools that forward this to theirfetchcalls get true HTTP-level cancellation when/bot killfires.
-
Deliberate-tool-mode allowlist gate — if
context.deliberateToolModeActiveis true anddeliberateToolAllowedNamesis set and the requested tool is not in the allowed set, the tool is not dispatched. A synthetic failure response is produced instead:{ status: "blocked_by_deliberate_tool_mode", functionName, allowedToolNames }This is model-visible (returned as a tool response) so the model can adapt its next turn without a user-facing error.
-
ToolRegistry.executeToolwith timeout + kill race — actual dispatch, wrapped in aPromise.raceagainst two cancellation promises:- Timeout promise — resolves after
TOOL_EXECUTION_TIMEOUT_MS(default 5 min) with a synthetic{ success: false, error: "timed out" }result. The timer is fresh per tool call, so a chain of fast tools is unaffected. - Kill promise — resolves immediately if the turn-level
AbortSignalfires (i.e.,/bot killwas used while the tool was running).
After the race, if
StreamOrchestrator.hasStopRequestis true (kill was requested), the stage returns{kind: "abort", status: "stopped_by_user"}immediately — the failed result is never fed back to the model. For a plain timeout (no kill), the{ success: false }result is returned normally so the model can handle it gracefully.Returns
ToolResult:{ success: boolean; data?: unknown; error?: string;message?: string; endTurn?: boolean; imageMetadata?: … } - Timeout promise — resolves after
-
retainSuccessfulToolAffordance— on success, extends the deliberate- tool-mode affordance window for this channel so short follow-up turns (“do it again”) keep the tool exposed fordeliberateToolContextTurnsadditional turns. No-op when deliberate-tool-mode is inactive. -
Deliberate-trigger hidden notice — if
deliberateToolTriggerMatchByToolNamehas an entry for this tool and mode is active, sends a hidden embed viarouteHiddenToolNotice(thought-log only; not shown to users) describing which trigger phrase caused deliberate mode to expose the tool. -
Enhanced-context restart check — calls
handleEnhancedContextRestart(toolLoop.ts:333-364) withtoolResult.data. If it returnstrue, returns{kind: "restart"}. -
Build function response — wraps
toolResult.data(success) or a standardized error object (failure) into thefunctionResponseshape:{ functionResponse: { name, response: { result: … } } } -
Return
{kind: "history"}withhistoryEntry(the pairedfunctionCall+functionResponse+ optionalimageMetadata).
Invariants
Section titled “Invariants”After this stage runs:
- A tool was dispatched at most once per call (the deliberate-mode synthetic
failure short-circuits before
executeTool). - If
kind === "restart"is returned,functionHistorywill not receive an entry for this tool call — the restart mechanism replaces the tool response with enriched context. consecutiveToolErrorsin the outer loop is reset to0onsuccess === trueorkind === "restart".- If
/bot killfired during tool execution,kind === "abort"is returned immediately — no history entry is added and the model never sees the failed tool result. - A tool timeout (no
/bot kill) returnskind === "history"withsuccess: false— the model is informed and can decide how to proceed. - Tool execution duration is logged at
INFOlevel ("Function call completed: ${name} (${ms}ms)").
Extension points
Section titled “Extension points”| Surface | Plugin-relevance |
|---|---|
ToolRegistry.executeTool | The tool registration contract is the seam — A plugin adding a new tool registers it with the ToolRegistry. → plugin plan candidate |
Deliberate-tool-mode allowlist (deliberateToolAllowedNames) | Internal — controlled by turnPlanner; tool plugins declare their trigger patterns, not the gating logic |
ToolContext.abortSignal | Tools that forward this to their fetch calls gain free cancellation on /bot kill. New tools should always thread it through. |
ToolContext shape | The context contract — tools depend on its fields; adding a field here widens the contract for all tools |
retainSuccessfulToolAffordance | Internal — deliberate-tool-mode retention window; operational parameter, not plugin-relevant |
handleEnhancedContextRestart | See stage 03 — the context_restart_* namespace is the seam |
Configuration
Section titled “Configuration”| Env var | Default | Minimum | Purpose |
|---|---|---|---|
TOOL_EXECUTION_TIMEOUT_MS | 300000 (5 min) | 10000 (10 s) | Per-tool execution timeout; resets fresh for every tool call in a chain |
Related docs
Section titled “Related docs”- Tool registry: →
README.md - Enhanced-context restart: → stage 03 —
handleEnhancedContextRestart - Deliberate tool mode: →
src/utils/tools/deliberateToolMode.ts - Tool-loop coordinator: →
README.md