Server
Server
The FabrCore server hosts your agents on an Orleans cluster. It provides REST and WebSocket APIs for client communication and background services for housekeeping.
Extension Methods
AddFabrCoreServer
Configures FabrCore server infrastructure including Orleans, services, streaming, and automatic discovery of the entry assembly plus referenced FabrCore dependencies.
var builder = WebApplication.CreateBuilder(args);
// Basic setup
builder.AddFabrCoreServer();
// Only needed for a runtime-loaded assembly outside the dependency graph
builder.AddFabrCoreServer(new FabrCoreServerOptions
{
AdditionalAssemblies = new List<Assembly>
{
typeof(MyCustomAgent).Assembly
}
});
What it configures:
- Orleans silo with clustering, persistence, and reminders
IFabrCoreRegistry(singleton) — Agent, plugin, and tool discoveryIFabrCoreChatClientService(singleton) — Chat client factoryIEmbeddings(singleton) — Embedding generation with cached clientIFileStorageService(singleton) — File storage with TTLFileCleanupBackgroundService— Automatic file cleanupAgentRegistryCleanupService— Automatic agent registry cleanup
UseFabrCoreServer
Registers FabrCore middleware, including the WebSocket endpoint for real-time agent communication.
var app = builder.Build();
app.UseFabrCoreServer();
Complete Startup Example
var builder = WebApplication.CreateBuilder(args);
builder.AddFabrCoreServer();
builder.Services.AddControllers();
var app = builder.Build();
app.UseFabrCoreServer();
app.MapControllers();
await app.RunAsync();
Cloud Server Configuration
A host normally reads its model configurations and API keys from a local fabrcore.json. With the Cloud Server feature enabled it pulls that same payload from a remote server instead and reports periodic heartbeats — useful when a fleet of silos should share one source of configuration rather than each shipping its own secrets file.
The feature is disabled by default and existing hosts are unaffected. It is configured entirely through appsettings.json; no fabrcore.json is required.
{
"FabrCore": {
"HostUrl": "https://agents.example.com",
"CloudServer": {
"Enabled": true,
"Url": "https://forge.vulcan365.ai",
"ApiKey": "<per-cluster API key>",
"ClusterId": null,
"Environment": null
},
"RemoteAdministration": {
"Enabled": true,
"PollWait": "00:00:20"
}
}
}
| Option | Default | Description |
|---|---|---|
Enabled | false | Opt in to cloud-delivered configuration |
Url | https://forge.vulcan365.ai | Base URL; override for a self-hosted server |
ApiKey | null | Per-cluster key presented as a bearer token |
ClusterId | Orleans ClusterOptions.ClusterId | Cluster identifier sent to the server |
Environment | IHostEnvironment.EnvironmentName | Used for appsettings-style layering |
RefreshInterval | 00:05:00 | How often the host checks for updated configuration |
RequestTimeout | 00:00:30 | Timeout for individual requests |
CacheLastKnownGood | true | Cache the last good config to disk so the host can start while the server is unreachable |
CacheFilePath | fabrcore.cloud-cache.json | Cache location, relative to the content root |
StartupFailureBehavior | Fail | Fail stops startup with a clear error; StartDegraded starts with no configuration and returns 404 on model lookups until a sync succeeds |
Securing ApiKey — user secrets, environment variables, or a vault-backed configuration provider — is up to you. The disk cache has the same secrets-on-disk profile as fabrcore.json, so treat it the same way.
Remote administration
Remote administration lets the cloud server dispatch administrative calls into the host. It is disabled by default and requires CloudServer:Enabled to be true — a host cannot enable remote administration without also enabling its cloud connection. It reuses FabrCore:CloudServer:ApiKey; there is no separate credential.
FabrCore:HostUrl is the only remote-administration target. It must be an absolute http(s) URL reachable from the host process, and requests outside /fabrcoreapi/ are rejected. The host logs a startup warning for a non-loopback URL, because the Cloud Server API key then travels the network.
Connect is a long poll, not an ordinary request. PollWait defaults to 20 seconds and configuration validation rejects anything above 55, but the effective server wait is clamped to 1–25 seconds — so a configured 50 is silently honored as 25. The host adds a 10-second transport buffer on top for its per-attempt timeout, and the connect channel uses a dedicated transport so app-wide HttpClient resilience defaults (notably Aspire's 10-second attempt timeout) cannot cut the poll short. A 204 after the wait is a normal empty queue, not a failure.
FabrCore Forge is the first-party server, but the wire contract is open: protocol constants and DTOs ship in FabrCore.Core under FabrCore.Core.CloudServer, paths are versioned (/fabrcore-cloud/v1/…), and all JSON is camelCase. A host configured against any conforming server behaves identically. Envelopes with a schemaVersion newer than the host supports are rejected and the last-known-good configuration is kept.
FabrCoreRegistry
FabrCoreRegistry discovers agents, plugins, tools, and provider registrations at startup by scanning the application entry assembly and referenced FabrCore dependency graph for alias attributes. No routine assembly list is required — decorate your classes and reference their projects or packages.
Discovery Scope and Dynamic Assemblies
In v1.7.1, referenced agent and plugin projects are found automatically. Use AdditionalAssemblies only for assemblies loaded dynamically or otherwise outside the application's dependency graph:
builder.AddFabrCoreServer(new FabrCoreServerOptions
{
AdditionalAssemblies = new List<Assembly>
{
typeof(MyCustomAgent).Assembly,
typeof(MyPluginLibrary).Assembly
}
});
FabrCoreServerOptions.RegistryAssemblies exposes the exact assembly set used to build the registry. This keeps service discovery and runtime registry construction aligned and prevents a plugin from appearing in one path but not the other. For UI components or MVC controllers, continue to use ASP.NET Core application parts.
How Discovery Works
| Attribute | Target | Description |
|---|---|---|
[AgentAlias("name")] | Class | Registers a FabrCoreAgentProxy under one or more aliases |
[PluginAlias("name")] | Class | Registers an IFabrCorePlugin for agent use |
[ToolAlias("name")] | Method | Registers a static method as a callable tool |
All attributes support AllowMultiple = true, so a single component can have multiple aliases:
[AgentAlias("assistant")]
[AgentAlias("my-assistant")]
public class AssistantAgent : FabrCoreAgentProxy
{
// Reachable via either alias
}
Registry API
| Method | Returns | Description |
|---|---|---|
GetAgentTypes() | List<RegistryEntry> | All discovered agents with their aliases |
GetPlugins() | List<RegistryEntry> | All discovered plugins with their aliases |
GetTools() | List<RegistryEntry> | All discovered tools with their aliases |
FindAgentType(alias) | Type? | Resolve an agent alias to its concrete type |
Discovery REST Endpoint
The registry is exposed via a built-in REST endpoint, allowing clients to discover available agents, plugins, and tools at runtime:
| Method | Endpoint | Description |
|---|---|---|
GET | /fabrcoreapi/Discovery | Returns all registered agents, plugins, and tools |
{
"agents": [
{ "typeName": "MyApp.AssistantAgent", "aliases": ["assistant", "my-assistant"] }
],
"plugins": [
{ "typeName": "MyApp.WeatherPlugin", "aliases": ["weather"] }
],
"tools": [
{ "typeName": "MyApp.MathTools.Add", "aliases": ["add-numbers"] }
]
}
The discovery endpoint makes it easy to verify your agents, plugins, and tools are correctly registered. Hit /fabrcoreapi/Discovery during development to confirm everything was picked up by the assembly scan — no guesswork needed.
REST API
All endpoints are prefixed with /fabrcoreapi.
Agent API
| Method | Endpoint | Description |
|---|---|---|
POST | /fabrcoreapi/Agent/create | Create or intentionally reconfigure one or more agents |
POST | /fabrcoreapi/Agent/blueprint | Idempotently ensure a principal's baseline agent set |
GET | /fabrcoreapi/Agent/health/{handle} | Get agent health status |
POST | /fabrcoreapi/Agent/chat/{handle} | Send a message and receive a response |
All Agent API endpoints require the x-user-handle header to identify the caller principal. For an idempotent, application-owned way to provision a standard agent set, see Blueprints.
POST /fabrcoreapi/Agent/create
x-user-handle: principal123
[{
"handle": "my-agent",
"agentType": "MyAgent",
"models": "default",
"systemPrompt": "You are a helpful assistant."
}]
Diagnostics API
| Method | Endpoint | Description |
|---|---|---|
GET | /fabrcoreapi/Diagnostics/agents | List all registered agents |
GET | /fabrcoreapi/Diagnostics/agents/{key} | Get specific agent info |
GET | /fabrcoreapi/Diagnostics/agents/statistics | Agent count statistics |
POST | /fabrcoreapi/Diagnostics/agents/purge | Purge deactivated agents |
File & Embeddings API
| Method | Endpoint | Description |
|---|---|---|
POST | /fabrcoreapi/File/upload | Upload a file (multipart/form-data) |
GET | /fabrcoreapi/File/{fileId} | Download a file |
GET | /fabrcoreapi/File/{fileId}/info | Get file metadata |
POST | /fabrcoreapi/Embeddings | Generate a vector embedding for a single text |
POST | /fabrcoreapi/embeddings/batch | Generate embeddings for multiple texts in a single request (up to 2,048 items) |
Batch Embeddings
The batch endpoint accepts a list of items, each with a caller-provided id and text:
{
"items": [
{ "id": "item-1", "text": "First document to embed" },
{ "id": "item-2", "text": "Second document to embed" },
{ "id": "item-3", "text": "Third document to embed" }
]
}
{
"results": [
{ "id": "item-1", "vector": [0.0123, -0.0456, ...], "dimensions": 1536 },
{ "id": "item-2", "vector": [0.0789, -0.0321, ...], "dimensions": 1536 },
{ "id": "item-3", "vector": [0.0654, -0.0987, ...], "dimensions": 1536 }
]
}
Validation Rules
| Condition | Error (400) |
|---|---|
| Items is null or empty | "Items list must not be empty." |
| Any item has empty id | "Item at index {i} has an empty Id." |
| Any item has empty text | "Item at index {i} (Id='{id}') has empty Text." |
| More than 2,048 items | "Batch size {n} exceeds maximum of 2048." |
All endpoints return consistent error responses: 200 (Success), 400 (Bad Request), 404 (Not Found), 500 (Internal Server Error).
Chat Completions API
FabrCore exposes an OpenAI-compatible chat completions endpoint via the ChatCompletionController, allowing external systems to interact with configured LLM models using standard OpenAI client libraries.
Endpoint
| Method | Endpoint | Description |
|---|---|---|
POST | /fabrcoreapi/ChatCompletion | Send a chat completion request to the configured LLM |
The endpoint uses IFabrCoreChatClientService to resolve the model by name from fabrcore.json. It is designed for single-turn completions — no streaming or tool calling.
Request Format
{
"Messages": [
{ "Role": "user", "Content": "Extract entities from this text..." }
],
"Options": {
"Model": "gpt-4o-mini",
"MaxOutputTokens": 2048,
"Temperature": 0.2
}
}
Options is optional. Supported options: Model (defaults to "default"), MaxOutputTokens, Temperature, TopP, TopK, StopSequences, FrequencyPenalty, PresencePenalty.
Response Format
{
"Text": "The extracted response text...",
"Model": "gpt-4o-mini",
"Usage": { "InputTokens": 150, "OutputTokens": 80 }
}
On a client host (AddFabrCoreClient), use IFabrCoreHostApiClient.GetChatCompletionAsync() which POSTs to this endpoint on the server host. On a server host (AddFabrCoreServer), resolve IFabrCoreChatClientService directly from DI.
WebSocket API
WebSocket v2 provides authenticated real-time messaging at /ws. Browser clients first obtain a 30-second, single-use ticket from POST /fabrcoreapi/ws/ticket, then connect with the fabrcore.v2 and fabrcore.ticket.<token> subprotocols. Native clients can use the same ticket flow through FabrCore.Client.WebSocket.
Query-string identity and raw AgentMessage payloads are no longer accepted. Clients send typed operations such as message.send, event.send, health.get, and tracked.list. Durable clients use sequence acknowledgements, replay, and explicit gap recovery.
See the WebSocket v2 guide for the handshake, typed client, operation catalog, delivery guarantees, and migration checklist.
Orleans Streaming
FabrCore uses Orleans streams for agent communication:
| Stream | Namespace | Purpose |
|---|---|---|
| AgentChat | AgentChat | Request/response messaging |
| AgentEvent | AgentEvent | Fire-and-forget events |
Background Services
| Service | Interval | Description |
|---|---|---|
FileCleanupBackgroundService | Configurable | Removes expired files and orphaned entries |
AgentRegistryCleanupService | Every 6 hours | Purges agents deactivated more than 7 days ago |
Orleans Provider Packages & Auto-Provisioning
FabrCore.Host ships with in-memory Localhost mode built in. Production backends live in dedicated provider packages that are discovered automatically — reference the package, set Orleans:ClusteringMode, and provide a connection string. No registration code is required (explicit options.UseSqlServer() / options.UseAzureStorage() calls are also available).
| Mode | NuGet Package | Clustering | Grain State | Reminders | Streams |
|---|---|---|---|---|---|
Localhost | built into FabrCore.Host | In-memory | In-memory | In-memory | In-memory |
SqlServer | FabrCore.Host.SqlServer | SQL Server | SQL Server | SQL Server | In-memory |
AzureStorage | FabrCore.Host.AzureStorage | Azure Tables | Azure Blobs (default) or Tables | Azure Tables | Azure Queues (default) or in-memory |
Both provider packages auto-provision their backing resources at startup (controlled by Orleans:AutoInitDatabase, default true) and fail fast with a clear error when the connection string is invalid. Custom backends can implement IFabrCoreOrleansProvider and register via options.UseOrleansProvider(...).
SQL Server: Automatic Table Deployment
The initializer runs embedded SQL scripts that create tables for all four Orleans subsystems under a dedicated orlns schema:
| Subsystem | Schema | Purpose |
|---|---|---|
| Clustering | orlns | Silo membership and liveness tables for cluster coordination |
| Persistence | orlns | Grain state storage tables for agent and client state |
| Reminders | orlns | Persistent reminder registration tables |
| Streaming | orlns | Pub/sub state tables for Orleans stream delivery |
The SQL scripts are embedded resources within the FabrCore.Host.SqlServer package. The initializer checks whether the schema and tables already exist before running, making it safe for repeated startups.
All Orleans tables are created under the orlns schema to avoid naming conflicts with your application's tables. This keeps the Orleans infrastructure cleanly separated from your domain data.
// appsettings.json — tables are auto-provisioned on first startup
{
"Orleans": {
"ClusterId": "prod",
"ServiceId": "fabrcore",
"ClusteringMode": "SqlServer",
"ConnectionString": "Server=localhost;Database=FabrCore;Trusted_Connection=True;"
}
}
Azure Storage: Tables, Blobs, and Queues
With the FabrCore.Host.AzureStorage package, the host runs entirely on an Azure Storage account: tables for clustering, reminders, and stream pub/sub state; blobs for grain state; queues for streams. At startup the provider creates the tables, the grain-state blob container, and the stream queues if they do not exist.
| Subsystem | Azure Service | Notes |
|---|---|---|
| Clustering | Table Storage | Silo membership and liveness (OrleansSiloInstances) |
| Grain persistence | Blob Storage (default) | Agent conversation state can exceed the 1 MB table entity limit; Table Storage is opt-in for known-small state |
| Reminders | Table Storage | Persistent reminder registrations |
| Streams | Queue Storage (default) | Durable delivery; queue names derived deterministically from ServiceId |
// Resources are auto-provisioned on first startup.
// For local development, run Azurite and use "UseDevelopmentStorage=true".
{
"Orleans": {
"ClusterId": "prod",
"ServiceId": "fabrcore",
"ClusteringMode": "AzureStorage",
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...",
"AzureStorage": {
"GrainStorage": "Blob",
"ContainerName": "fabrcore-grainstate",
"Streams": "AzureQueue",
"StreamQueueCount": 8
}
}
}
The values shown above are the defaults — omit the entire Orleans:AzureStorage section and you get blob grain storage and durable queue streams out of the box. StreamQueueCount must match across all silos in a cluster.