Docs / Tool-calling
ADR-013

Tool-calling. Provider-agnostic multi-step reasoning.

Org Chat (and any future caller) can invoke a provider with native tool-calling via the optional LLMToolCapableProvider extension interface. Implementations stay opt-in. Providers that don't implement it fall back to the legacy single-shot path automatically.

Why provider-agnostic, not OpenAI Assistants

  • No vendor lock-in. Works with Anthropic, OpenAI, Azure, Bedrock, Ollama. Adding Vertex tomorrow is one Apex class.
  • No per-customer multi-tenancy operational cost. Server-side state stays in Salesforce Platform Cache, not on the LLM vendor's infrastructure.
  • No sync-drift failure mode. Schema describe + inventory snapshot are authoritative locally; nothing to keep in sync with a vendor-side index.
  • Same trust boundary. Every run_soql tool call routes through FMSoqlValidator. Identical to the legacy single-shot path.

The interface

Apex
public interface LLMToolCapableProvider {
    Boolean supportsTools();
    ProviderResponse invokeWithTools(
        ProviderRequest req,
        List<FMToolSchema> tools,
        FMOrgChatToolDispatcher dispatcher,
        FMThreadState state
    );
}

The 4 canonical tools

ToolBoundaryReturns
run_soql FMSoqlValidator 8-gate sanitiser → Database.queryWithBinds(USER_MODE) Up to LIMIT rows
lookup_metadata FMOrgIntrospector.scopedExcerpt over allowlisted SObjects Triggers, flows, validation rules, dependency degree, perm-set FLS
object_relationships FMSchemaCatalog parent + child rels for one allowlisted SObject Parent lookups + child relationships
inventory_search SOQL against FM_Org_Inventory_Snapshot__c Component matches by name / type

DML deliberately not exposed. Assistant has read-only powers via tools. DML stays via the existing intent-detection + two-step modal path.

Provider matrix

ProvidersupportsTools()Native API
AnthropicProvidertrueMessages API tool_use
OpenAIProvidertrueChat Completions tools / tool_calls
AzureOpenAIProvidertrueChat Completions (deployment-based)
BedrockProvidertruebedrock-2023-05-31 Anthropic protocol
OllamaProvidertrue/api/chat tool_calls
EdenAIProviderfalseSmart-router; falls back to single-shot
GoogleVertexProviderfalseGemini function-calling not yet wired
MockProviderfalseTest fixture

Enable

anonymous Apex
// Enable
FMConfig.set('orgChatToolCallingEnabled', 'true');

// Tune (optional)
FMConfig.set('orgChatToolCallingMaxCalls', '5');     // per-turn cap
FMConfig.set('orgChatToolCallingTimeoutMs', '25000'); // wall-clock budget

// Verify the provider supports tools
LLMProvider p = ProviderFactory.getProvider('anthropic');
System.assert(p instanceof LLMToolCapableProvider);
System.assert(((LLMToolCapableProvider) p).supportsTools());

Loop + cost safety

  • Per-turn call cap: orgChatToolCallingMaxCalls (default 5). Dispatcher tracks via FMOrgChatToolDispatcher.budgetExceeded().
  • Wall-clock budget: orgChatToolCallingTimeoutMs (default 25000). Belt-and-braces against runaway loops.
  • Provider-side: native stop_reason = end_turn (or equivalent) terminates the loop.
  • Validator rejects DML + multi-statement attempts at the boundary.

Thread state (cost win)

FMThreadState stores per-conversation provider history in Platform Cache (24-hour TTL). Next turn sends only the delta, not the full transcript. Long conversations (10+ turns) typically see 30-50% prompt-token reduction. Eviction is non-fatal. The next turn rebootstraps from the LWC-side transcript.

Adding tool support to a new provider

  1. Implement LLMToolCapableProvider. supportsTools() should reflect a runtime probe (config flag) so the host can disable without redeploying.
  2. Translate FMToolSchema → vendor's tool description in the request.
  3. Translate vendor tool_call blocks → FMToolCall instances.
  4. Call dispatcher.dispatch(toolCall)FMToolResult. Routes through security boundaries automatically.
  5. Translate FMToolResult → vendor's tool_result shape, append, loop.
  6. Loop terminates on stop_reason = end_turn OR dispatcher.budgetExceeded().
  7. Persist incremental thread state via state.appendUserTurn / appendAssistantTurn / appendToolRoundTrip.

Related