Ardian About Directory
Claude Code
Email me

Avagenc Zee

2026 — An AI agent that runs a Tuya smart home from a chat message

01 What Zee is

Zee is the member of Avagenc Chat who runs the smart home. A person writes "open the bedroom curtains", the curtains move, and Zee says so in the chat.

Underneath, Zee is a language model given a written job description and four functions it may ask to run. The model cannot touch a curtain. What it can do is answer with a request to run a function, such as send_commands on this device with these values, instead of answering in words. The program around it runs the function, hands the result back, and lets the model choose the next step, until the model finally answers in words. That loop of reading, acting, and reading the result is what makes Zee an agent and not a chatbot.

The whole agent is one Go function built on Google's Agent Development Kit, ADK for short. It takes the model, a Tuya client, and an optional extra instruction, and returns an agent that has Zee's job description and tools but no server, no users, and no thread of its own.

//go:embed internal/description.txt
var description string

//go:embed internal/instruction.txt
var systemInstruction string

func New(cfg Config) (agent.Agent, error) {
tools, err := adktuya.Tools(cfg.TuyaClient)
if err != nil {
return nil, fmt.Errorf("zee: tuya tools: %w", err)
}

instruction := "[SYSTEM_INSTRUCTION]" + systemInstruction + "\n[/SYSTEM_INSTRUCTION]"
if cfg.AdditionalInstruction != "" {
instruction = "[SYSTEM_INSTRUCTION]" + systemInstruction + "\n\n" + cfg.AdditionalInstruction + "\n[/SYSTEM_INSTRUCTION]"
}

a, err := llmagent.New(llmagent.Config{
Name: "zee",
Model: cfg.Model,
Tools: tools,
Description: description,
Instruction: instruction,
})
if err != nil {
return nil, fmt.Errorf("zee: agent: %w", err)
}
return a, nil
}

02 Curtains count backwards

A Tuya device is never told "open" or "turn off". It is given data points, each one a code and a value. A light has a data point called switch that is true or false, and a curtain has one called percent_control that takes a number from 0 to 100.

That number runs the opposite way from what most people would guess. 0 is fully open and 100 is fully closed, so a model left to guess would close the curtains it was asked to open. Zee's instruction, written in Indonesian like the rest of it, states it outright. Translated, it reads "Lights, data point switch (true = ON, false = OFF). Curtains and blinds, data point percent_control (0 to 100), 0 = fully open, 100 = fully closed."

Anything the instruction does not name, Zee reads from the device itself, since every device lists its data points and their current values.

03 Three rules for acting

The rest of the instruction is three rules about acting, and each one is there because a model would otherwise do something else.

Always verify. "I must not assume a device's state from the conversation history. Physical switches and the Tuya app can change the state at any time. I have to call a tool to make sure." The thread may say Zee closed the curtains an hour ago while someone has pulled them open by hand since, so Zee reads the device before it answers.

Mandatory execution. "Every control or status check MUST trigger a tool call. Skipping the action is forbidden." A model can write "done, the lights are off" without doing anything, and that sentence reads exactly like the true one.

Silent execution. "Do NOT output any text before or while calling a tool, no 'one moment, I'm processing', no 'I'll turn it off first'. Call the needed tool straight away, then give EXACTLY ONE final response after seeing the result." This rule came in on 22 June 2026 to stop turns where Zee answered with two messages instead of one.

04 Four tools

The functions come from agentkit, my library that hands outside services to ADK agents, and its Tuya part gives Zee four of them. get_account checks that the person has linked a Tuya account, list_devices returns every device with its id, kind, data points, and the names given to each button of a multi-button switch, device_status reads one device, and send_commands sets data points on one device.

Each tool acts as the person who is talking. ADK hands every tool the user ID of the run, and the Tuya client looks up which Tuya account that person linked, so Zee only ever reaches the home of the person in the thread. In the version of the client Zee is built on, a device that is not on that account is refused before anything is sent to Tuya.

When a call fails, ADK passes the error to the model as text. The binding rewrites the two failures a person can fix into sentences the model can pass on, and every other error reaches the model unchanged.

func forAgent(err error) error {
switch {
case errors.Is(err, tuya.ErrAccountNotLinked):
return errors.New("the human hasn't linked their Tuya account yet — they need to link it before I can see or control any device")
case errors.Is(err, tuya.ErrDeviceNotOwned):
return errors.New("that device isn't on the human's Tuya account — I can only act on their own devices; double-check the id with list_devices")
default:
return err
}
}

05 In the chat

In Avagenc Chat a person shares one thread with Ava and her specialists. A message that mentions @zee goes straight to Zee. A message with no mention goes to Ava, who either answers herself or hands it on with a message that is only "@zee". Either way Zee reads the last messages of the thread and finds the request in the person's own words.

Ava chooses whom to hand work to from a short description of each specialist, and Zee's states a limit along with the job. "Zee has no prospective memory or any scheduling ability. Future-time tasks must not and never be given to zee, not even only a few seconds in the future." So "close the curtains at ten" stays with Ava, who sets herself a reminder and hands Zee the curtains when ten comes.

None of this is in the Zee module. Avagenc Chat passes it in through the one extra field, AdditionalInstruction, as a layer telling every agent that it sits in a group chat whose messages everyone reads and a layer telling specialists to act on the request already in the thread. Each run then adds a line saying whether the human or Ava woke Zee, and when Ava wakes it for a request Zee has already answered, its reply is empty.

zeeAgent, err := zee.New(zee.Config{Model: agentModel, TuyaClient: tuyaAppClient, AdditionalInstruction: specialist.Instruction()})

06 One agent, any product

Zee began in February 2026 as a web service of its own with one chat endpoint, and by April it had two endpoints on one shared history, one for the person and one for Ava. On 21 June 2026 it became a plain Go module with no server, and the next day its instruction dropped everything that tied it to Ava or to a group chat.

The reason was reuse. An agent should not belong to one product, so a later product, such as an assistant for hotels on the same Tuya devices, can import Zee with its job description and tools instead of building them again. How a product uses Zee is that product's business.

The module carries two small programs that run Zee alone, one as a chat in the terminal and one in ADK's web playground. Both keep the conversation in memory and link every user to one fixed Tuya account, so the same agent that sits in a group chat in Avagenc Chat is a plain one-on-one assistant here, with nothing in the module changed.

zeeAgent, err := zee.New(zee.Config{
Model: model,
TuyaClient: tuyaClient,
})
if err != nil {
log.Fatalf("FATAL: zee agent: %v", err)
}

config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(zeeAgent),
SessionService: session.InMemoryService(),
}

l := universal.NewLauncher(console.NewLauncher())
args := append([]string{"console"}, os.Args[1:]...)
if err = l.Execute(ctx, config, args); err != nil {
log.Fatalf("FATAL: %v\n\n%s", err, l.CommandLineSyntax())
}

07 Linked by hand

Tuya lets a cloud project reach someone's home only once their Tuya app account is linked to that project, and offering that link to the public takes a partnership with Tuya. Without one, the team linked each account by hand, and Avagenc Chat only shows whether a person's account is linked.

Everything after the link works, and Zee is paused along with Avagenc Chat.

Avagenc Zee

Reads the device before it believes the chat, and says one thing only after the device has answered