Actionary is designed agent-first. The substrate describes the business as data. The embedded agent reasons over that data through the same contract the UI renders on — one entity_config payload feeds the SPA, the admin REST API, and the MCP tool surface. Same schema. Same permissions. Same audit trail.
Adding a field to a Warranty Claim entity surfaces it in the sidebar, the list view, the modal, the search index, and the agent’s tool catalogue — automatically, because there is nothing to update.
MCP is the contract
The tool surface is ~72 MCP tools mounted on a FastMCP server, gated by OAuth 2.0 + Dynamic Client Registration + PKCE at mcp.{base}. Any DCR-compliant MCP client registers once and calls the same catalogue the embedded agent sees. One OAuth flow. One set of tool schemas. One permission model.
Because the business is described as data, the MCP tools work on any entity — propose_entity_update and execute_entity_update operate on Deals and on a tenant-defined Warranty Claim uniformly. Registering a new entity through Schema Admin extends the agent’s reach the same instant it extends the UI’s. Every tool declares its role permissions via AGENT_TOOL_ROLE_ALLOWLIST, and the same @require_any(...) decorators that gate the browser gate the agent. If the caller cannot do it, the agent cannot do it. Every agent action runs under the caller’s own permissions — one code path, one authorisation check.
Reasoning proposes, the platform lands — durably
The agent proposes actions. The platform lands them exactly once. Firing the same tool call twice produces the same outcome as firing it once. The propose_entity_update tool stages a diff in Redis with a 900-second TTL; a separate execute_entity_update call commits it. The LLM proposes. The operator or a downstream check confirms. The platform applies.
The destructive-confirm gate lives on agent_pending_tool_confirmations with a (conversation_id, tool_use_id) unique constraint. Delete, archive, revoke, bulk — the gate matches on tool-name pattern, generic across entities. The crucial property: the pending tool_use block stays out of the assistant message until confirmation lands. The message protocol stays self-consistent across the pending → confirmed → tool_result transition, so a replay hours later still finds a well-formed conversation with no orphan calls.
Deeper mechanics — FOR UPDATE SKIP LOCKED step claims in the workflow engine, Idempotency-Key on public writes, HMAC-signed retriable webhooks with a dead-letter ledger, and 350+ deliberate ON CONFLICT clauses across the schema — are the durable execution story.
Continuous-learning reflection memory
Every multi-tool agent turn writes a per-tenant agent_reflections row: vector(1024) pgvector embedding, ordered tool sequence, referenced entities, referenced fields, trust tier, outcome. The next turn retrieves top-K reflections by hybrid score (cosine × trust-tier × outcome + recency + capped hit-count) and injects them as a past_experience block. Schema-drift triggers on entity_fields invalidate reflections whose referenced field is deleted or renamed, so the memory tracks the schema turn by turn.
Operators see and govern every reflection at /settings/agent/memory — edit, re-embed, promote to Curated or Playbook, invalidate, restore. An Evaluation tab rolls up retrieval health: ever-retrieved percentage, average hits per retrieved, outcome stack, top playbooks, recent rejections. Per-tenant embedding provider — OpenAI text-embedding-3-small@1024d (enterprise default, covered by the OpenAI DPA), Voyage voyage-3, or a self-hosted sidecar — all three emit 1024-dim vectors, so the column type is provider-agnostic.
RAG on the substrate — same gates, same audit log
The agent reads what the tenant uploads: PDFs, Word documents, PowerPoint decks, CSVs, JSON, plain text and Markdown, and every audio and video container the meeting recorder handles. A complete Retrieval-Augmented Generation stack built on the primitives that serve the rest of the platform.
Retrieval is pgvector HNSW cosine search over content_asset_chunks, embedded with text-embedding-3-small@1536d. Every query is filtered by the unified record_access row-level visibility predicate — the same rule the SPA list endpoint applies — so an asset a caller cannot see cannot surface. Hits reach the agent through search_upload_contents with real page numbers (PDF, PowerPoint) or time offsets (audio, video), bounded at ~2 k tokens per retrieval block. A citation reads “page 14 of the Q4 board pack” or “at 08:42 in the Acme discovery call” — the exact human-readable pointer the operator can open.
The RAG stack runs on the same Postgres, the same audit log, and the same visibility gates as every other read path in the product.
The five-layer safety perimeter
Defence in depth applied to every agent turn:
- Typed
tool_resultframing — untrusted tool output is structurally distinguished from operator intent at the message-protocol level. - System-prompt directive — names the common injection patterns explicitly so the model has a first-line filter.
- Untrusted-content markers —
shared/agent_untrusted.pywraps every text-shaped path a tenant can influence. - Tenant tool policy —
allow_destructive_toolsandrequire_confirm_destructivesurface at/settings/agent. - Tenant content policy —
blocked_phrases,profanity_filter_enabled,response_token_cap,tool_allow_listrun after every turn and before the assistant text is persisted; the per-turn log entry tags which rule fired without writing the un-redacted text.
Read the full security posture for how the perimeter composes with tenant isolation, RLS, and the record_access visibility gate.
BYOK every provider — per-step failover
The provider layer is portable. Anthropic, OpenAI, AWS Bedrock, Azure OpenAI, and Gemini all ship as full streaming adapters. Tenant settings expose a primary and a fallback. Per-step automatic failover fires on provider_unavailable, rate_limit, or quota_exhausted — the SPA renders an inline “Switched from <primary> to <fallback>” badge and a fresh TurnStart opens with the fallback model. Failover fires only before any TextDelta or TurnEnd on the current step; once visible content lands, the active provider owns the rest of the step. Token counts stay clean and streams stay intact.
Tenant keys are encrypted at rest with the HKDF-derived llm-provider-keys subsystem key. Logs carry the last-4 fingerprint of the key and nothing more.
Eight token-efficiency mechanisms
Token cost is engineered down at eight points: two-layer prompt caching (a stable generic system prompt + a per-tenant entity catalog, both ephemeral-cached), an in-prompt entity catalog so the agent skips orientation tool calls, deterministic tool-result compaction outside the most-recent-4 window, an operator /compact slash command, per-step model routing (a reasoning-tier model for planning, a smaller model for dispatch), a conversation-size pre-flight guardrail that refuses turns projected over 85% of the per-minute cap, idle-aware resume tuned to the 15-minute prompt-cache TTL, and a current-date preamble in the per-turn user message so the cached entity catalog stays valid across days.
Live rate-limit headroom sits under the composer — a continuous countdown of the provider’s per-minute input-token budget, amber under 16% remaining, red under 4%. Operators see budget pressure as it happens, not after the failure.
Latency by construction
Read-only tool_use blocks within one turn fan out under asyncio.Semaphore(8) and stream results in completion order via asyncio.as_completed. Destructive calls stay sequential. A turn with ten read calls finishes in roughly the slowest call’s time — typically a 3-5× wall-clock win over sequential dispatch. Every tool call and its duration lands on agent_messages.tool_calls_json + tool_durations_json, so /platform/agent renders per-tenant, per-tool, per-entity p50 / p95 latency and a most-recent-50 drilldown. Full traffic, one observability pipeline, the same rows the agent already writes.
The takeaway
Actionary is agent-first from the substrate up. The business is data. The substrate generates the MCP tool surface. The execution layer lands what the agent proposes exactly once, no matter how many times the network retries. The agent inherits the caller’s permissions through the same code path as a browser session. The reflection memory is operator-inspectable. The safety perimeter has five composing layers. The LLM provider is a per-tenant configuration. Agent-first is the shape of the platform. For the sales-leader view of what this unlocks, see the CRO angle.