Docs/Your First NPC

Your First NPC

List your NPCs, connect to one, and stream a reply.

Two calls do the work: MistscaleSDK.list_npcs() to find an NPC, and MistscaleSDK.connect_npc(npc_id) to open a conversation. Everything after that is signals.

List your NPCs

list_npcs() is a coroutine — call it with await. Returns every NPC in your project; create one in the dashboard first if the array comes back empty.

list_npcs.gd
var result = await MistscaleSDK.list_npcs()
if not result["ok"]:
    push_error(result["error"].message)
    return
var npcs = result["npcs"]

Connect and chat

tavern_keeper.gd
var connection: MistscaleNPCConnection

func _ready() -> void:
    connection = MistscaleSDK.connect_npc(npc_id)
    connection.chat_chunk.connect(_on_chunk)      # append streamed tokens as they arrive
    connection.chat_revision.connect(_on_revision) # grounding rewrote the reply — replace, don't append
    connection.chat_message.connect(_on_message)   # the turn is settled
    connection.opened.connect(_on_opened)

func _on_opened() -> void:
    connection.send_chat("Hello there!")

func _on_chunk(chat_id: String, delta: String) -> void:
    render_partial_reply(delta)

func _on_revision(chat_id: String, text: String) -> void:
    render_partial_reply(text)

func _on_message(chat_id: String, text: String, finalized: bool, metadata: Dictionary) -> void:
    render_final_reply(text)

If you don't want to render a live stream, just connect to chat_message and ignore chat_chunk / chat_revision entirely — it always fires once per turn with the final text, whether or not you consumed the intermediate chunks.

The connection surface

send_chat(message, sender_id = "")
Sends a player message. sender_id overrides the connection's default player id for just this message.
send_voice_chunk(data, end = false, sender_id = "")
Streams captured audio. Covered in Voice.
set_spatial_context(location, time_of_day = "", weather = "")
Updates the NPC's sense of place. Covered in Spatial Context in Godot.
get_evolution_status() / get_quota_status()
Requests a one-off evolution_status / quota_status signal with the NPC's current mood / usage snapshot.
close()
Closes the connection and stops auto-reconnect.
state
CONNECTING, OPEN, CLOSING, or CLOSED (MistscaleNPCConnection.State).

Signals

opened / closed(code, reason, expected) / sdk_error(error)
Connection lifecycle. expected is true only for a close() you called yourself.
chat_chunk(chat_id, delta)
One streamed token. Append it.
chat_revision(chat_id, text)
The reply was corrected after generation. Replace your accumulated text with it.
chat_message(chat_id, text, finalized_by_metadata, metadata)
The turn is done. Fires exactly once per turn regardless of whether you used chat_chunk.
chat_blocked(chat_type, reason, limit, used)
The NPC hit its usage quota and did not reply.
reconnecting(attempt, delay_ms)
The addon is about to retry an unexpected disconnect. Automatic; you don't need to call anything.
Who does the NPC think it's talking to?
Every connection has a player id — auto-generated if you don't set one in configure(), in which case two running game instances get two separate relationships with the same NPC. For a real game, pass your own stable player id via MistscaleSDK.connect_npc(npc_id, player_id) so returning players are recognized across sessions and devices.