This file provides project-level context for AI coding assistants and new contributors: business logic, technical architecture, directory layout, and coding guidelines.
TClaw is a hardware-oriented AI agent running on edge devices. Its core value is enabling users to interact with IoT devices through natural language via IM channels (Telegram / Discord / Feishu). The on-device agent receives instructions, invokes local MCP tools to perform real actions, and replies through the same channel.
Typical user scenarios:
| Scenario | Flow |
|---|---|
| Set a reminder | User sends "remind me in 5 minutes" on Telegram β Agent calls get_current_time + cron_add β cron fires and pushes a message when due |
| Manage files | User sends "read /memory/MEMORY.md" β Agent calls read_file β returns file content |
| Remote execution | User sends "run ls -la" (Raspberry Pi) β Agent calls tool_exec β returns command output |
| Memory & diary | Agent autonomously writes important info to MEMORY.md or daily notes, preserving context across sessions |
| OpenClaw control | User sends claw-machine command β Agent sends control via ACP/WebSocket gateway |
Key business characteristics:
- Unified multi-channel: Telegram, Discord, Feishu, WebSocket, serial CLI all share a single agent loop
- Tool loop: A single user message can trigger up to 10 LLM β tool iterations until the agent produces a final reply
- Persistent memory: Long-term memory (MEMORY.md), daily notes (YYYY-MM-DD.md), personality (SOUL.md), user profile (USER.md) persisted on flash/SD
- Cross-platform deployment: Same codebase runs on Tuya T5AI, ESP32-S3, Raspberry Pi, and Linux desktop via conditional compilation
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β IM Channel Layer β
β Telegram β Discord β Feishu β WebSocket β CLI β
βββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β im_msg_t (inbound / outbound)
βββββββββΌββββββββ
β Message Bus β Thread-safe dual queue
βββββββββ¬ββββββββ
β
βββββββββΌββββββββ
β Agent Loop β Outer: wait for msg
β β Inner: β€10 tool iterations
βββββ¬ββββββββ¬ββββ
β β
βββββββββββΌββ ββββΌβββββββββββ
β Context β β Cloud AI β
β Builder β β (ai_agent) β
β sys promptβ β stream cb β
βββββββ¬ββββββ ββββββββ¬βββββββ
β β
βββββββββββΌβββββββββββββββΌβββββββββββ
β MCP Tools Layer β
β files β cron β exec β openclaw β
βββββββββββ¬βββββββββββββββ¬βββββββββββ
β β
βββββββββββΌββββββ ββββββΌββββββββββ
β Memory / β β Gateway β
β Session β β WS + ACP β
βββββββββββββββββ ββββββββββββββββ
- Inbound: IM channel (or WS/cron/ACP) builds an
im_msg_tβmessage_bus_push_inbound() - Agent consumes:
agent_loop_taskblocks onmessage_bus_pop_inbound() - Inner loop:
context_build_system_prompt()assembles system prompt + history βai_agent_send_text()sends to cloud - AI callback:
tclaw_chat.c's__ai_chat_handle_eventreceives stream events:STREAM_START/DATA/STOP: accumulate text β record in historyEND: callsagent_loop_set_last_response()+agent_loop_notify_turn_done()(posts semaphore)
- Tool execution: Cloud AI triggers MCP tool call β
__on_tool_executedhook records result β setss_turn.tool_called = true - Loop decision: After semaphore post, agent checks
tool_calledβ if true, feeds tool result as next input; otherwise forwards final reply to outbound queue - Outbound:
outbound_dispatch_taskdequeues message β dispatches to the corresponding IM SDK bychannelfield
| Thread | Responsibility | Entry |
|---|---|---|
tuya_app_main / main |
SDK init + tuya_iot_yield main loop |
user_main() |
agent_loop |
Outer loop + inner tool iteration | agent_loop_task() |
outbound_loop |
Outbound message dispatch | outbound_dispatch_task() |
ws_server |
WebSocket server | ws_server.c |
acp_client |
OpenClaw gateway WS client | acp_client.c |
cron_service |
Scheduled task scheduler | cron_service.c |
| IM channel threads | Telegram polling / Discord gateway / Feishu WS | *_bot.c |
- Agent loop β AI callback: Binary semaphore
s_turn.sem(agent_loop_taskwaits,tclaw_chatposts) - Shared history:
s_history_mutexprotectss_history_json(cJSON array, sliding window β€ 10 entries) - Tool state:
s_turn.lockprotectstool_called/tool_result - Message bus:
tal_queueis inherently thread-safe
TClaw/ (git repo: tuya/TClaw, a.k.a. Ducky)
βββ agent/ # Core agent loop and context builder
β βββ agent_loop.c/h # Outer + inner tool-iteration loop (semaphore sync)
β βββ context_builder.c/h # System prompt assembly (rules, memory, skills, personality)
β
βββ IM/ # Unified instant-messaging abstraction layer
β βββ bus/message_bus.c/h # Thread-safe inbound/outbound dual queue
β βββ channels/ # Telegram / Discord / Feishu bot implementations
β βββ cli/serial_cli.c/h # Local serial/CLI input channel
β βββ proxy/http_proxy.c/h# TLS HTTP client (proxy mode)
β βββ certs/ # TLS CA certificate bundles
β βββ im_api.h # Unified IM interface declarations
β βββ im_config.h # IM constants and config
β βββ im_utils.c/h # IM utility functions
β
βββ tools/ # MCP tool registration and implementations
β βββ tools_register.c/h # Unified tool registration entry point
β βββ tool_files.c/h # File operations (read/write/edit/list/find)
β βββ tool_cron.c/h # Scheduled tasks (cron_add/list/remove)
β βββ tool_exec.c/h # Remote command execution (Linux only)
β βββ tool_openclaw_ctrl.c/h # OpenClaw gateway control
β
βββ memory/ # Persistent memory and session management
β βββ memory_manager.c/h # MEMORY.md, daily notes, SOUL.md, USER.md
β βββ session_manager.c/h # Session JSONL persistence
β
βββ gateway/ # Gateway connections
β βββ ws_server.c/h # Device-side WebSocket server
β βββ acp_client.c/h # OpenClaw ACP WebSocket client
β
βββ cron_service/ # Background scheduled-task service
β βββ cron_service.c/h # In-memory job table + cron.json persistence + scheduler thread
β
βββ heartbeat/ # Heartbeat service
β βββ heartbeat.c/h # Periodically reads HEARTBEAT.md to drive AI
β
βββ skills/ # Skill loader
β βββ skill_loader.c/h # Scans/installs built-in skill .md files
β
βββ src/ # Application glue layer
β βββ tuya_app_main.c # Entry point user_main(), initialization orchestration
β βββ tclaw_chat.c # AI stream event handling, semaphore bridge to agent_loop
β βββ app_im.c # IM init, outbound dispatch, channel switching
β βββ cli_cmd.c # Extended CLI commands
β βββ reset_netcfg.c # Network config reset logic
β
βββ include/ # Global headers
β βββ tuya_app_config.h # Product ID, channel tokens, gateway config defaults
β βββ tuya_app_config_secrets.h(.example) # Sensitive credentials (gitignored)
β βββ app_im.h # IM application interface
β βββ tclaw_chat.h # Chat module interface
β
βββ ai_components/ # TuyaOpen AI component adapters (sub-CMake)
βββ config/ # Board-level Kconfig snapshots
β βββ RaspberryPi.config
β βββ TUYA_T5AI_BOARD_LCD_3.5_CAMERA.config
β βββ ESP32S3_BREAD_COMPACT_WIFI.config
β
βββ CMakeLists.txt # Application-level CMake build script
βββ Kconfig # Aggregates sub-module Kconfigs
βββ CLAUDE.md # AI coding assistant guide (Claude Code)
βββ AGENTS.md # This file
βββ TuyaOpen/ # SDK submodule (git submodule)
user_main()
βββ cJSON_InitHooks() // PSRAM or standard heap
βββ tal_log_init / tal_kv_init / tal_cli_init ...
βββ tuya_iot_init() // Tuya IoT client
βββ netmgr_init() // Network manager
βββ board_register_hardware()
βββ tclaw_chat_init() // Register AI stream event callback
βββ app_im_init() // Subscribe to MQTT connected event (deferred IM init)
βββ ws_server_start() // Start WebSocket server
βββ tool_registry_init() // Subscribe to MQTT connected β one-shot MCP tool chain init
βββ acp_client_init() // Start ACP WebSocket client
βββ agent_loop_init() // Create semaphore/mutex β start agent_loop thread
βββ tuya_iot_start() β for(;;) tuya_iot_yield()
After MQTT connects:
app_im_init_evt_cbβ init message_bus β start IM bot β start outbound dispatcher thread__ai_mcp_initβ init filesystem, cron, heartbeat, memory, session, skills β register all MCP tools
All threading, synchronization, timers, and memory allocation must go through tal_* APIs (tal_thread_*, tal_mutex_*, tal_semaphore_*, tal_queue_*, tal_malloc/tal_free). Never use pthread_*, malloc/free, sem_*, etc. When external PSRAM is enabled (ENABLE_EXT_RAM), use the claw_malloc/claw_free wrappers that automatically switch between heap and PSRAM.
// Correct
claw_malloc(size);
tal_mutex_create_init(&lock);
tal_thread_create_and_start(&handle, NULL, NULL, task_fn, NULL, &cfg);
// Wrong
malloc(size);
pthread_mutex_init(&lock, NULL);
pthread_create(&tid, NULL, task_fn, NULL);Use the unified guard pattern #if defined(X) && (X == 1) for all platform-specific code (WiFi, display, camera, audio, PSRAM, Linux-only execution). When adding a new capability, follow the same pattern and declare a corresponding config entry in Kconfig.
#if defined(ENABLE_WIFI) && (ENABLE_WIFI == 1)
// WiFi-specific code
#endif
#if defined(PLATFORM_LINUX) && (PLATFORM_LINUX == 1)
// Linux-only code (e.g. tool_exec)
#endifAny new inbound message source (new IM channel, sensor event, etc.) must build an im_msg_t and call message_bus_push_inbound(). The agent_loop_task is the sole consumer. No channel may call the AI interface or bypass the bus directly. Outbound messages likewise go through message_bus_push_outbound() β outbound_dispatch_task.
im_msg_t msg = {0};
strncpy(msg.channel, "my_channel", sizeof(msg.channel) - 1);
msg.content = claw_malloc(len + 1);
// fill content ...
message_bus_push_inbound(&msg);All non-callback functions must return OPERATE_RET (OPRT_OK = 0 on success). Use TUYA_CALL_ERR_RETURN for critical steps (fail β return immediately) and TUYA_CALL_ERR_LOG for non-critical steps (log and continue). Always check for NULL after memory allocation.
OPERATE_RET my_module_init(void)
{
OPERATE_RET rt = OPRT_OK;
char *buf = claw_malloc(BUF_SIZE);
if (!buf) {
return OPRT_MALLOC_FAILED;
}
TUYA_CALL_ERR_RETURN(tal_mutex_create_init(&s_lock)); // fail β return
TUYA_CALL_ERR_LOG(optional_feature_init()); // fail β log only
return OPRT_OK;
}This project follows the TuyaOpen C conventions. Below is a self-contained reference for contributors unfamiliar with the style.
.h files must follow this order:
| # | Section | Notes |
|---|---|---|
| 1 | Include guard | #ifndef __MODULE_H__ / #define / #endif |
| 2 | C++ guard | #ifdef __cplusplus / extern "C" / #endif |
| 3 | Includes | Only what the header needs |
| 4 | Macros | #define constants |
| 5 | Type definitions | typedef, struct, enum |
| 6 | Function declarations | Public API |
.c files must follow this order:
| # | Section | Notes |
|---|---|---|
| 1 | Includes | Project β TuyaOpen SDK (tal_*.h) β System (<string.h>) |
| 2 | Macros | #define constants |
| 3 | Type definitions | typedef, struct, enum |
| 4 | File-scope variables | Including STATIC variables |
| 5 | Forward declarations | If any |
| 6 | Function implementations | No separator between functions |
Section separators β use only between different section types (e.g. includes β macros), never between items of the same type (e.g. function β function):
/* ---------------------------------------------------------------------------
* Section name
* --------------------------------------------------------------------------- */Every .h and .c must start with a Doxygen file header in English:
/**
* @file module_name.c
* @brief Brief description of the module
* @version 0.1
* @date 2025-03-25
* @copyright Copyright (c) Tuya Inc. All Rights Reserved.
*/Every function (both declaration in .h and implementation in .c) must have a Doxygen comment. Required tags: @brief, @param (per parameter, use [in]/[out]; omit if no params), @return (use @return none for void). Add @note for threading constraints or special usage.
/**
* @brief Create and start the cron service
* @param[in] interval_ms scheduler tick interval in milliseconds
* @return OPRT_OK on success, error code on failure
* @note Must be called after tool_files_fs_init(). Not reentrant.
*/
OPERATE_RET cron_service_start(uint32_t interval_ms);| Element | Convention | Example |
|---|---|---|
| File-scope variable | s_ prefix |
s_ctx, s_history_json |
| Global variable | g_ prefix |
g_device_state |
| Internal/static function | __ prefix |
__on_tool_executed, __build_and_send |
| Struct typedef | _T suffix |
MODULE_CTX_T |
| Enum typedef | _E suffix |
IM_CHANNEL_E |
| Macro / constant | UPPER_SNAKE_CASE |
TOOL_LOOP_MAX, STREAM_DATA_MAX_LEN |
| Public function | module_action |
agent_loop_init, message_bus_push_inbound |
Use standard C <stdint.h> / <stdbool.h> types. The TuyaOpen SDK defines legacy uppercase aliases (UINT32_T, VOID_T, etc.) β you will see them in older SDK headers and some existing code, but new application code should prefer the standard names:
| Use | Avoid in new code |
|---|---|
uint32_t, uint8_t, int32_t |
UINT32_T, UINT8_T, INT32_T |
void |
VOID_T |
bool (true/false) |
BOOL_T (TRUE/FALSE) |
static |
STATIC |
const |
CONST |
size_t |
(no alias) |
The following project-specific types remain in use and should not be replaced:
OPERATE_RETβ function return type for error codes (OPRT_OK,OPRT_*)THREAD_HANDLE,MUTEX_HANDLE,SEM_HANDLE,QUEUE_HANDLEβ TuyaOpen OS abstraction handlesTHREAD_CFG_T,TIMER_IDβ TuyaOpen OS config structs
Note: Do not refactor existing files solely to convert legacy aliases. When modifying a function, you may update its signature to standard types if the change is localized.
- Always use braces, even for single-statement
if/else/while/forblocks. - Indent with 4 spaces (no tabs).
- Space after keywords:
if (,while (,for (.
// Wrong
if (ptr == NULL)
return OPRT_INVALID_PARM;
// Correct
if (ptr == NULL) {
return OPRT_INVALID_PARM;
}/**
* @file my_module.c
* @brief Example module following TuyaOpen C style
* @version 0.1
* @date 2025-03-25
* @copyright Copyright (c) Tuya Inc. All Rights Reserved.
*/
#include "my_module.h"
#include "tal_api.h"
#include <string.h>
/* ---------------------------------------------------------------------------
* Macros
* --------------------------------------------------------------------------- */
#define MY_BUF_SIZE 1024
/* ---------------------------------------------------------------------------
* File-scope variables
* --------------------------------------------------------------------------- */
static MUTEX_HANDLE s_lock = NULL;
static char *s_buf = NULL;
/* ---------------------------------------------------------------------------
* Function implementations
* --------------------------------------------------------------------------- */
/**
* @brief Internal helper to reset buffer
* @return none
*/
static void __reset_buf(void)
{
if (s_buf) {
memset(s_buf, 0, MY_BUF_SIZE);
}
}
/**
* @brief Initialize my module
* @return OPRT_OK on success, error code on failure
*/
OPERATE_RET my_module_init(void)
{
if (s_lock) {
return OPRT_OK;
}
s_buf = claw_malloc(MY_BUF_SIZE);
if (!s_buf) {
return OPRT_MALLOC_FAILED;
}
TUYA_CALL_ERR_RETURN(tal_mutex_create_init(&s_lock));
__reset_buf();
return OPRT_OK;
}