tila-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dawid Leszczyński (@davebream)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,318 @@
1
+ # tila-sdk
2
+
3
+ TypeScript SDK for [tila](https://github.com/davebream/tila) -- a state-and-coordination engine for multi-machine agentic work.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install tila-sdk
9
+ ```
10
+
11
+ `zod` is an optional peer dependency. Install it to enable opt-in response validation:
12
+
13
+ ```bash
14
+ npm install zod
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```typescript
20
+ import { TilaClient, createEntityMethods } from "tila-sdk";
21
+
22
+ const client = new TilaClient({
23
+ baseUrl: process.env.TILA_URL!,
24
+ token: process.env.TILA_TOKEN!,
25
+ });
26
+
27
+ const projectId = "my-project";
28
+ const entities = createEntityMethods(client, projectId);
29
+
30
+ // Create an entity
31
+ const task = await entities.create("task-1", "task", {
32
+ title: "Process dataset",
33
+ status: "pending",
34
+ });
35
+
36
+ // Read it back
37
+ const detail = await entities.get("task-1");
38
+
39
+ // List entities by type
40
+ const list = await entities.list({ type: "task" });
41
+
42
+ // Update with new data
43
+ const updated = await entities.update("task-1", {
44
+ status: "in-progress",
45
+ assignee: "agent-7",
46
+ });
47
+
48
+ // Archive when done
49
+ await entities.archive("task-1");
50
+ ```
51
+
52
+ ### Constructor Options
53
+
54
+ | Option | Type | Default | Description |
55
+ |--------|------|---------|-------------|
56
+ | `baseUrl` | `string` | (required) | tila Worker URL |
57
+ | `token` | `string` | (required) | API token or session token |
58
+ | `validate` | `boolean` | `false` | Enable Zod response validation (requires `zod` installed) |
59
+ | `timeoutMs` | `number` | `30000` | Request timeout in milliseconds |
60
+
61
+ > **Note:** `validate` defaults to `false` to keep the bundle lightweight. Pass `validate: true` to enable Zod schema validation on every response (requires `zod` installed as a peer dependency).
62
+
63
+ If you have a `.tila/config.toml` project file:
64
+
65
+ ```typescript
66
+ import { TilaClient } from "tila-sdk";
67
+
68
+ const client = TilaClient.fromConfig(config, process.env.TILA_TOKEN!);
69
+ ```
70
+
71
+ ## Claim Lifecycle
72
+
73
+ tila uses a first-writer-wins coordination model built on fencing tokens. The `withClaim` primitive acquires a resource lock, runs your callback, and releases the lock in `finally` -- preventing resource leaks.
74
+
75
+ Every `ClaimHandle` carries a monotonic `fence` number. Destructive writes (entity update, artifact upload) carry this fence automatically. The server rejects stale fences with error code `stale-fence`.
76
+
77
+ ```typescript
78
+ import { TilaClient, withClaim } from "tila-sdk";
79
+
80
+ const client = new TilaClient({
81
+ baseUrl: process.env.TILA_URL!,
82
+ token: process.env.TILA_TOKEN!,
83
+ });
84
+
85
+ const projectId = "my-project";
86
+
87
+ await withClaim(client, projectId, "dataset/batch-42", "exclusive", 60_000, async (handle) => {
88
+ // handle.fence is the monotonic fencing token
89
+ // handle.expiresAt is the claim expiry (epoch ms)
90
+
91
+ // Start heartbeat -- auto-renews at 40% of TTL (24s intervals for 60s TTL)
92
+ const hb = handle.startHeartbeat(60_000);
93
+
94
+ // Early-warning timer -- fires 5s before claim expires
95
+ const expiry = handle.onClaimExpiring(5_000, () => {
96
+ console.warn("Claim expiring soon -- wrap up!");
97
+ });
98
+
99
+ // Listen for heartbeat errors (409 = lost claim, 401 = auth expired)
100
+ handle.on("error", (err) => {
101
+ console.error("Heartbeat failed:", err.message);
102
+ });
103
+
104
+ try {
105
+ // Fence-threaded entity update -- fence is carried automatically
106
+ await handle.updateEntity("task-1", { status: "processing" });
107
+
108
+ // ... do work ...
109
+
110
+ await handle.updateEntity("task-1", { status: "complete" });
111
+ } finally {
112
+ expiry.stop();
113
+ hb.stop();
114
+ }
115
+ });
116
+ // Claim is released automatically when the callback exits
117
+ ```
118
+
119
+ ### Claim Modes
120
+
121
+ | Mode | Behavior |
122
+ |------|----------|
123
+ | `"exclusive"` | Only one holder at a time. Acquire fails if already held. |
124
+ | `"shared"` | Multiple holders allowed. Each gets a unique fence. |
125
+
126
+ ## Artifacts
127
+
128
+ ### Upload
129
+
130
+ **Inside a claim context (preferred):** The fence is threaded automatically.
131
+
132
+ ```typescript
133
+ await withClaim(client, projectId, "output/report", "exclusive", 30_000, async (handle) => {
134
+ const hb = handle.startHeartbeat(30_000);
135
+ try {
136
+ // Upload from a File or Blob
137
+ const result = await handle.uploadArtifact(
138
+ new Blob(["report content"], { type: "text/plain" }),
139
+ { kind: "output" },
140
+ );
141
+
142
+ console.log(result.key); // content-addressed key
143
+ console.log(result.deduplicated); // true if content already existed
144
+ } finally {
145
+ hb.stop();
146
+ }
147
+ });
148
+ ```
149
+
150
+ **Standalone upload (no claim):**
151
+
152
+ ```typescript
153
+ import { createArtifactMethods } from "tila-sdk";
154
+
155
+ const artifacts = createArtifactMethods(client, projectId);
156
+
157
+ const result = await artifacts.upload(
158
+ new Blob(["data"], { type: "application/json" }),
159
+ { kind: "intermediate", mimeType: "application/json" },
160
+ );
161
+ ```
162
+
163
+ **`mimeType` requirement:** When the file's `.type` property is empty (plain `Blob` with no type set), you must pass `mimeType` explicitly. A `TypeError` is thrown synchronously before any network request if `mimeType` is absent and `file.type` is empty.
164
+
165
+ ### Download
166
+
167
+ `download()` returns a raw `ReadableStream`. The caller owns consumption and cleanup.
168
+
169
+ ```typescript
170
+ const artifacts = createArtifactMethods(client, projectId);
171
+
172
+ const { body, contentType, contentLength } = await artifacts.download(
173
+ "artifacts/task-1/abc123.json",
174
+ );
175
+
176
+ // Pipe to a file (Node.js)
177
+ const file = Bun.file("output.json");
178
+ await Bun.write(file, body);
179
+
180
+ // Or collect as text
181
+ const text = await new Response(body).text();
182
+ ```
183
+
184
+ ## Error Handling
185
+
186
+ ### Typed Catch Pattern
187
+
188
+ Use `isTilaApiError()` (preferred over `instanceof` for cross-realm/bundled code):
189
+
190
+ ```typescript
191
+ import { isTilaApiError, TILA_ERRORS } from "tila-sdk";
192
+
193
+ try {
194
+ await entities.update("task-1", { status: "done" });
195
+ } catch (err) {
196
+ if (isTilaApiError(err)) {
197
+ switch (err.code) {
198
+ case TILA_ERRORS.STALE_FENCE:
199
+ // Fence was superseded -- re-acquire the claim
200
+ break;
201
+ case TILA_ERRORS.UNAUTHORIZED:
202
+ // Token expired or invalid -- re-authenticate
203
+ break;
204
+ case TILA_ERRORS.NOT_FOUND:
205
+ // Entity does not exist
206
+ break;
207
+ default:
208
+ console.error(`API error ${err.status}: [${err.code}] ${err.message}`);
209
+ }
210
+ } else {
211
+ // Network error, timeout, or malformed response
212
+ console.error("Non-API error:", err);
213
+ }
214
+ }
215
+ ```
216
+
217
+ `TilaApiError` fields:
218
+
219
+ | Field | Type | Description |
220
+ |-------|------|-------------|
221
+ | `status` | `number` | HTTP status code |
222
+ | `code` | `string` | Machine-readable error code |
223
+ | `message` | `string` | Human-readable description |
224
+ | `retryable` | `boolean` | Whether the server considers this retryable |
225
+
226
+ ### Error Code Conventions
227
+
228
+ tila uses two wire-format conventions for error codes:
229
+
230
+ - **Worker/auth layer:** `SCREAMING_SNAKE_CASE` -- e.g., `"UNAUTHORIZED"`, `"SESSION_EXPIRED"`, `"RATE_LIMITED"`
231
+ - **DO (Durable Object) layer:** `kebab-case` -- e.g., `"stale-fence"`, `"not-found"`, `"already-held"`
232
+
233
+ The `TILA_ERRORS` constant object normalizes both under typed keys so you never hardcode string literals:
234
+
235
+ ```typescript
236
+ TILA_ERRORS.UNAUTHORIZED // "UNAUTHORIZED" (worker layer)
237
+ TILA_ERRORS.STALE_FENCE // "stale-fence" (DO layer)
238
+ TILA_ERRORS.NOT_FOUND // "not-found" (DO layer)
239
+ TILA_ERRORS.RATE_LIMITED // "RATE_LIMITED" (worker layer)
240
+ ```
241
+
242
+ ### Retry Wrapper
243
+
244
+ `withRetry` implements exponential backoff with full jitter (AWS pattern):
245
+
246
+ ```typescript
247
+ import { withRetry, withClaim } from "tila-sdk";
248
+
249
+ const result = await withRetry(
250
+ async () => {
251
+ return await withClaim(client, projectId, "resource", "exclusive", 30_000, async (handle) => {
252
+ const hb = handle.startHeartbeat(30_000);
253
+ try {
254
+ await handle.updateEntity("task-1", { status: "done" });
255
+ return "success";
256
+ } finally {
257
+ hb.stop();
258
+ }
259
+ });
260
+ },
261
+ { maxRetries: 5, baseDelayMs: 200 },
262
+ );
263
+ ```
264
+
265
+ **Hard stop rule:** A `TilaApiError` with `retryable === false` is never retried, regardless of `maxRetries`. Network errors and timeouts are always retried up to the limit.
266
+
267
+ | Option | Type | Default | Description |
268
+ |--------|------|---------|-------------|
269
+ | `maxRetries` | `number` | `3` | Maximum retry attempts after first failure |
270
+ | `baseDelayMs` | `number` | `200` | Base delay for exponential backoff |
271
+ | `maxDelayMs` | `number` | `30000` | Maximum delay cap |
272
+ | `jitter` | `boolean` | `true` | Apply full jitter to delay |
273
+
274
+ ## API Reference
275
+
276
+ ### Method Factories
277
+
278
+ `withClaim` + `ClaimHandle` is the recommended high-level coordination API. The method factories below are lower-level building blocks for advanced use -- e.g., when managing claim acquire/release manually.
279
+
280
+ | Factory | Primary Methods | Description |
281
+ |---------|----------------|-------------|
282
+ | `createEntityMethods(client, projectId)` | `create`, `get`, `list`, `update`, `archive`, `addRelationship`, `addArtifactRef`, `listArtifactRefs` | Entity CRUD and relationships |
283
+ | `createClaimMethods(client, projectId)` | `acquire`, `renew`, `release`, `list`, `get` | Low-level claim management |
284
+ | `createArtifactMethods(client, projectId)` | `upload`, `download`, `list`, `search`, `addRelationship`, `listRelationships` | Artifact storage and search |
285
+ | `createPresenceMethods(client, projectId)` | `heartbeat`, `list`, `listAll` | Machine presence tracking |
286
+ | `createSignalMethods(client, projectId)` | `inbox`, `send`, `ack` | Inter-machine signaling |
287
+ | `createGateMethods(client, projectId)` | `list`, `create`, `resolve`, `remove` | Coordination gates |
288
+ | `createTemplateMethods(client, projectId)` | `instantiate` | Entity template instantiation |
289
+ | `createSummaryMethods(client, projectId)` | `get` | Project summary |
290
+ | `createJournalMethods(client, projectId)` | `query` | Event journal queries |
291
+ | `createSchemaMethods(client, projectId)` | `get`, `apply`, `history` | Schema-as-config management |
292
+ | `createTokenMethods(client)` | `issue`, `revoke`, `list` | API token management (no `projectId`) |
293
+
294
+ ### GitHub Token Exchange
295
+
296
+ For CI environments (GitHub Actions) where a tila API token is not available:
297
+
298
+ ```typescript
299
+ import { exchangeGitHubToken, TilaClient } from "tila-sdk";
300
+
301
+ const { sessionToken, expiresAt, permission } = await exchangeGitHubToken(
302
+ process.env.TILA_URL!,
303
+ "my-project",
304
+ process.env.GITHUB_TOKEN!,
305
+ );
306
+
307
+ const client = new TilaClient({
308
+ baseUrl: process.env.TILA_URL!,
309
+ token: sessionToken,
310
+ });
311
+ // sessionToken is short-lived -- expiresAt is epoch ms
312
+ ```
313
+
314
+ > **Note:** `exchangeGitHubToken` is a standalone function, not a `TilaClient` method. The repository must be registered via `tila init --github` before tokens can be exchanged.
315
+
316
+ ## License
317
+
318
+ See the repository root for license information.