@lua-ai-global/device-client connects a Node process to an agent as a device over Socket.IO or MQTT, runs command handlers, sends triggers, and uploads files. Version 1.1.0; Node 18 or later.
Verified against @lua-ai-global/device-client 1.1.0.
Quick example
device.ts
trigger() needs the triggers operation on the credential: without it the platform answers with a trigger_error event, OPERATION_FORBIDDEN, and the call rejects after the 10-second acknowledgment wait.
DeviceClient
new DeviceClient(config) creates a client; it doesn’t connect until connect() is called. The constructor throws deviceCredential or apiKey is required when neither credential field is set and deviceCredential and apiKey disagree when both are set to different values.
string
required
The agent the device connects to. Must equal the agent in the credential’s binding.
string
required
The device name. Must equal the name in the credential’s binding; hyphens become underscores in tool names.
string
The device credential. Required unless
apiKey is set.string
deprecated
A legacy API key. Still accepted; use
deviceCredential for new devices.DeviceCommandDefinition[]
default:"[]"
Commands sent at connect time; each becomes a tool. Omit when the device is declared with
defineDevice.'socketio' | 'mqtt'
default:"socketio"
mqtt routes everything through MqttTransport.string
default:"https://api.heylua.ai"
Socket.IO server. The client connects to the
/devices namespace over WebSocket only.string
default:"wss://mqtt.heylua.ai/mqtt"
Broker URL for the MQTT transport.
string
default:"https://cdn.heylua.ai"
Base URL for
cdn.string
Group name reported at connect; enables group fan-out for devices declared with
defineDevice.string
default:"device-node"
Client family reported in the Socket.IO handshake for diagnostics. Never used for authorization.
string
default:"1.1.0"
Your application’s version, reported for diagnostics alongside the package’s
device-node family.DeviceCommandDefinition
string
required
Command name; must match
^[a-z][a-z0-9_]{0,63}$ or the platform drops it.string
required
The tool description the model reads. Cut at 500 characters.
Record<string, any>
JSON Schema for the payload; at most 4 KB when serialized.
number
default:30000
How long the platform waits for the response.
{ maxAttempts: number; backoffMs: number }
Platform-side retries after a timeout or transport failure, waiting
backoffMs × attempt between attempts. Both fields are typed as required; on the wire a missing maxAttempts counts as 1 attempt and a missing backoffMs as 1,000 ms.Methods
connect()
Opens the connection and resolves once the platform has accepted the credential.connected event.
Errors — rejects with an error whose code is AUTH_FAILED or MISSING_AUTH when the platform refuses the credential; reconnection is disabled for the client’s lifetime. Over MQTT it rejects with the broker’s Not authorized error. Other failures are retried, not thrown.
onCommand()
Registers the handler for one command name. Registering the same name again replaces the handler.string
required
Command name, as declared in
commands or in defineDevice.CommandHandler
required
Receives the payload; its resolved value is sent as
data.{ commandId, success: true, data }; a thrown error is sent as { commandId, success: false, error: message }; an unregistered command answers Unknown command: <name>.
trigger()
Sends a trigger and waits for the platform’s acknowledgment. Triggers are sent one at a time in call order.string
required
Trigger name, matched exactly against the handlers on the agent.
any
required
JSON payload; at most 1 MB serialized.
{ triggerId, received }; received is true when the event was queued.
Errors — Not connected to device gateway (or Not connected to MQTT broker) before connect() resolves; Trigger '<name>' ACK timeout (10s) when no acknowledgment arrives within 10 seconds. Over Socket.IO a trigger the platform rejects arrives as a trigger_error event, not as a rejection; the MQTT transport doesn’t subscribe to trigger_error, so a rejected trigger times out instead.
onTriggerResult()
Registers a handler for atrigger_result message. The platform doesn’t send that message, so the handler never runs.
disconnect()
Closes the connection and stops reconnection. Over MQTT it first publishes a retainedoffline status.
isConnected()
true between connected and the next disconnected.
Events
DeviceClient extends EventEmitter.
Connection behavior
- Heartbeat every 30 seconds while connected.
- Socket.IO reconnects with a jittered delay from 1 to 30 seconds, without limit; after a disconnect the platform initiated, for example
AGENT_FORBIDDENorDEVICE_DISABLED, it retries every second, without limit.AUTH_FAILEDandMISSING_AUTHstop reconnection for good.RATE_LIMITEDpauses for the platform’sretryAfterMs, clamped between 1 second and 15 minutes, or 30 seconds when none is given. - MQTT reconnects every 5 seconds and uses a persistent session, so commands published while the client was away are delivered on reconnect.
- While a self-describing device is disconnected its command tools are removed from the agent on the next turn and return with the next handshake. The platform keeps the connect-time command list for 24 hours only, so a device connected longer than that loses its command tools until you call
disconnect()and thenconnect(); the client has no timer for this. - Command IDs are cached for 5 minutes (1,000 entries); a redelivered command gets the cached response without running the handler.
SIGTERMandSIGINTcalldisconnect()and log[lua-device] Shutting down device '<name>'...; the process exits when nothing else keeps it alive.
MqttTransport
The MQTT implementationDeviceClient uses when transport is 'mqtt'; usable on its own with the same DeviceClientConfig.
lua-<agentId>-<deviceName>, username <agentId>:<deviceName>, the credential as password, keep-alive 60 seconds, a persistent session, and a retained last-will offline status. Methods connect(), onCommand(), trigger(), onTriggerResult(), disconnect(), and isConnected() behave as on DeviceClient; it emits the same events except trigger_error, whose topic it doesn’t subscribe to. Topics are listed in the MQTT protocol.
CDN
device.cdn is a CDN instance authenticated with the device’s credential, which must grant assets.upload.
upload()
Buffer | Blob
required
File contents; at most 100 MB.
string
required
Name sent with the multipart upload.
string
default:"application/octet-stream"
MIME type for a
Buffer; a Blob keeps its own type.{ fileId, mediaType, extension, url }; url is <cdnUrl>/<fileId>.
Errors — Error with the platform’s message, or CDN upload failed: <status>.
getUrl()
download()
CDN download failed: <status>.
lua-device runner
The package installs alua-device binary that connects a device without a script. It sends no command list, so pair it with a defineDevice declaration pushed to the agent.
It logs
Registered command handler: <name> per handler, Connecting to <url>..., then Connected as '<name>' to agent '<id>' and Listening for commands... (Ctrl+C to stop). A refused connection logs Connection failed: <message> and exits 1.
handlers.cjs
Types
All exported from the package.interface
The constructor options listed under DeviceClient.
interface
{ name; description; inputSchema?; timeoutMs?; retry? }.interface
{ commandId; command; payload; timeout? } as delivered to the device. timeout is not sent by the platform.interface
{ commandId; success; data?; error? } as sent back.interface
{ triggerId; received; error? }.interface
{ fileId; mediaType; extension; url }.type
(payload: any) => Promise<any>.type
(result: any) => void.See also
- Python device client — the MQTT-only asyncio client
- MQTT protocol — topics and payloads the MQTT transport uses
- Expose device commands as tools
- Reliability and limits
defineDevice— the agent-side declaration the runner needs

