Implement a custom module

Implement `IConvaiModule` to add custom runtime behavior that starts with the SDK, accesses runtime services, and reacts to domain events.

Build a custom module that integrates with the Convai runtime lifecycle, accesses SDK services, and subscribes to domain events. Before starting, read Runtime module system to understand when a module is the right tool and how the lifecycle states map to your implementation.

Prerequisites

  • A working Convai scene with a ConvaiManager component

  • C# proficiency, including async/await and Unity's MonoBehaviour lifecycle

  • Familiarity with the Runtime module system

Quickstart: minimal module

Before reading the full interface contract, here is the shortest path to a working module — a MonoBehaviour that registers itself and subscribes to one SDK event:

// MinimalModule.cs
using System;
using System.Collections.Generic;
using System.Threading;
using Convai.Domain.DomainEvents.Runtime;
using Convai.Runtime.Components;
using Convai.Runtime.Core.Modules;
using UnityEngine;

public class MinimalModule : MonoBehaviour, IConvaiModule
{
    public string ModuleId    => "my-project.minimal";
    public string DisplayName => "Minimal Module";

    public IReadOnlyList<string> RequiredModules  => Array.Empty<string>();
    public IReadOnlyList<Type>   RequiredServices => Array.Empty<Type>();
    public IReadOnlyList<Type>   ProvidedServices => Array.Empty<Type>();
    public bool IsActive { get; private set; }

    private IDisposable _sub;

    private void Awake() => ConvaiManager.ActiveManager?.RegisterModule(this);
    private void OnDestroy() => ConvaiManager.ActiveManager?.UnregisterModule(this);

    public System.Threading.Tasks.ValueTask RegisterAsync(IModuleContext ctx, CancellationToken ct = default)
        => System.Threading.Tasks.ValueTask.CompletedTask;

    public System.Threading.Tasks.ValueTask StartAsync(IModuleContext ctx, CancellationToken ct = default)
    {
        _sub = ctx.Events.Subscribe<CharacterSpeechStateChanged>(e =>
        {
            if (e.IsSpeaking) Debug.Log($"[MinimalModule] Character {e.CharacterId} started speaking.");
        });
        IsActive = true;
        return System.Threading.Tasks.ValueTask.CompletedTask;
    }

    public System.Threading.Tasks.ValueTask PauseAsync(RuntimePauseReason r, CancellationToken ct = default)
    { IsActive = false; return System.Threading.Tasks.ValueTask.CompletedTask; }

    public System.Threading.Tasks.ValueTask ResumeAsync(CancellationToken ct = default)
    { IsActive = true; return System.Threading.Tasks.ValueTask.CompletedTask; }

    public System.Threading.Tasks.ValueTask StopAsync(CancellationToken ct = default)
    { _sub?.Dispose(); IsActive = false; return System.Threading.Tasks.ValueTask.CompletedTask; }
}

Add this component to any GameObject in the scene. The full interface contract and advanced patterns follow below.

IConvaiModule interface

Lifecycle method reference

Method
When called
What to do

RegisterAsync

During runtime build — before any StartAsync

Register services via context.ProvideModuleService<T>(). Subscribe to events.

StartAsync

Runtime start — after all modules are registered

Start active behaviors: begin processing, initialize hardware, start coroutines.

PauseAsync

Runtime paused (app loses focus, deliberate pause)

Stop processing. Use RuntimePauseReason to distinguish why.

ResumeAsync

Runtime resumed

Restart processing paused in PauseAsync.

StopAsync

Runtime stopping or module removed

Clean up: unsubscribe, stop coroutines, release resources.

IModuleContext services

Property
Type
Availability
Description

Runtime

ConvaiRuntime

Always

The runtime instance this module belongs to.

Events

IEventHub

Always

Publish and subscribe to domain events.

Agents

IAgentRegistry

Always

Query registered characters and players.

Transport

ITransportProvider

May be null

Platform-specific communication layer.

Preferences

IRuntimePreferences

May be null

Mutable runtime preferences.

Logger

ILogger

May be null

Logger for diagnostics.

RoomAudio

IConvaiRoomAudioService

May be null

Microphone and playback service.

Credentials

ICredentialProvider

May be null

API key and server URL resolution.

For the full list of subscribable domain events, see Event System.

Implement a module

Event subscriber example

A module that subscribes to a domain event and triggers haptic feedback when a character speaks.

Service provider and consumer example

A module declares its provided services in ProvidedServices, registers the instance in RegisterAsync, and consuming modules retrieve it via TryGetModuleService<T>.

A consuming module:

Register a module

Attach the module as a component to any GameObject. It self-registers with ConvaiManager on Awake.

After ConvaiManager.Start() completes, ConvaiManager.ActiveManager.IsInitialized returns true, indicating all registered modules have been discovered and the runtime has started.

CreateRuntimeBuilder override

Use this when you prefer all customization in one place, or when the module is not a MonoBehaviour.

Use the dependency injection pattern

Components on ConvaiCharacter or ConvaiPlayer GameObjects can receive SDK services automatically by implementing IInjectable<TDependencies>. The SDK injects dependencies after the character or player is registered with the runtime.

IInjectable<TDependencies>

IConvaiCharacterDependencies

Property
Type
Availability

EventHub

IEventHub

Required

ConnectionService

IConvaiRoomConnectionService

Required

AudioService

IConvaiRoomAudioService

Required

AgentRegistry

IAgentRegistry

Optional

Logger

ILogger

Optional

IConvaiPlayerDependencies

Property
Type
Availability

PlayerInputService

IPlayerInputService

Optional

RuntimeSettingsService

IConvaiRuntimeSettingsService

Optional

Logger

ILogger

Optional

Write an injectable component

Add this component to the same GameObject as ConvaiCharacter. The SDK calls InjectDependencies automatically during character registration.

Usage examples

Example 1: Biometric correlation module for medical simulation

Records character emotion data alongside biometric sensor readings for post-session analysis.

Example 2: Assessment scoring module for industrial training

Tracks character-triggered actions against a scoring rubric and exposes the score service to other modules via ProvideModuleService.

Troubleshooting

Symptom
Likely cause
Fix

Module's StartAsync never called

RegisterAsync threw an unhandled exception; the runtime halts module startup silently

Wrap RegisterAsync body in a try-catch and log explicitly.

TryGetModuleService<T> returns false unexpectedly

ProvideModuleService<T> was called in StartAsync instead of RegisterAsync

Move ProvideModuleService<T> to RegisterAsync — services must be registered before any module's StartAsync runs.

Module starts but misses early events

Subscribed in RegisterAsync but event fires during startup before StartAsync

Move subscriptions to StartAsync, or guard with IsActive check in the handler.

RequiredModules entry causes startup error

Listed module ID not registered before runtime build

Verify the module ID string matches exactly — IDs are case-sensitive.

InjectDependencies never called on IInjectable component

Component is not on a GameObject in the ConvaiCharacter hierarchy

IInjectable<IConvaiCharacterDependencies> only works on GameObjects that are children of a character.

ConvaiManager.ActiveManager is null in Awake

Manager's Awake has not yet run at execution order −1100

Register modules in Start() or use ConvaiManager.ActiveManager?.RegisterModule(this) with null-safety.

Next steps

Logging, metrics, and retry policyEvent system

Last updated

Was this helpful?