Assistant Identity

Focus

Name, Avatar & Emoji Resolution

The assistant identity subsystem resolves the display name, avatar, and emoji for each assistant in a session. It follows a priority chain — workspace configuration files take precedence over global config, which falls back to built-in defaults. Strict size limits and normalization ensure consistent rendering across all clients.

Core Type AssistantIdentity — agentId, name, avatar, emoji
Default Identity {agentId: "main", name: "Assistant", avatar: "A"}
Resolution Function resolveAssistantIdentity(params)
Size Limits Name: 50 chars, Avatar: 200 chars, Emoji: 16 chars

Table of Contents

Overview

When the Gateway renders a message from an assistant, it needs to know what name to display, what avatar image (or letter) to show, and optionally what emoji to use as a status indicator. These values are not hardcoded — they come from a resolution chain that checks multiple sources in priority order.

This design allows workspace-level customization (e.g., a project can define a custom assistant name and avatar in its configuration files) while ensuring that every assistant always has a valid identity, even if no customization is provided.

🎨 Customization: Teams can override assistant appearance per-workspace by placing identity configuration in their project directory. The resolver automatically picks up these overrides without any Gateway restart.

Core Types

AssistantIdentity

The resolved identity object that fully describes how an assistant should be presented in the UI.

interface AssistantIdentity {
  /** Unique agent identifier (e.g., "main", "code-review", "docs") */
  agentId: string;

  /** Display name shown in the chat UI (max 50 characters) */
  name: string;

  /**
   * Avatar value. Can be:
   * - A single uppercase letter (e.g., "A")
   * - A URL pointing to an image (max 200 characters)
   * - An emoji character used as the avatar
   */
  avatar: string;

  /** Optional emoji displayed next to the assistant name */
  emoji?: string;
}

DEFAULT_ASSISTANT_IDENTITY

The built-in fallback identity used when no configuration overrides are found.

const DEFAULT_ASSISTANT_IDENTITY: AssistantIdentity = {
  agentId: 'main',
  name: 'Assistant',
  avatar: 'A',
  // emoji is intentionally undefined by default
};

Size Limit Constants

ConstantValueApplied To
MAX_ASSISTANT_NAME50AssistantIdentity.name
MAX_ASSISTANT_AVATAR200AssistantIdentity.avatar
MAX_ASSISTANT_EMOJI16AssistantIdentity.emoji
⚠ Truncation behavior: Values exceeding these limits are silently truncated, not rejected. This prevents configuration errors from breaking the entire identity resolution pipeline. A warning is logged when truncation occurs.

Resolution Priority

The resolveAssistantIdentity() function checks sources in the following priority order. The first source that provides a value for each field wins.

flowchart TD
    A[resolveAssistantIdentity called] --> B{workspaceDir provided?}
    B -- Yes --> C[Read workspace config files]
    C --> D{Workspace has identity config?}
    D -- Yes --> E[Extract name, avatar, emoji from workspace]
    D -- No --> F{agentId provided?}
    B -- No --> F
    E --> G[Apply size limits and normalization]
    F -- Yes --> H[Lookup agentId in global config]
    H --> I{Config has entry for agentId?}
    I -- Yes --> J[Extract name, avatar, emoji from config]
    I -- No --> K[Use DEFAULT_ASSISTANT_IDENTITY]
    F -- No --> K
    J --> G
    K --> G
    G --> L[normalizeAvatarValue]
    L --> M[normalizeEmojiValue]
    M --> N[Return AssistantIdentity]

resolveAssistantIdentity

The main resolution function takes configuration, an optional agent ID, and an optional workspace directory, then returns a fully-resolved identity.

Function Signature & Parameters

The function accepts a single params object to keep the API clean and extensible.

interface ResolveAssistantIdentityParams {
  /** Global gateway configuration */
  cfg: GatewayConfig;

  /** Agent ID to resolve (defaults to "main") */
  agentId?: string;

  /** Workspace directory for project-level overrides */
  workspaceDir?: string;
}

function resolveAssistantIdentity(
  params: ResolveAssistantIdentityParams
): AssistantIdentity {
  const { cfg, agentId = 'main', workspaceDir } = params;

  // 1. Check workspace-level overrides first
  if (workspaceDir) {
    const wsIdentity = readWorkspaceIdentity(workspaceDir, agentId);
    if (wsIdentity) {
      return normalizeIdentity(wsIdentity);
    }
  }

  // 2. Check global config for agent-specific identity
  const configIdentity = cfg.assistants?.[agentId];
  if (configIdentity) {
    return normalizeIdentity({
      agentId,
      name: configIdentity.name,
      avatar: configIdentity.avatar,
      emoji: configIdentity.emoji,
    });
  }

  // 3. Fall back to defaults
  return { ...DEFAULT_ASSISTANT_IDENTITY, agentId };
}

Workspace File Reading

The workspace reader looks for identity configuration in well-known file locations within the project directory.

function readWorkspaceIdentity(
  workspaceDir: string,
  agentId: string
): Partial<AssistantIdentity> | null {
  const configPaths = [
    path.join(workspaceDir, '.assistant', `${agentId}.json`),
    path.join(workspaceDir, '.assistant', 'identity.json'),
    path.join(workspaceDir, '.config', 'assistant.json'),
  ];

  for (const configPath of configPaths) {
    try {
      const raw = fs.readFileSync(configPath, 'utf-8');
      const parsed = JSON.parse(raw);
      if (parsed.name || parsed.avatar || parsed.emoji) {
        return { agentId, ...parsed };
      }
    } catch {
      // File not found or invalid JSON — try next path
    }
  }

  return null;
}

Normalization Pipeline

Every resolved identity passes through normalization to enforce size limits and validate avatar values.

function normalizeIdentity(
  raw: Partial<AssistantIdentity> & { agentId: string }
): AssistantIdentity {
  return {
    agentId: raw.agentId,
    name: (raw.name ?? DEFAULT_ASSISTANT_IDENTITY.name)
      .slice(0, MAX_ASSISTANT_NAME),
    avatar: normalizeAvatarValue(
      raw.avatar ?? DEFAULT_ASSISTANT_IDENTITY.avatar
    ),
    emoji: raw.emoji
      ? normalizeEmojiValue(raw.emoji)
      : undefined,
  };
}

Avatar Normalization

The normalizeAvatarValue() helper validates and constrains avatar values. URLs are checked for validity, and all values are truncated to the size limit.

function isAvatarUrl(value: string): boolean {
  try {
    const url = new URL(value);
    return url.protocol === 'https:' || url.protocol === 'http:';
  } catch {
    return false;
  }
}

function normalizeAvatarValue(value: string): string {
  const trimmed = value.trim();
  if (trimmed.length === 0) {
    return DEFAULT_ASSISTANT_IDENTITY.avatar;
  }

  if (isAvatarUrl(trimmed)) {
    // URL avatars: truncate to limit, warn if truncated
    if (trimmed.length > MAX_ASSISTANT_AVATAR) {
      console.warn(`Avatar URL truncated from ${trimmed.length} to ${MAX_ASSISTANT_AVATAR} chars`);
    }
    return trimmed.slice(0, MAX_ASSISTANT_AVATAR);
  }

  // Non-URL avatars: take first character (letter or emoji)
  return trimmed.charAt(0).toUpperCase();
}

Emoji Normalization

The normalizeEmojiValue() helper ensures emoji values are within size limits and contain valid Unicode characters.

function normalizeEmojiValue(value: string): string | undefined {
  const trimmed = value.trim();
  if (trimmed.length === 0) return undefined;

  // Truncate to MAX_ASSISTANT_EMOJI to prevent oversized values
  const normalized = trimmed.slice(0, MAX_ASSISTANT_EMOJI);

  // Verify at least one emoji-like character exists
  const emojiRegex = /\p{Emoji}/u;
  if (!emojiRegex.test(normalized)) {
    console.warn(`Invalid emoji value: "${normalized}"`);
    return undefined;
  }

  return normalized;
}

Identity Class Diagram

The following diagram shows the relationships between the identity types, the resolver, and the normalization helpers.

classDiagram
    class AssistantIdentity {
        +string agentId
        +string name
        +string avatar
        +string emoji
    }

    class DEFAULT_ASSISTANT_IDENTITY {
        +agentId = "main"
        +name = "Assistant"
        +avatar = "A"
    }

    class ResolveAssistantIdentityParams {
        +GatewayConfig cfg
        +string agentId
        +string workspaceDir
    }

    class GatewayConfig {
        +Record assistants
    }

    class SizeLimits {
        +MAX_ASSISTANT_NAME = 50
        +MAX_ASSISTANT_AVATAR = 200
        +MAX_ASSISTANT_EMOJI = 16
    }

    class Helpers {
        +isAvatarUrl(value) bool
        +normalizeAvatarValue(value) string
        +normalizeEmojiValue(value) string
        +normalizeIdentity(raw) AssistantIdentity
        +readWorkspaceIdentity(dir, id) Partial
    }

    ResolveAssistantIdentityParams --> GatewayConfig : uses
    ResolveAssistantIdentityParams --> AssistantIdentity : produces
    DEFAULT_ASSISTANT_IDENTITY --> AssistantIdentity : implements
    Helpers --> SizeLimits : enforces
    Helpers --> AssistantIdentity : returns
    Helpers --> DEFAULT_ASSISTANT_IDENTITY : fallback

Edge Cases & Error Handling

ScenarioBehaviorResult
Workspace config is malformed JSONSilently skipped, falls through to next sourceUses global config or defaults
Name exceeds 50 charactersTruncated to 50 charactersWarning logged
Avatar URL exceeds 200 charactersTruncated to 200 charactersWarning logged, may break URL
Empty name stringFalls back to "Assistant"Default name used
Empty avatar stringFalls back to "A"Default avatar used
Invalid emoji (no Unicode emoji chars)Emoji field set to undefinedNo emoji shown
Non-HTTPS avatar URLHTTP URLs are allowed but not recommendedWorks in development; blocked by CSP in prod
Unknown agentIdNo config entry foundUses DEFAULT_ASSISTANT_IDENTITY with provided agentId
Workspace dir does not existreadFileSync throws, caught silentlyFalls through to global config
✅ Graceful degradation: The resolution pipeline is designed to never throw. Every error condition is caught and falls through to the next source in the priority chain. The DEFAULT_ASSISTANT_IDENTITY is always the last resort, guaranteeing a valid return value.

Workspace Configuration

Projects can customize assistant identity by placing JSON files in their workspace directory. The resolver checks these locations in order:

PriorityPathScope
1 (highest).assistant/{agentId}.jsonAgent-specific override
2.assistant/identity.jsonDefault identity for all agents
3.config/assistant.jsonLegacy configuration path

Example workspace config

// .assistant/main.json
{
  "name": "ProjectBot",
  "avatar": "https://example.com/bot-avatar.png",
  "emoji": "🚀"
}

// .assistant/code-review.json
{
  "name": "CodeReviewer",
  "avatar": "R",
  "emoji": "🔍"
}
📄 File format: Workspace identity files must be valid JSON. JSONC (JSON with comments) and YAML are not supported to keep the parser simple and dependency-free.

Integration Points

The assistant identity resolver is called from several points in the Gateway.

CallerWhenPurpose
message.send handlerBefore streaming responseAttach identity to assistant messages
session.create handlerOn new sessionSet initial assistant display info
assistant.resolve handlerOn explicit resolve requestLet clients preview identity before use
Session snapshot derivationOn snapshot generationInclude identity in lifecycle snapshots

Directory Map

Source files for this topic
src/gateway/
├── identity/
│   ├── index.ts               ← AssistantIdentity, DEFAULT_ASSISTANT_IDENTITY
│   ├── resolve.ts             ← resolveAssistantIdentity(), ResolveAssistantIdentityParams
│   ├── normalize.ts           ← normalizeAvatarValue(), normalizeEmojiValue()
│   ├── constants.ts           ← MAX_ASSISTANT_NAME, MAX_ASSISTANT_AVATAR, MAX_ASSISTANT_EMOJI
│   └── workspace.ts           ← readWorkspaceIdentity()
├── config/
│   └── types.ts               ← GatewayConfig (includes assistants map)
└── handlers/
    └── assistant.ts           ← assistant.resolve handler