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 +21 -0
- package/README.md +318 -0
- package/dist/index.cjs +2168 -0
- package/dist/index.d.cts +464 -0
- package/dist/index.d.ts +464 -0
- package/dist/index.js +2142 -0
- package/package.json +61 -0
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.
|