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_soqltool call routes throughFMSoqlValidator. 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
| Tool | Boundary | Returns |
|---|---|---|
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
| Provider | supportsTools() | Native API |
|---|---|---|
| AnthropicProvider | true | Messages API tool_use |
| OpenAIProvider | true | Chat Completions tools / tool_calls |
| AzureOpenAIProvider | true | Chat Completions (deployment-based) |
| BedrockProvider | true | bedrock-2023-05-31 Anthropic protocol |
| OllamaProvider | true | /api/chat tool_calls |
| EdenAIProvider | false | Smart-router; falls back to single-shot |
| GoogleVertexProvider | false | Gemini function-calling not yet wired |
| MockProvider | false | Test 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 viaFMOrgChatToolDispatcher.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
- Implement
LLMToolCapableProvider.supportsTools()should reflect a runtime probe (config flag) so the host can disable without redeploying. - Translate
FMToolSchema→ vendor's tool description in the request. - Translate vendor
tool_callblocks →FMToolCallinstances. - Call
dispatcher.dispatch(toolCall)→FMToolResult. Routes through security boundaries automatically. - Translate
FMToolResult→ vendor'stool_resultshape, append, loop. - Loop terminates on
stop_reason = end_turnORdispatcher.budgetExceeded(). - Persist incremental thread state via
state.appendUserTurn / appendAssistantTurn / appendToolRoundTrip.
Related
- Org Chat — the primary consumer
- Provider Configuration per-vendor
- Enterprise Security — trust boundary