norns-sdk 0.1.0 → 0.3.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/CHANGELOG.md +20 -0
- package/README.md +6 -0
- package/dist/index.cjs +88 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.js +86 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `norns-sdk` are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-10-22
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- A worker holding a scoped key (`nrnw_...`, issued for one gard's worker)
|
|
9
|
+
skips the agent registration on `start()`, since the key cannot reach the
|
|
10
|
+
REST API; the definitions are registered with a tenant key instead.
|
|
11
|
+
`isScopedKey()` is exported.
|
|
12
|
+
- `KeyRevokedError`: `worker.stopped` rejects with it when Norns revokes the
|
|
13
|
+
key the worker joined on.
|
|
14
|
+
|
|
15
|
+
## [0.2.0] - 2026-10-16
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- The worker joins with `features: ["message_cache"]` and keeps the message
|
|
19
|
+
list Norns last sent it per run, so Norns can send later steps as
|
|
20
|
+
`messages_delta` (the change) instead of the whole history. A delta on a
|
|
21
|
+
base the worker no longer holds is answered with `message_cache_miss` and
|
|
22
|
+
Norns resends in full. Older Norns ignores the feature and sends the full
|
|
23
|
+
list as before.
|
|
24
|
+
|
|
5
25
|
## [0.1.0] - 2026-10-15
|
|
6
26
|
|
|
7
27
|
### Added
|
package/README.md
CHANGED
|
@@ -90,6 +90,12 @@ execute: async ({ port }, ctx) => {
|
|
|
90
90
|
|
|
91
91
|
A fatally rejected claim (bad token, destroyed gard) rejects `start()` with `JoinError` instead of reconnect-looping. If the gard is destroyed while the worker is connected, `worker.stopped` rejects with `GardDestroyedError`.
|
|
92
92
|
|
|
93
|
+
A worker inside a gard should hold a [scoped key](https://github.com/nornscode/norns/blob/main/docs/gards.md#scoped-worker-keys) (`nrnw_...`) rather than the tenant key: it can join the worker socket, claim its gard and serve tasks, and nothing else. With one, `start()` skips the agent registration, since the key cannot reach the REST API; register the definitions with a tenant key instead. Pass `capabilities: ["tools"]` for a worker with no LLM access, matching what the key allows. If the key is revoked while the worker is connected, `worker.stopped` rejects with `KeyRevokedError`.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const worker = new NornsWorker({ agents: [coder], gard: 3, claimToken: "tok_...", capabilities: ["tools"] });
|
|
97
|
+
```
|
|
98
|
+
|
|
93
99
|
### Shutdown
|
|
94
100
|
|
|
95
101
|
On SIGTERM or SIGINT the worker drains instead of dying mid-task: it tells Norns to stop sending it work, finishes the tasks it holds, reports their results, leaves the channel, and the process exits. Tasks still running after `shutdownTimeoutMs` (default 30 seconds, or `NORNS_SHUTDOWN_TIMEOUT` in seconds) are dropped and Norns re-dispatches them. A second signal exits at once.
|
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
DEFAULT_MODEL: () => DEFAULT_MODEL,
|
|
24
24
|
GardDestroyedError: () => GardDestroyedError,
|
|
25
25
|
JoinError: () => JoinError,
|
|
26
|
+
KeyRevokedError: () => KeyRevokedError,
|
|
26
27
|
NornsApiError: () => NornsApiError,
|
|
27
28
|
NornsClient: () => NornsClient,
|
|
28
29
|
NornsError: () => NornsError,
|
|
@@ -33,6 +34,7 @@ __export(index_exports, {
|
|
|
33
34
|
consoleLogger: () => consoleLogger,
|
|
34
35
|
defineAgent: () => defineAgent,
|
|
35
36
|
inferProvider: () => inferProvider,
|
|
37
|
+
isScopedKey: () => isScopedKey,
|
|
36
38
|
jsonSchema: () => import_ai4.jsonSchema,
|
|
37
39
|
silentLogger: () => silentLogger,
|
|
38
40
|
tool: () => tool
|
|
@@ -245,6 +247,12 @@ var GardDestroyedError = class extends NornsError {
|
|
|
245
247
|
this.name = "GardDestroyedError";
|
|
246
248
|
}
|
|
247
249
|
};
|
|
250
|
+
var KeyRevokedError = class extends NornsError {
|
|
251
|
+
constructor() {
|
|
252
|
+
super("the worker's scoped key was revoked");
|
|
253
|
+
this.name = "KeyRevokedError";
|
|
254
|
+
}
|
|
255
|
+
};
|
|
248
256
|
var StreamClosedError = class extends NornsError {
|
|
249
257
|
reason;
|
|
250
258
|
constructor(reason) {
|
|
@@ -573,6 +581,9 @@ function resolveApiKey(apiKey) {
|
|
|
573
581
|
if (!value) throw new NornsError("Norns API key missing: pass `apiKey` or set NORNS_API_KEY");
|
|
574
582
|
return value;
|
|
575
583
|
}
|
|
584
|
+
function isScopedKey(apiKey) {
|
|
585
|
+
return apiKey.startsWith("nrnw_");
|
|
586
|
+
}
|
|
576
587
|
function wsBase(url) {
|
|
577
588
|
return url.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
|
|
578
589
|
}
|
|
@@ -1149,6 +1160,51 @@ function neutralUsage(usage) {
|
|
|
1149
1160
|
return out;
|
|
1150
1161
|
}
|
|
1151
1162
|
|
|
1163
|
+
// src/history-cache.ts
|
|
1164
|
+
var HistoryCache = class {
|
|
1165
|
+
#runs = /* @__PURE__ */ new Map();
|
|
1166
|
+
#maxRuns;
|
|
1167
|
+
constructor(maxRuns = 256) {
|
|
1168
|
+
this.#maxRuns = maxRuns;
|
|
1169
|
+
}
|
|
1170
|
+
/** The task with `messages` filled in, or `undefined` on a miss. */
|
|
1171
|
+
resolve(task) {
|
|
1172
|
+
if (task.purpose === "compact") return task;
|
|
1173
|
+
const runId = task.run_id;
|
|
1174
|
+
const delta = task.messages_delta;
|
|
1175
|
+
if (delta === void 0) {
|
|
1176
|
+
if (runId !== void 0 && Array.isArray(task.messages)) this.#remember(runId, task.messages);
|
|
1177
|
+
return task;
|
|
1178
|
+
}
|
|
1179
|
+
const previous = runId === void 0 ? void 0 : this.#runs.get(runId);
|
|
1180
|
+
if (runId === void 0 || previous === void 0 || previous.length !== delta.base_count) {
|
|
1181
|
+
return void 0;
|
|
1182
|
+
}
|
|
1183
|
+
const messages = [...previous.slice(delta.drop), ...delta.append];
|
|
1184
|
+
this.#remember(runId, messages);
|
|
1185
|
+
const resolved = { ...task, messages };
|
|
1186
|
+
delete resolved.messages_delta;
|
|
1187
|
+
return resolved;
|
|
1188
|
+
}
|
|
1189
|
+
/** Forget everything: a new connection gets the full history anyway. */
|
|
1190
|
+
clear() {
|
|
1191
|
+
this.#runs.clear();
|
|
1192
|
+
}
|
|
1193
|
+
/** How many runs are held; for tests. */
|
|
1194
|
+
get size() {
|
|
1195
|
+
return this.#runs.size;
|
|
1196
|
+
}
|
|
1197
|
+
#remember(runId, messages) {
|
|
1198
|
+
this.#runs.delete(runId);
|
|
1199
|
+
this.#runs.set(runId, messages);
|
|
1200
|
+
while (this.#runs.size > this.#maxRuns) {
|
|
1201
|
+
const oldest = this.#runs.keys().next().value;
|
|
1202
|
+
if (oldest === void 0) break;
|
|
1203
|
+
this.#runs.delete(oldest);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1152
1208
|
// src/worker.ts
|
|
1153
1209
|
var TOPIC = "worker:lobby";
|
|
1154
1210
|
var NornsWorker = class {
|
|
@@ -1169,6 +1225,7 @@ var NornsWorker = class {
|
|
|
1169
1225
|
#client;
|
|
1170
1226
|
#resolveModel;
|
|
1171
1227
|
#inFlight = /* @__PURE__ */ new Set();
|
|
1228
|
+
#history = new HistoryCache();
|
|
1172
1229
|
#state = "idle";
|
|
1173
1230
|
#socket;
|
|
1174
1231
|
#channel;
|
|
@@ -1237,9 +1294,15 @@ var NornsWorker = class {
|
|
|
1237
1294
|
return this.#starting;
|
|
1238
1295
|
}
|
|
1239
1296
|
async #start() {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1297
|
+
if (isScopedKey(this.#apiKey)) {
|
|
1298
|
+
this.#logger.info(
|
|
1299
|
+
"Worker key is scoped (nrnw_): skipping agent registration. Register the agent definitions with a tenant key."
|
|
1300
|
+
);
|
|
1301
|
+
} else {
|
|
1302
|
+
for (const agent of this.#agents) {
|
|
1303
|
+
const info = await this.#client.agents.ensure(agent);
|
|
1304
|
+
this.#logger.info(`Registered agent '${agent.name}' (id=${info.id})`);
|
|
1305
|
+
}
|
|
1243
1306
|
}
|
|
1244
1307
|
if (this.#capabilities.includes("llm")) {
|
|
1245
1308
|
for (const model of new Set(this.#agents.map((a) => a.model))) {
|
|
@@ -1261,7 +1324,10 @@ var NornsWorker = class {
|
|
|
1261
1324
|
...this.#socketOptions
|
|
1262
1325
|
});
|
|
1263
1326
|
this.#socket = socket;
|
|
1264
|
-
socket.onOpen(() =>
|
|
1327
|
+
socket.onOpen(() => {
|
|
1328
|
+
this.#logger.info(`Connected to ${this.url}`);
|
|
1329
|
+
this.#history.clear();
|
|
1330
|
+
});
|
|
1265
1331
|
socket.onError((err) => {
|
|
1266
1332
|
if (this.#state === "starting" || this.#state === "running") {
|
|
1267
1333
|
this.#logger.warn(`Connection error (${errorMessage(err)}); retrying`);
|
|
@@ -1287,6 +1353,10 @@ var NornsWorker = class {
|
|
|
1287
1353
|
this.#logger.error("This worker's gard was destroyed; shutting down");
|
|
1288
1354
|
this.#fail(new GardDestroyedError(this.gard));
|
|
1289
1355
|
});
|
|
1356
|
+
channel.on("key_revoked", () => {
|
|
1357
|
+
this.#logger.error("This worker's key was revoked; shutting down");
|
|
1358
|
+
this.#fail(new KeyRevokedError());
|
|
1359
|
+
});
|
|
1290
1360
|
channel.onError((reason) => {
|
|
1291
1361
|
if (this.#state === "running")
|
|
1292
1362
|
this.#logger.warn(`Channel error (${errorMessage(reason)}); rejoining`);
|
|
@@ -1320,7 +1390,10 @@ var NornsWorker = class {
|
|
|
1320
1390
|
const params = {
|
|
1321
1391
|
worker_id: this.workerId,
|
|
1322
1392
|
tools: [...this.#tools.values()].map(toolRegistration),
|
|
1323
|
-
capabilities: this.#capabilities
|
|
1393
|
+
capabilities: this.#capabilities,
|
|
1394
|
+
// This worker keeps the history it was last sent per run, so Norns may
|
|
1395
|
+
// send later steps as the change only (HistoryCache).
|
|
1396
|
+
features: ["message_cache"]
|
|
1324
1397
|
};
|
|
1325
1398
|
if (this.gard !== void 0) {
|
|
1326
1399
|
params.gard = this.gard;
|
|
@@ -1449,7 +1522,14 @@ var NornsWorker = class {
|
|
|
1449
1522
|
(resp) => this.#logger.warn(`Server rejected result for ${taskId}: ${errorMessage(resp)}`)
|
|
1450
1523
|
);
|
|
1451
1524
|
}
|
|
1452
|
-
async #runLlm(
|
|
1525
|
+
async #runLlm(received, signal) {
|
|
1526
|
+
const task = this.#history.resolve(received);
|
|
1527
|
+
if (task === void 0) {
|
|
1528
|
+
this.#logger.info(
|
|
1529
|
+
"LLM task arrived as a delta on an unknown base; asking for the full history"
|
|
1530
|
+
);
|
|
1531
|
+
return { status: "error", error: "message_cache_miss" };
|
|
1532
|
+
}
|
|
1453
1533
|
const model = task.model ?? "?";
|
|
1454
1534
|
const tools = task.tools ?? [];
|
|
1455
1535
|
this.#logger.info(
|
|
@@ -1590,6 +1670,7 @@ var NornsWorker = class {
|
|
|
1590
1670
|
DEFAULT_MODEL,
|
|
1591
1671
|
GardDestroyedError,
|
|
1592
1672
|
JoinError,
|
|
1673
|
+
KeyRevokedError,
|
|
1593
1674
|
NornsApiError,
|
|
1594
1675
|
NornsClient,
|
|
1595
1676
|
NornsError,
|
|
@@ -1600,6 +1681,7 @@ var NornsWorker = class {
|
|
|
1600
1681
|
consoleLogger,
|
|
1601
1682
|
defineAgent,
|
|
1602
1683
|
inferProvider,
|
|
1684
|
+
isScopedKey,
|
|
1603
1685
|
jsonSchema,
|
|
1604
1686
|
silentLogger,
|
|
1605
1687
|
tool
|