@smarthome.exchange/sdk
⚡ TypeScript automation authoring SDK for smarthome.exchange.
The SDK is the tiny, testable layer automation authors import. It lets automation files register typed triggers with on, ask device proxies to create tool plans, ask agent proxies for decisions, and run the same automation logic with or without a live hub executor attached.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Install
pnpm add @smarthome.exchange/sdk
30-Second Automation
import { agents, devices, on } from '@smarthome.exchange/sdk';
on(
{
id: 'manual:evening-check',
kind: 'manual',
expression: 'evening-check',
},
async () => {
await devices.read('climate.office');
await agents.decide({
agentId: 'comfort',
goal: 'Prepare the office for an evening focus session.',
scopes: ['climate.read', 'climate.write', 'light.read', 'light.write'],
confidence: 0.72,
});
}
);
Custom Runtime Context
Use ShxAutomationContext when you want isolated tests, a custom caller id, or a hub-backed executor:
import { ShxAutomationContext } from '@smarthome.exchange/sdk';
const context = new ShxAutomationContext({
callerId: 'automation:morning-routine',
});
context.on({ id: 'manual:morning', kind: 'manual', expression: 'morning' }, async () => {
await context.devices.read('energy.battery');
});
await context.runTrigger({
triggerId: 'manual:morning',
kind: 'manual',
payload: {},
});
If no executePlan function is provided, plans are returned with suggested results containing only callId and status. The SDK does not fabricate execution output when no execution occurred. That makes automation files safe to test without touching real devices.
Public API
| Export | Purpose |
|---|---|
ShxAutomationContext |
Owns automation registrations, broad and exact trigger execution, caller id, device proxy, agent proxy, and optional plan executor. |
on |
Convenience registration function bound to the default automation context. |
subscribe |
Registers on the default context and returns a handle for removing that registration. |
devices |
Default DeviceProxy for building device tool plans. |
agents |
Default AgentProxy for building agent decision plans. |
DeviceProxy |
Exposes read(deviceId) and call(...) for device-oriented tool plans. |
AgentProxy |
Exposes decide(...) for agent decision tool plans. |
Trigger Matching
runTrigger(event) preserves the original broad dispatch behavior: it invokes registrations whose trigger id matches or whose trigger kind matches the event kind, in registration order. Use it when existing automations depend on same-kind dispatch.
runExactTrigger(event) invokes only registrations whose trigger id exactly matches event.triggerId, regardless of trigger kind. Duplicate registrations for that exact id are all invoked in registration order. An event without a matching id invokes no handlers.
Both dispatch methods await each handler before starting the next registration. A handler error rejects dispatch and prevents later matching handlers from running.
Registration Lifecycle
on(trigger, handler) preserves its return value: the supplied trigger. The context stores a detached copy, and getRegistrations() returns detached registration and trigger objects. Changing either the original trigger or an inspection snapshot does not change dispatch matching.
Use subscribe(trigger, handler) when the registration needs to be removed:
const subscription = context.subscribe(
{ id: 'manual:refresh', kind: 'manual', expression: 'refresh' },
async (_event, runtime) => {
await runtime.devices.read('energy.battery');
}
);
subscription.unsubscribe(); // true when this registration was removed
subscription.unsubscribe(); // false when already removed
await context.close();
Each handle removes only its own registration, including when several handlers share a trigger id. A dispatch selects its handlers before starting them. Subscription changes affect future dispatches; handlers already selected retain their order.
close() immediately rejects new registrations and trigger dispatches, waits for every admitted dispatch to finish, and clears the registrations. Concurrent calls return the same promise. Existing handlers can finish their tool plans, and handler failures remain failures of their original dispatch. Call and await close() from the runtime owner outside the handlers it is draining. Closing does not cancel or retry physical effects, and it does not close the hub executor.
Each context keeps its configured caller id. A hub should bind trusted identity, home access, and authorization to its executor closure; the caller id in a plan is attribution and grants no authority.
Interfaces 2.0 Runtime Boundary
The SDK consumes @smarthome.exchange/interfaces 2.0.0. Its plan-only executePlan callback uses the unchanged IToolPlan and IToolPlanResult contracts. Supply that callback from an authenticated runtime bound to the intended home. When that runtime creates an IReq_ExecuteToolPlan request, it must include both context: { homeId } and plan; the SDK does not create the transport envelope or choose a home implicitly.
An agent decision's scopes describe requested restrictions, and agent definitions describe configured restrictions. Neither grants access. The hub intersects those restrictions with verified home grants and enforces tool authorization. Approval actions must bind the verified requester's subjectId and subjectKind together; the runtime includes both in the immutable action digest and authorizes dispatch using the stored identity kind. Existing actions without a kind require explicit handling before dispatch; a kind cannot be inferred from callerId, an approver, or a later request.
Approval decisions belong to the runtime API and require the exact reviewed expectedActionDigest; the SDK does not generate, approve, or replay those decisions. An indeterminate tool result means the runtime must reconcile authoritative device state before retrying. The SDK returns the executor's result unchanged.
Device Calls
DeviceProxy.call(...) retains deviceId as part of its public options, but tool-call input is a detached object built only from the supplied input. The selected toolId identifies the owner-bound device operation. Consequently, devices.read(deviceId) emits exactly {} as its tool input; it never serializes deviceId into the input payload.
Invocation Execution
Use runInvocation when an event has a stable owner-assigned identity. It selects the exact registered trigger, captures an immutable JSON snapshot, and gives its handlers a separate context with invocation, callerId and abortSignal. Supply a stable operationKey for every invocation-scoped device call, agent decision or direct plan; repeated event and operation identities produce the same plan and call IDs. Direct plans use their explicit call IDs as keys. Changing an input while reusing those identities is checked by the Hub's immutable action digest. The SDK neither stores a durable ledger nor retries or replays effects.
const context = new ShxAutomationContext({
callerId: 'automation:office',
executePlan: client.executeToolPlan.bind(client),
});
context.on({ id: 'manual:office', kind: 'manual', expression: 'office' }, async (_event, runtime) => {
const result = await runtime.devices.call({
deviceId: 'light.office',
toolId: 'device:integration:light.office:brightness:write', // use a current listTools() ID
title: 'Set office brightness', reason: 'Requested office scene',
input: { value: 35 }, operationKey: 'office:brightness',
});
// Inspect each result; queuedForApproval and indeterminate need runtime handling.
});
await context.runInvocation({
triggerId: 'manual:office', invocationId: 'external:event:stable-owner-id',
kind: 'manual', payload: {},
}, { abortSignal: lifetime.signal, timeoutMs: 5000 });
await context.close();
The executor receives optional { abortSignal, timeoutMs } compatible with the API client's executeToolPlan. Use current tool IDs from listTools() and an authenticated API client bound to the intended home. SDK caller IDs and contexts grant no home authority. Executor results, including approval queues and indeterminate outcomes, are returned unchanged.
Cancellation, deadline, handler failure and owner close stop invocation admission terminally. Saved invocation proxies cannot start later work. Public cancellation may settle while foreign work continues; close() retains and joins actual admitted handler and executor promises and rejects an AggregateError for retained invocation failures. getInvocationFailures() returns detached inspection records. Concurrent or reentrant close calls share one promise.
Foreign handlers and executors must cooperate with the signal and await their owned work. Cleanup can remain pending for a promise that ignores cancellation; a deadline does not prove foreign work or a physical effect has ended. Await owner close outside the work it joins. Legacy broad and exact handlers already admitted when close starts can finish their tool plans; terminally closed root proxies reject later calls.
Invocation input accepts finite, acyclic JSON made of plain data objects, dense arrays and primitives, bounded to 256 KiB, depth 32 and 16,384 nodes. Accessors, symbols, class instances, undefined values and nonfinite numbers are rejected before execution. Identity keys are nonblank strings of at most 256 characters without Unicode controls or surrogates. Stable ASCII plan and call IDs use SHA-256 through Web Crypto, which the runtime must provide; browsers require a secure context.
Scripts
pnpm test
pnpm build
pnpm buildDocs
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the LICENSE file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.