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/dist/index.d.cts CHANGED
@@ -264,6 +264,12 @@ declare class NornsClient {
264
264
  delete: (agent: AgentRef, key: string) => Promise<void>;
265
265
  };
266
266
  }
267
+ /**
268
+ * A key issued by `POST /api/v1/api_keys` with a worker scope (`nrnw_...`).
269
+ * It opens the worker socket and nothing else, so a worker holding one must
270
+ * not try the REST API.
271
+ */
272
+ declare function isScopedKey(apiKey: string): boolean;
267
273
 
268
274
  /**
269
275
  * Where the SDK writes what it is doing. Anything with these four methods
@@ -301,7 +307,11 @@ interface NornsWorkerOptions {
301
307
  agents: AgentDefinition | AgentDefinition[];
302
308
  /** The Norns base URL. Defaults to `NORNS_URL`. */
303
309
  url?: string;
304
- /** The tenant's bearer token. Defaults to `NORNS_API_KEY`. */
310
+ /**
311
+ * The bearer token. Defaults to `NORNS_API_KEY`. A scoped worker key
312
+ * (`nrnw_...`) joins without registering the agents, since it cannot reach
313
+ * the REST API; register the definitions with a tenant key.
314
+ */
305
315
  apiKey?: string;
306
316
  /**
307
317
  * How a model name on an LLM task becomes a model to call: a function, a
@@ -582,6 +592,10 @@ declare class JoinError extends NornsError {
582
592
  declare class GardDestroyedError extends NornsError {
583
593
  constructor(gard: string | number | undefined);
584
594
  }
595
+ /** The scoped key this worker joined on was revoked while it was connected. */
596
+ declare class KeyRevokedError extends NornsError {
597
+ constructor();
598
+ }
585
599
  /** A stream's socket closed before the run reached a terminal event. */
586
600
  declare class StreamClosedError extends NornsError {
587
601
  readonly reason: unknown;
@@ -626,6 +640,12 @@ interface LlmTask {
626
640
  summary?: string | null;
627
641
  date?: string | null;
628
642
  messages?: NeutralMessage[];
643
+ /** In place of `messages` once this worker holds the run's history (see HistoryCache). */
644
+ messages_delta?: {
645
+ base_count: number;
646
+ drop: number;
647
+ append: NeutralMessage[];
648
+ };
629
649
  tools?: NeutralTool[];
630
650
  context_policy?: unknown;
631
651
  max_tokens?: number | null;
@@ -669,5 +689,5 @@ type ToolResult = {
669
689
  error: string;
670
690
  };
671
691
 
672
- export { DEFAULT_MODEL, GardDestroyedError, JoinError, NornsApiError, NornsClient, NornsError, NornsTimeoutError, NornsWorker, NotFoundError, StreamClosedError, consoleLogger, defineAgent, inferProvider, silentLogger, tool };
692
+ export { DEFAULT_MODEL, GardDestroyedError, JoinError, KeyRevokedError, NornsApiError, NornsClient, NornsError, NornsTimeoutError, NornsWorker, NotFoundError, StreamClosedError, consoleLogger, defineAgent, inferProvider, isScopedKey, silentLogger, tool };
673
693
  export type { AgentDefinition, AgentInfo, AgentMode, AgentOptions, AgentRef, Capability, CheckpointPolicy, ContextPolicy, ContextStrategy, Conversation, InferToolInput, ListRunsOptions, LlmResult, LlmTask, Logger, MessageResult, ModelResolver, ModelSource, ModelsOption, NeutralMessage, NeutralToolCall, NornsClientOptions, NornsWorkerOptions, OnFailure, RegisterPortOptions, RegisteredPort, Run, RunEvent, RunStatus, SendMessageOptions, StreamEvent, StreamOptions, SubagentConversation, SubagentPolicy, ToolContext, ToolDefinition, ToolInputSchema, ToolOptions, ToolResult, ToolTask, WaitingFor, WebSocketConstructor };
package/dist/index.d.ts CHANGED
@@ -264,6 +264,12 @@ declare class NornsClient {
264
264
  delete: (agent: AgentRef, key: string) => Promise<void>;
265
265
  };
266
266
  }
267
+ /**
268
+ * A key issued by `POST /api/v1/api_keys` with a worker scope (`nrnw_...`).
269
+ * It opens the worker socket and nothing else, so a worker holding one must
270
+ * not try the REST API.
271
+ */
272
+ declare function isScopedKey(apiKey: string): boolean;
267
273
 
268
274
  /**
269
275
  * Where the SDK writes what it is doing. Anything with these four methods
@@ -301,7 +307,11 @@ interface NornsWorkerOptions {
301
307
  agents: AgentDefinition | AgentDefinition[];
302
308
  /** The Norns base URL. Defaults to `NORNS_URL`. */
303
309
  url?: string;
304
- /** The tenant's bearer token. Defaults to `NORNS_API_KEY`. */
310
+ /**
311
+ * The bearer token. Defaults to `NORNS_API_KEY`. A scoped worker key
312
+ * (`nrnw_...`) joins without registering the agents, since it cannot reach
313
+ * the REST API; register the definitions with a tenant key.
314
+ */
305
315
  apiKey?: string;
306
316
  /**
307
317
  * How a model name on an LLM task becomes a model to call: a function, a
@@ -582,6 +592,10 @@ declare class JoinError extends NornsError {
582
592
  declare class GardDestroyedError extends NornsError {
583
593
  constructor(gard: string | number | undefined);
584
594
  }
595
+ /** The scoped key this worker joined on was revoked while it was connected. */
596
+ declare class KeyRevokedError extends NornsError {
597
+ constructor();
598
+ }
585
599
  /** A stream's socket closed before the run reached a terminal event. */
586
600
  declare class StreamClosedError extends NornsError {
587
601
  readonly reason: unknown;
@@ -626,6 +640,12 @@ interface LlmTask {
626
640
  summary?: string | null;
627
641
  date?: string | null;
628
642
  messages?: NeutralMessage[];
643
+ /** In place of `messages` once this worker holds the run's history (see HistoryCache). */
644
+ messages_delta?: {
645
+ base_count: number;
646
+ drop: number;
647
+ append: NeutralMessage[];
648
+ };
629
649
  tools?: NeutralTool[];
630
650
  context_policy?: unknown;
631
651
  max_tokens?: number | null;
@@ -669,5 +689,5 @@ type ToolResult = {
669
689
  error: string;
670
690
  };
671
691
 
672
- export { DEFAULT_MODEL, GardDestroyedError, JoinError, NornsApiError, NornsClient, NornsError, NornsTimeoutError, NornsWorker, NotFoundError, StreamClosedError, consoleLogger, defineAgent, inferProvider, silentLogger, tool };
692
+ export { DEFAULT_MODEL, GardDestroyedError, JoinError, KeyRevokedError, NornsApiError, NornsClient, NornsError, NornsTimeoutError, NornsWorker, NotFoundError, StreamClosedError, consoleLogger, defineAgent, inferProvider, isScopedKey, silentLogger, tool };
673
693
  export type { AgentDefinition, AgentInfo, AgentMode, AgentOptions, AgentRef, Capability, CheckpointPolicy, ContextPolicy, ContextStrategy, Conversation, InferToolInput, ListRunsOptions, LlmResult, LlmTask, Logger, MessageResult, ModelResolver, ModelSource, ModelsOption, NeutralMessage, NeutralToolCall, NornsClientOptions, NornsWorkerOptions, OnFailure, RegisterPortOptions, RegisteredPort, Run, RunEvent, RunStatus, SendMessageOptions, StreamEvent, StreamOptions, SubagentConversation, SubagentPolicy, ToolContext, ToolDefinition, ToolInputSchema, ToolOptions, ToolResult, ToolTask, WaitingFor, WebSocketConstructor };
package/dist/index.js CHANGED
@@ -206,6 +206,12 @@ var GardDestroyedError = class extends NornsError {
206
206
  this.name = "GardDestroyedError";
207
207
  }
208
208
  };
209
+ var KeyRevokedError = class extends NornsError {
210
+ constructor() {
211
+ super("the worker's scoped key was revoked");
212
+ this.name = "KeyRevokedError";
213
+ }
214
+ };
209
215
  var StreamClosedError = class extends NornsError {
210
216
  reason;
211
217
  constructor(reason) {
@@ -534,6 +540,9 @@ function resolveApiKey(apiKey) {
534
540
  if (!value) throw new NornsError("Norns API key missing: pass `apiKey` or set NORNS_API_KEY");
535
541
  return value;
536
542
  }
543
+ function isScopedKey(apiKey) {
544
+ return apiKey.startsWith("nrnw_");
545
+ }
537
546
  function wsBase(url) {
538
547
  return url.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
539
548
  }
@@ -1113,6 +1122,51 @@ function neutralUsage(usage) {
1113
1122
  return out;
1114
1123
  }
1115
1124
 
1125
+ // src/history-cache.ts
1126
+ var HistoryCache = class {
1127
+ #runs = /* @__PURE__ */ new Map();
1128
+ #maxRuns;
1129
+ constructor(maxRuns = 256) {
1130
+ this.#maxRuns = maxRuns;
1131
+ }
1132
+ /** The task with `messages` filled in, or `undefined` on a miss. */
1133
+ resolve(task) {
1134
+ if (task.purpose === "compact") return task;
1135
+ const runId = task.run_id;
1136
+ const delta = task.messages_delta;
1137
+ if (delta === void 0) {
1138
+ if (runId !== void 0 && Array.isArray(task.messages)) this.#remember(runId, task.messages);
1139
+ return task;
1140
+ }
1141
+ const previous = runId === void 0 ? void 0 : this.#runs.get(runId);
1142
+ if (runId === void 0 || previous === void 0 || previous.length !== delta.base_count) {
1143
+ return void 0;
1144
+ }
1145
+ const messages = [...previous.slice(delta.drop), ...delta.append];
1146
+ this.#remember(runId, messages);
1147
+ const resolved = { ...task, messages };
1148
+ delete resolved.messages_delta;
1149
+ return resolved;
1150
+ }
1151
+ /** Forget everything: a new connection gets the full history anyway. */
1152
+ clear() {
1153
+ this.#runs.clear();
1154
+ }
1155
+ /** How many runs are held; for tests. */
1156
+ get size() {
1157
+ return this.#runs.size;
1158
+ }
1159
+ #remember(runId, messages) {
1160
+ this.#runs.delete(runId);
1161
+ this.#runs.set(runId, messages);
1162
+ while (this.#runs.size > this.#maxRuns) {
1163
+ const oldest = this.#runs.keys().next().value;
1164
+ if (oldest === void 0) break;
1165
+ this.#runs.delete(oldest);
1166
+ }
1167
+ }
1168
+ };
1169
+
1116
1170
  // src/worker.ts
1117
1171
  var TOPIC = "worker:lobby";
1118
1172
  var NornsWorker = class {
@@ -1133,6 +1187,7 @@ var NornsWorker = class {
1133
1187
  #client;
1134
1188
  #resolveModel;
1135
1189
  #inFlight = /* @__PURE__ */ new Set();
1190
+ #history = new HistoryCache();
1136
1191
  #state = "idle";
1137
1192
  #socket;
1138
1193
  #channel;
@@ -1201,9 +1256,15 @@ var NornsWorker = class {
1201
1256
  return this.#starting;
1202
1257
  }
1203
1258
  async #start() {
1204
- for (const agent of this.#agents) {
1205
- const info = await this.#client.agents.ensure(agent);
1206
- this.#logger.info(`Registered agent '${agent.name}' (id=${info.id})`);
1259
+ if (isScopedKey(this.#apiKey)) {
1260
+ this.#logger.info(
1261
+ "Worker key is scoped (nrnw_): skipping agent registration. Register the agent definitions with a tenant key."
1262
+ );
1263
+ } else {
1264
+ for (const agent of this.#agents) {
1265
+ const info = await this.#client.agents.ensure(agent);
1266
+ this.#logger.info(`Registered agent '${agent.name}' (id=${info.id})`);
1267
+ }
1207
1268
  }
1208
1269
  if (this.#capabilities.includes("llm")) {
1209
1270
  for (const model of new Set(this.#agents.map((a) => a.model))) {
@@ -1225,7 +1286,10 @@ var NornsWorker = class {
1225
1286
  ...this.#socketOptions
1226
1287
  });
1227
1288
  this.#socket = socket;
1228
- socket.onOpen(() => this.#logger.info(`Connected to ${this.url}`));
1289
+ socket.onOpen(() => {
1290
+ this.#logger.info(`Connected to ${this.url}`);
1291
+ this.#history.clear();
1292
+ });
1229
1293
  socket.onError((err) => {
1230
1294
  if (this.#state === "starting" || this.#state === "running") {
1231
1295
  this.#logger.warn(`Connection error (${errorMessage(err)}); retrying`);
@@ -1251,6 +1315,10 @@ var NornsWorker = class {
1251
1315
  this.#logger.error("This worker's gard was destroyed; shutting down");
1252
1316
  this.#fail(new GardDestroyedError(this.gard));
1253
1317
  });
1318
+ channel.on("key_revoked", () => {
1319
+ this.#logger.error("This worker's key was revoked; shutting down");
1320
+ this.#fail(new KeyRevokedError());
1321
+ });
1254
1322
  channel.onError((reason) => {
1255
1323
  if (this.#state === "running")
1256
1324
  this.#logger.warn(`Channel error (${errorMessage(reason)}); rejoining`);
@@ -1284,7 +1352,10 @@ var NornsWorker = class {
1284
1352
  const params = {
1285
1353
  worker_id: this.workerId,
1286
1354
  tools: [...this.#tools.values()].map(toolRegistration),
1287
- capabilities: this.#capabilities
1355
+ capabilities: this.#capabilities,
1356
+ // This worker keeps the history it was last sent per run, so Norns may
1357
+ // send later steps as the change only (HistoryCache).
1358
+ features: ["message_cache"]
1288
1359
  };
1289
1360
  if (this.gard !== void 0) {
1290
1361
  params.gard = this.gard;
@@ -1413,7 +1484,14 @@ var NornsWorker = class {
1413
1484
  (resp) => this.#logger.warn(`Server rejected result for ${taskId}: ${errorMessage(resp)}`)
1414
1485
  );
1415
1486
  }
1416
- async #runLlm(task, signal) {
1487
+ async #runLlm(received, signal) {
1488
+ const task = this.#history.resolve(received);
1489
+ if (task === void 0) {
1490
+ this.#logger.info(
1491
+ "LLM task arrived as a delta on an unknown base; asking for the full history"
1492
+ );
1493
+ return { status: "error", error: "message_cache_miss" };
1494
+ }
1417
1495
  const model = task.model ?? "?";
1418
1496
  const tools = task.tools ?? [];
1419
1497
  this.#logger.info(
@@ -1553,6 +1631,7 @@ export {
1553
1631
  DEFAULT_MODEL,
1554
1632
  GardDestroyedError,
1555
1633
  JoinError,
1634
+ KeyRevokedError,
1556
1635
  NornsApiError,
1557
1636
  NornsClient,
1558
1637
  NornsError,
@@ -1563,6 +1642,7 @@ export {
1563
1642
  consoleLogger,
1564
1643
  defineAgent,
1565
1644
  inferProvider,
1645
+ isScopedKey,
1566
1646
  jsonSchema3 as jsonSchema,
1567
1647
  silentLogger,
1568
1648
  tool