logicspike/docs

Chat Engine

Tool Registry — Architecture Reference

Last updated: 2026-06-29
Status: Architecture decided. Implementation Phase 7.


Context

CE currently gives the LLM one tool: recall_memory. This makes it a talking bot only.

Tool Registry transforms CE into an action platform — the bot can check orders, capture leads, book appointments, submit forms, and call any tenant-configured HTTP endpoint without a CE code change.


Two Types of Tools

Built-in Tools (ship with CE)

Tool What it does Requires
recall_memory Fetch CI memories on demand (L3) ciEnabled + memoryTier >= 3
search_knowledge Vector search the knowledge base knowledgeEnabled + sources configured
collect_lead Save name/email/phone → leads table leadCaptureEnabled
escalate_to_human Trigger SLA alarm + handoff Always available
submit_form Fill and submit a vlozi Form formId configured in config

Custom Tenant Tools (webhook-configured, no code change)

Tenant registers in dashboard:

{
  "name": "check_order",
  "description": "Look up a customer order by order number. Use when customer asks about their order status.",
  "parameters": {
    "order_id": { "type": "string", "description": "The order number" }
  },
  "webhook_url": "https://mystore.com/api/orders/{order_id}",
  "auth_header": "Bearer sk-...",
  "timeout_ms": 5000
}

CE dynamically constructs the LLM tool definition, calls the webhook when invoked, injects the result. No CE deployment per new tool — the tenant owns the configuration.


Tool Call Loop

// Build tool set for this message (intent-gated, session-deduplicated)
const tools = buildToolSet(config, intent, sessionVariables)
 
// LLM loop — max 5 rounds, prevents runaway loops
let round = 0
while (round < 5) {
  const response = await llm.stream(messages, tools)
 
  if (response.type === "text") break         // final response, return to contact
 
  if (response.type === "tool_call") {
    const result = await executeTool(response.tool, response.args, config)
    messages.push(toolResultMessage(response.tool, result))
    round++
  }
}

executeTool() dispatches:

  • Built-in name → built-in handler (callCI, leads service, forms service, etc.)
  • Custom name → look up ce_custom_tools → HTTP webhook call

Tool Permission Gating

Tools are filtered before being offered to the LLM. The LLM never sees a tool it can't use.

Tool Condition
recall_memory ciEnabled AND memoryTier >= 3
search_knowledge knowledgeEnabled AND sources configured
collect_lead leadCaptureEnabled
escalate_to_human Always — never filtered
submit_form formId present in config
Custom tools tool.enabled AND tenant subscription active

Session deduplication: If collect_lead already succeeded this session, it's removed from the tool set — don't re-collect the same lead. Session variables track completed tool states:

{ "lead_collected": true, "order_id_checked": "12345" }

Intent → Tool Mapping

Only offer tools relevant to the detected intent. Giving the LLM 10 tools on every message wastes tokens and causes hallucinated tool calls.

Intent Tools offered
greeting / chitchat None
product_question search_knowledge
order_status Custom check_order (if configured)
purchase_intent collect_lead, custom check_availability
complaint escalate_to_human
escalation_request escalate_to_human
Tier 3+ any intent + recall_memory always available as safety net

Tool Failure Handling

Decision: Acknowledge and continue.

When a tool fails (timeout, 4xx, 5xx, malformed JSON):

  1. CE returns error string to LLM:
    "Tool 'check_order' failed: upstream timeout. Inform the customer you cannot retrieve their order right now."

  2. LLM responds naturally:
    "I'm having trouble pulling up your order right now. Could you check your confirmation email, or try again in a few minutes?"

  3. Session continues — bot stays in control

  4. Failure logged to ce_tool_calls + Analytics Engine

Circuit breaker per tool per session:
Same custom tool fails 3 times in one session → removed from tool set for the remainder of that session. Prevents repeated failures from inflating token cost.

No automatic escalation on tool failure — the bot handles it conversationally.


Cost Impact

Every tool call adds one LLM round (tool result injected → LLM continues):

Tool calls per message LLM rounds Cost multiplier
0 1
1 2 ~1.8×
2 3 ~2.5×
5 (max) 5 ~4×

Mitigations:

  • Intent-gated tool set reduces unnecessary tool calls
  • Session variable deduplication removes already-completed tools
  • Haiku used where tool tasks are mechanical (not emotional/complex)
  • ce_tool_calls analytics surfaces which tools are used vs ignored — prune accordingly

Database

ce_custom_tools

CREATE TABLE ce_custom_tools (
  id          TEXT PRIMARY KEY,
  tenant_id   TEXT NOT NULL,
  name        TEXT NOT NULL,
  description TEXT NOT NULL,
  parameters  JSONB NOT NULL,
  webhook_url TEXT NOT NULL,
  auth_header TEXT,                        -- stored encrypted
  timeout_ms  INTEGER DEFAULT 5000,
  enabled     BOOLEAN DEFAULT true,
  created_at  TIMESTAMPTZ DEFAULT now(),
  UNIQUE (tenant_id, name)
);

Separate table (not JSONB in ce_bot_config) for independent CRUD, per-tool enable/disable, and per-tool analytics.

ce_tool_calls

CREATE TABLE ce_tool_calls (
  id          TEXT PRIMARY KEY,
  session_id  TEXT NOT NULL,
  tenant_id   TEXT NOT NULL,
  message_id  TEXT,
  tool_name   TEXT NOT NULL,
  parameters  JSONB,
  result      JSONB,
  success     BOOLEAN NOT NULL,
  latency_ms  INTEGER,
  error       TEXT,
  created_at  TIMESTAMPTZ DEFAULT now()
);

Also written to Analytics Engine for dashboard: tool success rate, latency, most-used tools.

ce_session_variables

CREATE TABLE ce_session_variables (
  id         TEXT PRIMARY KEY,
  session_id TEXT NOT NULL,
  tenant_id  TEXT NOT NULL,
  key        TEXT NOT NULL,
  value      TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now(),
  UNIQUE (session_id, key)
);

Cached in DO during active session, flushed to Neon async. Used for: completed tool deduplication, slot filling state (Phase 10 Conversation Flows), lead data collected.


New Files (Phase 7)

apps/chat-engine/src/
├── lib/
│   ├── tools.ts          buildToolSet(), executeTool(), toolResultMessage()
│   └── tool-webhook.ts   HTTP webhook executor — timeout, circuit breaker, response parsing
└── routes/
    └── tools.route.ts    CRUD: POST/GET/PATCH/DELETE /tools (tenant tool management)

Modified:

  • src/lib/orchestrator.ts — replace static RECALL_MEMORY_TOOL with buildToolSet(); wrap LLM call in 5-round loop
  • src/db/schema.ts — add ce_custom_tools, ce_tool_calls, ce_session_variables

What Doesn't Change

  • recall_memory is currently the only tool (llm.ts:43). It already works, and the 2-pass recall loop in the orchestrator (orchestrator.ts:286) is exactly the pattern the 5-round loop generalizes — this is an extension of existing behavior, not a rewrite
  • [[HANDOFF]] persona trigger still works alongside escalate_to_human tool — both result in escalation
  • Billing accumulator — tool call LLM rounds billed as normal tokens
  • DO hot path — session variables cached in DO, flushed to Neon async
Chat Engine