The AI Character Engine includes a built-in HTTP API server for controlling the engine, managing characters, and inspecting state. The server uses native Node.js http module with no external framework dependencies.
Source: src/api/HttpServer.ts
- Server Configuration
- General Notes
- Characters
- Chat
- Events
- Proximity
- Decisions
- Social Systems
- State Management
- System
- Experiments
- Error Handling
import { HttpServer } from './src/api/HttpServer';
const server = new HttpServer(engine, {
port: 3000, // Default: 3000
host: '0.0.0.0', // Default: '0.0.0.0' (all interfaces)
});
await server.start();
// Server is now listening at http://0.0.0.0:3000
await server.stop();- Base URL: All endpoints are under
/api/. - Content-Type: All requests and responses use
application/json. - CORS: Enabled for all origins (
Access-Control-Allow-Origin: *). Methods: GET, POST, PUT, DELETE, OPTIONS. - Security Headers:
X-Content-Type-Options: nosniffX-Frame-Options: DENYX-Request-Id: req_<counter>(unique per request, useful for debugging)
- Body Size Limit: 1MB (1,048,576 bytes). Requests exceeding this limit receive a 400 error.
- Authentication: None. There is no authentication or authorization. This is noted for future implementation. Do not expose the API to untrusted networks without adding an authentication layer.
- Request IDs: Every response includes a
requestIdfield in error responses and anX-Request-Idheader in all responses.
List all registered characters with their proximity data.
Response:
{
"characters": [
{
"id": "barkeep",
"name": "Greta",
"archetype": "barkeep",
"identity": { "personality": "...", "backstory": "...", "goals": [], "traits": [] },
"activityTier": "active",
"closeness": 65.0,
"highWaterMark": 65.0,
"metadata": {},
"createdAt": 1709000000000,
"updatedAt": 1709000000000,
"proximity": {
"characterId": "barkeep",
"playerId": "default",
"closeness": 65.0,
"highWaterMark": 65.0,
"activityTier": "active",
"lastInteractionAt": 1709000000000,
"totalInteractions": 12,
"updatedAt": 1709000000000
}
}
]
}Example:
curl http://localhost:3000/api/charactersGet detailed information about a single character, including emotions and mood.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
string | Character ID |
Response (200):
{
"character": {
"id": "barkeep",
"name": "Greta",
"archetype": "barkeep",
"identity": { "personality": "...", "backstory": "...", "goals": [], "traits": [] },
"activityTier": "active",
"closeness": 65.0,
"highWaterMark": 65.0,
"metadata": {},
"createdAt": 1709000000000,
"updatedAt": 1709000000000
},
"proximity": {
"characterId": "barkeep",
"playerId": "default",
"closeness": 65.0,
"highWaterMark": 65.0,
"activityTier": "active",
"lastInteractionAt": 1709000000000,
"totalInteractions": 12,
"updatedAt": 1709000000000
},
"emotions": {
"characterId": "barkeep",
"active": [
{ "type": "joy", "intensity": 0.6, "decayRate": 0.05, "source": "good_business", "createdAt": 1709000000000 }
],
"mood": "joy",
"moodIntensity": 0.6
},
"mood": "joy"
}Response (404):
{ "error": "Character not found: unknown_id" }Example:
curl http://localhost:3000/api/characters/barkeepRegister a new character in the engine.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique character identifier |
name |
string | Yes | Character name |
archetype |
string | Yes | Must match a registered archetype ID |
identity |
object | Yes | Personality, backstory, goals, traits |
initialCloseness |
number | No | Starting closeness (0-100) |
metadata |
object | No | Arbitrary metadata |
Request Body Example:
{
"id": "new_char",
"name": "Sera",
"archetype": "merchant",
"identity": {
"personality": "Quiet but perceptive.",
"backstory": "A traveling alchemist.",
"goals": ["Find rare ingredients"],
"traits": ["observant", "cautious"]
},
"initialCloseness": 30
}Response (201):
{
"character": {
"id": "new_char",
"name": "Sera",
"archetype": "merchant",
"activityTier": "background",
"closeness": 30,
"highWaterMark": 30,
"metadata": {},
"createdAt": 1709000000000,
"updatedAt": 1709000000000
}
}Example:
curl -X POST http://localhost:3000/api/characters \
-H "Content-Type: application/json" \
-d '{"id":"new_char","name":"Sera","archetype":"merchant","identity":{"personality":"Quiet but perceptive.","backstory":"A traveling alchemist.","goals":["Find rare ingredients"],"traits":["observant","cautious"]}}'Remove a character from the engine.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
string | Character ID to remove |
Response (200):
{ "removed": "barkeep" }Response (404):
{ "error": "Character not found: unknown_id" }Example:
curl -X DELETE http://localhost:3000/api/characters/barkeepGet a comprehensive view of a character's internal state: memories, emotions, relationships, goals, needs, hierarchy, gossip, reputation, and more.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
string | Character ID |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
playerId |
string | Optional player ID (defaults to "default") |
Response (200):
{
"character": { "...CharacterState..." },
"proximity": { "...ProximityScore..." },
"emotions": { "...CharacterEmotions..." },
"relationships": [ { "fromId": "barkeep", "toId": "guard", "type": "friend", "strength": 70, "trust": 80 } ],
"goals": [ { "id": "g1", "description": "Keep the tavern profitable", "priority": 8, "status": "active", "steps": [] } ],
"recentMemories": [ { "id": "m1", "content": "...", "importance": 7, "currentImportance": 6.5 } ],
"summary": { "summary": "...", "relationshipNotes": "...", "keyFacts": [] },
"groups": [],
"workingMemory": [],
"routine": null,
"needs": { "characterId": "barkeep", "needs": [ { "type": "rest", "intensity": 0.3 } ] },
"nearbyCharacters": ["merchant", "bard"],
"gossipKnown": [],
"reputation": { "characterId": "barkeep", "scores": { "general": 15 } },
"hierarchy": [ { "characterId": "barkeep", "factionId": "tavern_staff", "rankLevel": 0 } ]
}Response (404):
{ "error": "Character not found: unknown_id" }Example:
curl "http://localhost:3000/api/characters/barkeep/introspection?playerId=player1"Send a message to a character and receive a reply. Requires closeness >= 40 (configurable via chatMinCloseness).
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
characterId |
string | Character to chat with |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
message |
string | Yes | The message to send |
playerId |
string | No | Player ID (defaults to "default") |
Response (200):
{ "reply": "Welcome to the Rusty Flagon, dear! What can I get you?" }Notes:
- The character must have closeness >= 40 to the player. Otherwise an error is returned.
- Chat interactions boost the character's closeness to the player.
- The conversation history is maintained in working memory.
Example:
curl -X POST http://localhost:3000/api/chat/barkeep \
-H "Content-Type: application/json" \
-d '{"message":"What is the mood in the tavern tonight?"}'Inject a game event into the engine. The event will be distributed to characters based on importance, proximity, and event filtering.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
event |
object | Yes | The event object |
event.type |
string | Yes | Event type (e.g., "bar_fight", "customer_arrives") |
event.source |
string | No | Who/what caused the event |
event.target |
string | No | Target of the event |
event.data |
object | No | Arbitrary event data |
event.importance |
number | No | Importance 1-10 |
event.timestamp |
number | Yes | Event timestamp (epoch ms) |
playerId |
string | No | Player ID |
Response (200):
{ "injected": true }Example:
curl -X POST http://localhost:3000/api/events \
-H "Content-Type: application/json" \
-d '{"event":{"type":"bar_fight","source":"patron","data":{"severity":"minor"},"importance":7,"timestamp":1709000000000}}'Get the closeness score between a character and the player.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
characterId |
string | Character ID |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
playerId |
string | Optional player ID (defaults to "default") |
Response (200):
{
"characterId": "barkeep",
"playerId": "default",
"closeness": 65.0,
"highWaterMark": 65.0,
"activityTier": "active",
"lastInteractionAt": 1709000000000,
"totalInteractions": 12,
"updatedAt": 1709000000000
}Response (404):
{ "error": "No proximity data for: unknown_id" }Example:
curl "http://localhost:3000/api/proximity/barkeep?playerId=player1"Manually boost a character's closeness to the player.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
characterId |
string | Character ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
amount |
number | Yes | Amount to boost (can be negative) |
playerId |
string | No | Player ID |
Response (200):
{
"characterId": "barkeep",
"playerId": "default",
"closeness": 75.0,
"highWaterMark": 75.0,
"activityTier": "active",
"lastInteractionAt": 1709000000000,
"totalInteractions": 13,
"updatedAt": 1709000000000
}Example:
curl -X POST http://localhost:3000/api/proximity/barkeep/boost \
-H "Content-Type: application/json" \
-d '{"amount":10}'Query the decision log with optional filters.
Request Body:
The body is a filters object. The specific filter fields depend on the DecisionLogEntry schema. Common filters include character ID, time range, and action type.
{
"characterId": "barkeep",
"limit": 10
}Response (200):
{
"decisions": [
{
"id": "d_1",
"characterId": "barkeep",
"playerId": "default",
"triggerType": "fast_tick",
"triggerEvent": null,
"contextTokens": 450,
"responseTokens": 85,
"inferenceTier": "heavy",
"action": "serve_drink",
"durationMs": 2800,
"createdAt": 1709000000000
}
],
"count": 1
}Example:
curl -X POST http://localhost:3000/api/decisions/query \
-H "Content-Type: application/json" \
-d '{"characterId":"barkeep"}'Get the total number of logged decisions.
Response (200):
{ "count": 586 }Example:
curl http://localhost:3000/api/decisions/countApply an emotion to a character.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
characterId |
string | Character ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
emotion |
string | Yes | Emotion type: joy, sadness, anger, fear, surprise, disgust, trust, anticipation |
intensity |
number | Yes | Intensity 0-1 |
Response (200):
{
"emotions": {
"characterId": "barkeep",
"active": [
{ "type": "joy", "intensity": 0.8, "decayRate": 0.05, "createdAt": 1709000000000 }
],
"mood": "joy",
"moodIntensity": 0.8
}
}Example:
curl -X POST http://localhost:3000/api/emotions/barkeep \
-H "Content-Type: application/json" \
-d '{"emotion":"joy","intensity":0.8}'Set or update a relationship between two characters.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
fromId |
string | Yes | Source character ID |
toId |
string | Yes | Target character ID |
type |
string | No | Relationship type: friend, rival, mentor, student, ally, enemy, neutral, romantic, family |
strength |
number | No | Relationship strength 0-100 |
trust |
number | No | Trust level 0-100 |
Response (200):
{
"relationship": {
"fromId": "barkeep",
"toId": "guard",
"type": "friend",
"strength": 70,
"trust": 80,
"notes": "",
"lastInteractionAt": 1709000000000,
"updatedAt": 1709000000000
}
}Example:
curl -X POST http://localhost:3000/api/relationships \
-H "Content-Type: application/json" \
-d '{"fromId":"barkeep","toId":"guard","type":"friend","strength":70,"trust":80}'Add a goal to a character.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
characterId |
string | Character ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
description |
string | Yes | Goal description |
priority |
number | No | Priority 1-10 (default: 5) |
steps |
array | No | Array of goal steps |
Response (201):
{
"goal": {
"id": "g_abc123",
"characterId": "barkeep",
"description": "Organize a tavern festival",
"priority": 7,
"status": "pending",
"steps": [
{ "description": "Buy decorations", "completed": false },
{ "description": "Invite performers", "completed": false }
],
"createdAt": 1709000000000
}
}Example:
curl -X POST http://localhost:3000/api/goals/barkeep \
-H "Content-Type: application/json" \
-d '{"description":"Organize a tavern festival","priority":7,"steps":[{"description":"Buy decorations","completed":false}]}'Set a world fact in the persistent world state.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | Fact key (unique identifier) |
value |
any | Yes | Fact value (any JSON-serializable value) |
category |
string | Yes | Category for grouping |
source |
string | Yes | Who/what set this fact |
Response (200):
{ "set": true, "key": "tavern_reputation" }Example:
curl -X POST http://localhost:3000/api/world-facts \
-H "Content-Type: application/json" \
-d '{"key":"tavern_reputation","value":"well-known","category":"locations","source":"narrator"}'Get a world fact by key.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
key |
string | Fact key |
Response (200):
{ "key": "tavern_reputation", "value": "well-known" }Response (404):
{ "error": "World fact not found: unknown_key" }Example:
curl http://localhost:3000/api/world-facts/tavern_reputationCreate a character group.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Group name |
memberIds |
string[] | Yes | Array of character IDs |
purpose |
string | Yes | Group purpose description |
Response (201):
{
"group": {
"id": "grp_abc123",
"name": "Tavern Staff",
"memberIds": ["barkeep", "bard"],
"leaderId": null,
"purpose": "Keep the tavern running",
"cohesion": 0.5,
"createdAt": 1709000000000
}
}Example:
curl -X POST http://localhost:3000/api/groups \
-H "Content-Type: application/json" \
-d '{"name":"Tavern Staff","memberIds":["barkeep","bard"],"purpose":"Keep the tavern running"}'Start a multi-agent conversation between characters.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
participantIds |
string[] | Yes | Array of character IDs |
topic |
string | Yes | Conversation topic |
maxTurns |
number | No | Maximum conversation turns |
Response (201):
{
"conversation": {
"id": "conv_abc123",
"participantIds": ["barkeep", "guard"],
"topic": "The suspicious stranger",
"turns": [
{
"characterId": "barkeep",
"content": "Did you see that cloaked figure?",
"timestamp": 1709000000000
},
{
"characterId": "guard",
"content": "I have been watching them closely.",
"timestamp": 1709000001000
}
],
"maxTurns": 6,
"status": "completed",
"startedAt": 1709000000000,
"completedAt": 1709000006000
}
}Example:
curl -X POST http://localhost:3000/api/conversations \
-H "Content-Type: application/json" \
-d '{"participantIds":["barkeep","guard"],"topic":"The suspicious stranger","maxTurns":6}'Persist the current engine state to the database.
Request Body: None required.
Response (200):
{ "saved": true }Example:
curl -X POST http://localhost:3000/api/state/saveCreate a named snapshot of the current state.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | No | Snapshot name (auto-generated if omitted) |
Response (201):
{ "snapshotId": "snap_abc123" }Example:
curl -X POST http://localhost:3000/api/state/snapshot \
-H "Content-Type: application/json" \
-d '{"name":"before-boss-fight"}'List all saved snapshots.
Response (200):
{
"snapshots": [
{
"id": "snap_abc123",
"name": "before-boss-fight",
"description": "",
"createdAt": 1709000000000
}
]
}Example:
curl http://localhost:3000/api/state/snapshotsExport the full engine state as a JSON object. Useful for backup or migration.
Request Body: None required.
Response (200):
Returns the complete engine state as a JSON object. The structure contains all character data, memories, relationships, world facts, and other subsystem state.
Example:
curl -X POST http://localhost:3000/api/state/export > backup.jsonImport engine state from a JSON body. Overwrites current state.
Request Body: The full state JSON object (as exported by /api/state/export).
Response (200):
{ "imported": true }Example:
curl -X POST http://localhost:3000/api/state/import \
-H "Content-Type: application/json" \
-d @backup.jsonGet engine statistics including character counts by tier, inference usage, and scheduler state.
Response (200):
{
"characters": {
"total": 4,
"active": 1,
"background": 2,
"dormant": 1
},
"inference": {
"totalRequests": 586,
"totalTokens": 125000,
"averageLatencyMs": 2800
},
"scheduler": {
"fastTicks": 1200,
"slowTicks": 40,
"running": true
}
}Example:
curl http://localhost:3000/api/statsHealth check endpoint. Returns 200 if all systems are operational, 503 if any system is degraded.
Response (200 or 503):
{
"inference": true,
"database": true
}A status of 503 is returned if either inference or database is false.
Example:
curl http://localhost:3000/api/healthGet a detailed metrics snapshot including latency percentiles, tool usage distribution, hint rates, and more.
Response (200):
{
"latency": {
"p50": 2800,
"p90": 4200,
"p99": 6100,
"count": 586,
"windowMs": 300000
},
"tools": {
"serve_drink": 85,
"tell_story": 62,
"trade_item": 45,
"patrol": 78,
"observe": 52,
"talk_to": 90
},
"actions": {
"tool_call": 412,
"dialogue": 150,
"idle": 24
},
"hints": {
"variety": 38,
"unused_tool": 22
}
}Notes: Metrics use a sliding window (default 5 minutes). Values outside the window are dropped automatically.
Example:
curl http://localhost:3000/api/metricsUpdate runtime configuration. Supports tick and proximity configuration changes without restart.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
tick |
object | No | Tick configuration overrides |
tick.fastTickMs |
number | No | Fast tick interval in milliseconds |
tick.slowTickMs |
number | No | Slow tick interval in milliseconds |
tick.batchSize |
number | No | Concurrent LLM calls per batch |
proximity |
object | No | Proximity configuration overrides |
proximity.decayRatePerTick |
number | No | Closeness decay per tick |
proximity.interactionBoost |
number | No | Closeness boost per interaction |
Response (200):
{ "updated": true }Example:
curl -X POST http://localhost:3000/api/config \
-H "Content-Type: application/json" \
-d '{"tick":{"fastTickMs":3000,"batchSize":8}}'The engine includes an A/B testing system (PromptExperiment) for testing different prompt configurations. These endpoints manage experiment variants and lifecycle.
Get the current experiment report including variant performance data.
Response (200):
{
"active": false,
"variants": [
{
"name": "control",
"config": {},
"weight": 1,
"assignments": 50,
"outcomes": { "tool_call": 30, "dialogue": 18, "idle": 2 }
},
{
"name": "verbose_prompt",
"config": { "systemPromptSuffix": "Be very detailed in your reasoning." },
"weight": 1,
"assignments": 48,
"outcomes": { "tool_call": 35, "dialogue": 12, "idle": 1 }
}
]
}Example:
curl http://localhost:3000/api/experimentRegister a new experiment variant.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Variant name |
config |
object | Yes | Variant configuration (prompt overrides, etc.) |
weight |
number | No | Selection weight (default: 1) |
Response (201):
{ "registered": "verbose_prompt" }Example:
curl -X POST http://localhost:3000/api/experiment/variant \
-H "Content-Type: application/json" \
-d '{"name":"verbose_prompt","config":{"systemPromptSuffix":"Be very detailed."},"weight":1}'Start the experiment. Variants will be assigned to characters using weighted random selection.
Request Body: None required.
Response (200):
{ "started": true, "active": true }Example:
curl -X POST http://localhost:3000/api/experiment/startStop the experiment and get the final report.
Request Body: None required.
Response (200):
{
"stopped": true,
"report": {
"active": false,
"variants": [
{
"name": "control",
"config": {},
"weight": 1,
"assignments": 50,
"outcomes": { "tool_call": 30, "dialogue": 18, "idle": 2 }
}
]
}
}Example:
curl -X POST http://localhost:3000/api/experiment/stopAll errors are returned as JSON with appropriate HTTP status codes.
{
"error": "Human-readable error message",
"code": "ERROR_CODE",
"requestId": "req_42"
}The code field is present for EngineError subclasses. The requestId field is always present in error responses.
| Status | Meaning | When |
|---|---|---|
| 200 | OK | Successful GET or POST |
| 201 | Created | Resource created (characters, goals, groups, snapshots, variants) |
| 204 | No Content | OPTIONS preflight response |
| 400 | Bad Request | Invalid JSON, missing required fields, type validation failure, body too large |
| 404 | Not Found | Resource not found (character, world fact, route) |
| 408 | Request Timeout | Inference provider timeout |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected errors (details not leaked to client) |
| 503 | Service Unavailable | Health check failed (inference or database down) |
The engine uses typed errors that map to specific HTTP status codes:
| Error Class | HTTP Status | Retriable | Description |
|---|---|---|---|
ValidationError |
400 | No | Invalid input data |
TimeoutError |
408 | Yes | Request timed out |
RateLimitError |
429 | Yes | Rate limit exceeded |
InferenceError |
500 | Yes | LLM provider error |
EngineError |
500 | No | General engine error |
Request bodies larger than 1MB (1,048,576 bytes) are rejected immediately with a 400 status:
{ "error": "Request body exceeds 1048576 bytes" }For non-EngineError exceptions, the server returns a 500 status with the error message but does not leak stack traces or internal implementation details to the client.