Inspiration

Smart-home commands are easy to send and easy to lose. A routine tells a bulb to change, then moves on. If the bulb misses the packet, the room is wrong and nothing notices.

We wanted the opposite for Kasa lights on our own Wi-Fi. Say the intention once, the way you would say it to Alexa+: "cycle the kitchen color every five seconds." After that, a loop on the LAN should set the bulb, read it back, and try again until the light matches. Alexa+ is the voice. The loop is the product.

What it does

Light Agent turns one plain-English intention into a verified control loop for TP-Link Kasa bulbs.

You can turn a light on or off, set brightness, or set color. You can also schedule that action on an interval. cycleColor steps the hue by $$45^\circ$$ on every tick. Each tick applies the action, reads the bulb with getState, and retries up to 3 times. The wait before retry $$n$$ is

$$t_n = 200 \times 2^{\,n-1} \text{ ms}$$

so 200 ms and then 400 ms. Three misses mark the task error. Later ticks never call the model.

  • npm run setup discovers bulbs on the LAN, asks for a friendly name, and saves them to lights.json.
  • MockConnector runs the same loop with no hardware.
  • KasaConnector drives real KL/LB bulbs through tplink-smarthome-api.

How we built it

TypeScript on Node, run directly with tsx. Four pieces stay separate so each one can be tested on its own:

  1. LightConnector is the contract. MockConnector keeps state in memory. KasaConnector maps a friendly id to a bulb host.
  2. LightAgent is a Claude tool-use loop. It compiles a sentence into power, brightness, color, or schedule_recurring_task, then stops.
  3. TaskScheduler owns the interval, the verify step, and the backoff. It emits a status line after every tick.
  4. Tools in src/agent/tools.ts are the only bridge from the model to the connector and the scheduler.

The mock path was exercised end to end: tool dispatch, hue steps, a successful verify, and the retry-to-error path. npm run typecheck is clean.

Challenges we ran into

  • CommonJS vs. ESM. tplink-smarthome-api is CommonJS with getter-based named exports. import { Client } typechecks and throws at runtime under Node's ESM loader. A default import, then destructuring, fixed it.
  • Discovery on segmented networks. Discovery is a UDP broadcast. On a segmented subnet the default broadcast finds nothing, so setup accepts KASA_BROADCAST and a longer scan.
  • Type narrowing. The library returns Bulb | Plug, so a device has to be narrowed before lighting.setLightState.
  • No hardware test yet. The Kasa connector is written and not yet run on physical bulbs. The mock connector is what made verify and retry testable without a LAN.
  • In-memory loops. Loops live in memory, so the process has to stay open for ticks to fire.
  • Alexa+ onboarding. Device onboarding is partner-gated, and a classic skill is the wrong surface. The eligible path is a self-hosted MCP server on spec 2025-11-25 over Streamable HTTP, with a page that is a real MCP client.

Accomplishments that we're proud of

  • The model is used once, as a compiler. The runtime after that is deterministic: apply, read back, retry, with no token cost per tick.
  • The hardware boundary held. The agent and the scheduler never mention Kasa. A new bulb brand is another LightConnector.
  • The failure path is real, not a log we hope is true. A connector that disagrees with the intended state is retried, then marked error after three attempts.

What we learned

A natural-language light switch is a demo. A closed loop is the product. Verification has to be a read of the bulb, and the schedule has to survive as state the next session can list.

We also learned the Alexa+ contract: judges meet the project through MCP (initialize, tools/list, tools/call), voice, and cards. The partner console being closed does not change the loop underneath.

What's next for Light Agent

  • Put the loop behind a Streamable HTTP MCP server and an Alexa+ page: microphone, spoken confirmation, and cards for color, loop status, and each verify or retry.
  • Persist tasks to tasks.json and restore them on startup.
  • Run the same sentences against real Kasa bulbs on the LAN.
  • Add more connectors behind the same interface.

Built With

Share this project:

Updates

Submission history