norns-sdk 0.0.0-stage → 0.2.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.cjs ADDED
@@ -0,0 +1,1666 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_MODEL: () => DEFAULT_MODEL,
24
+ GardDestroyedError: () => GardDestroyedError,
25
+ JoinError: () => JoinError,
26
+ NornsApiError: () => NornsApiError,
27
+ NornsClient: () => NornsClient,
28
+ NornsError: () => NornsError,
29
+ NornsTimeoutError: () => NornsTimeoutError,
30
+ NornsWorker: () => NornsWorker,
31
+ NotFoundError: () => NotFoundError,
32
+ StreamClosedError: () => StreamClosedError,
33
+ consoleLogger: () => consoleLogger,
34
+ defineAgent: () => defineAgent,
35
+ inferProvider: () => inferProvider,
36
+ jsonSchema: () => import_ai4.jsonSchema,
37
+ silentLogger: () => silentLogger,
38
+ tool: () => tool
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+ var import_ai4 = require("ai");
42
+
43
+ // src/agent.ts
44
+ var DEFAULT_MODEL = "claude-sonnet-5";
45
+ var SUBAGENT_CONVERSATIONS = ["per_launch", "per_parent"];
46
+ function defineAgent(options) {
47
+ const name = options.name?.trim();
48
+ if (!name) throw new Error("defineAgent(): name is required");
49
+ const tools = options.tools ?? [];
50
+ const known = new Set(tools.map((t) => t.name));
51
+ if (known.size !== tools.length) {
52
+ throw new Error(`Agent '${name}': tool names must be unique`);
53
+ }
54
+ if (options.allowedTools) {
55
+ const unknown = options.allowedTools.filter((n) => !known.has(n));
56
+ if (unknown.length > 0) {
57
+ throw new Error(
58
+ `Agent '${name}': allowedTools names ${JSON.stringify(unknown)} are not in its tools (${JSON.stringify([...known].sort())}); built-ins are always available and need no entry`
59
+ );
60
+ }
61
+ }
62
+ if (options.subagentConversation !== void 0 && !SUBAGENT_CONVERSATIONS.includes(options.subagentConversation)) {
63
+ throw new Error(
64
+ `Agent '${name}': subagentConversation must be one of ${SUBAGENT_CONVERSATIONS.join(", ")}`
65
+ );
66
+ }
67
+ if (options.maxTokens !== void 0 && !(Number.isInteger(options.maxTokens) && options.maxTokens > 0)) {
68
+ throw new Error(`Agent '${name}': maxTokens must be a positive integer`);
69
+ }
70
+ return Object.freeze({
71
+ name,
72
+ purpose: options.purpose,
73
+ model: options.model ?? DEFAULT_MODEL,
74
+ systemPrompt: options.systemPrompt ?? "",
75
+ tools: Object.freeze([...tools]),
76
+ mode: options.mode ?? "task",
77
+ checkpointPolicy: options.checkpointPolicy ?? "on_tool_call",
78
+ contextStrategy: options.contextStrategy ?? "sliding_window",
79
+ contextWindow: options.contextWindow ?? 20,
80
+ maxSteps: options.maxSteps ?? 50,
81
+ onFailure: options.onFailure ?? "retry_last_step",
82
+ contextPolicy: options.contextPolicy,
83
+ maxTokens: options.maxTokens,
84
+ allowedTools: options.allowedTools ? Object.freeze([...options.allowedTools]) : void 0,
85
+ subagents: options.subagents,
86
+ subagentConversation: options.subagentConversation
87
+ });
88
+ }
89
+ function agentBody(agent) {
90
+ const modelConfig = {
91
+ mode: agent.mode,
92
+ checkpoint_policy: agent.checkpointPolicy,
93
+ context_strategy: agent.contextStrategy,
94
+ context_window: agent.contextWindow,
95
+ on_failure: agent.onFailure,
96
+ context_policy: agent.contextPolicy ? { compact_at: agent.contextPolicy.compactAt, keep: agent.contextPolicy.keep } : null,
97
+ max_tokens: agent.maxTokens ?? null
98
+ };
99
+ if (agent.allowedTools) {
100
+ modelConfig.tools = { mode: "allowlist", allowed_tools: [...agent.allowedTools] };
101
+ }
102
+ if (agent.subagents) {
103
+ const s = agent.subagents;
104
+ const policy = {};
105
+ if (s.mode !== void 0) policy.mode = s.mode;
106
+ if (s.allowedAgents !== void 0) policy.allowed_agents = [...s.allowedAgents];
107
+ if (s.allowListAgents !== void 0) policy.allow_list_agents = s.allowListAgents;
108
+ if (s.maxDepth !== void 0) policy.max_depth = s.maxDepth;
109
+ modelConfig.subagents = policy;
110
+ }
111
+ if (agent.subagentConversation) {
112
+ modelConfig.subagent_conversation = agent.subagentConversation;
113
+ }
114
+ const body = {
115
+ name: agent.name,
116
+ system_prompt: agent.systemPrompt,
117
+ status: "idle",
118
+ model: agent.model,
119
+ max_steps: agent.maxSteps,
120
+ model_config: modelConfig
121
+ };
122
+ if (agent.purpose !== void 0) body.purpose = agent.purpose;
123
+ return body;
124
+ }
125
+ function agentList(agents) {
126
+ const items = Array.isArray(agents) ? [...agents] : [agents];
127
+ if (items.length === 0) throw new Error("A worker needs at least one agent");
128
+ const seen = /* @__PURE__ */ new Set();
129
+ const dupes = /* @__PURE__ */ new Set();
130
+ for (const a of items) {
131
+ if (seen.has(a.name)) dupes.add(a.name);
132
+ seen.add(a.name);
133
+ }
134
+ if (dupes.size > 0) {
135
+ throw new Error(
136
+ `Agent names must be unique on one worker: ${JSON.stringify([...dupes].sort())}`
137
+ );
138
+ }
139
+ return items;
140
+ }
141
+ function toolsByName(agents) {
142
+ const tools = /* @__PURE__ */ new Map();
143
+ const owners = /* @__PURE__ */ new Map();
144
+ for (const agent of agents) {
145
+ for (const t of agent.tools) {
146
+ const existing = tools.get(t.name);
147
+ if (existing === void 0) {
148
+ tools.set(t.name, t);
149
+ owners.set(t.name, agent.name);
150
+ } else if (existing !== t) {
151
+ throw new Error(
152
+ `Agents '${owners.get(t.name)}' and '${agent.name}' have different tools named '${t.name}'; tools are dispatched by name, so share one definition or rename one`
153
+ );
154
+ }
155
+ }
156
+ }
157
+ return tools;
158
+ }
159
+
160
+ // src/client.ts
161
+ var import_phoenix = require("phoenix");
162
+
163
+ // src/async-queue.ts
164
+ var AsyncQueue = class {
165
+ #values = [];
166
+ #waiters = [];
167
+ #done = false;
168
+ #error = void 0;
169
+ push(value) {
170
+ if (this.#done) return;
171
+ const waiter = this.#waiters.shift();
172
+ if (waiter) waiter.resolve({ value, done: false });
173
+ else this.#values.push(value);
174
+ }
175
+ close() {
176
+ if (this.#done) return;
177
+ this.#done = true;
178
+ for (const w of this.#waiters.splice(0)) w.resolve({ value: void 0, done: true });
179
+ }
180
+ fail(error) {
181
+ if (this.#done) return;
182
+ this.#done = true;
183
+ this.#error = error;
184
+ for (const w of this.#waiters.splice(0)) w.reject(error);
185
+ }
186
+ get closed() {
187
+ return this.#done;
188
+ }
189
+ next() {
190
+ if (this.#values.length > 0) {
191
+ return Promise.resolve({ value: this.#values.shift(), done: false });
192
+ }
193
+ if (this.#done) {
194
+ return this.#error !== void 0 ? Promise.reject(this.#error) : Promise.resolve({ value: void 0, done: true });
195
+ }
196
+ return new Promise((resolve, reject) => this.#waiters.push({ resolve, reject }));
197
+ }
198
+ [Symbol.asyncIterator]() {
199
+ return { next: () => this.next() };
200
+ }
201
+ };
202
+
203
+ // src/errors.ts
204
+ var NornsError = class extends Error {
205
+ constructor(message, options) {
206
+ super(message, options);
207
+ this.name = "NornsError";
208
+ }
209
+ };
210
+ var NornsApiError = class extends NornsError {
211
+ status;
212
+ body;
213
+ constructor(status, body, message) {
214
+ super(message ?? `Norns API error ${status}: ${describeBody(body)}`);
215
+ this.name = "NornsApiError";
216
+ this.status = status;
217
+ this.body = body;
218
+ }
219
+ };
220
+ var NotFoundError = class extends NornsApiError {
221
+ constructor(what, body = void 0) {
222
+ super(404, body, `${what} not found`);
223
+ this.name = "NotFoundError";
224
+ }
225
+ };
226
+ var NornsTimeoutError = class extends NornsError {
227
+ constructor(message) {
228
+ super(message);
229
+ this.name = "NornsTimeoutError";
230
+ }
231
+ };
232
+ var JoinError = class extends NornsError {
233
+ reason;
234
+ code;
235
+ constructor(reason, code) {
236
+ super(`worker join rejected: ${reason}${code ? ` (${code})` : ""}`);
237
+ this.name = "JoinError";
238
+ this.reason = reason;
239
+ this.code = code;
240
+ }
241
+ };
242
+ var GardDestroyedError = class extends NornsError {
243
+ constructor(gard) {
244
+ super(`gard ${gard ?? "?"} was destroyed`);
245
+ this.name = "GardDestroyedError";
246
+ }
247
+ };
248
+ var StreamClosedError = class extends NornsError {
249
+ reason;
250
+ constructor(reason) {
251
+ super(`stream closed before the run finished: ${describeBody(reason)}`);
252
+ this.name = "StreamClosedError";
253
+ this.reason = reason;
254
+ }
255
+ };
256
+ function describeBody(body) {
257
+ if (typeof body === "string") return body;
258
+ if (body && typeof body === "object" && "error" in body) {
259
+ const err = body.error;
260
+ return typeof err === "string" ? err : JSON.stringify(err);
261
+ }
262
+ if (body instanceof Error) return body.message;
263
+ try {
264
+ return JSON.stringify(body);
265
+ } catch {
266
+ return String(body);
267
+ }
268
+ }
269
+ function errorMessage(err) {
270
+ if (err instanceof Error) return err.message;
271
+ return describeBody(err);
272
+ }
273
+
274
+ // src/types.ts
275
+ var TERMINAL_EVENTS = /* @__PURE__ */ new Set(["completed", "error"]);
276
+
277
+ // src/client.ts
278
+ var WAIT_STOPS = /* @__PURE__ */ new Set(["completed", "failed", "error", "waiting"]);
279
+ var NornsClient = class {
280
+ url;
281
+ #apiKey;
282
+ #fetch;
283
+ #transport;
284
+ #socketOptions;
285
+ constructor(options = {}) {
286
+ this.url = resolveUrl(options.url);
287
+ this.#apiKey = resolveApiKey(options.apiKey);
288
+ this.#fetch = options.fetch ?? globalThis.fetch;
289
+ this.#transport = options.transport;
290
+ this.#socketOptions = options.socketOptions ?? {};
291
+ }
292
+ // --- Agents ---
293
+ agents = {
294
+ list: async () => {
295
+ const res = await this.#request("GET", "/api/v1/agents");
296
+ return res.data.map(parseAgent);
297
+ },
298
+ /** By id, or by name. */
299
+ get: async (agent) => {
300
+ if (typeof agent === "number") {
301
+ const res = await this.#request(
302
+ "GET",
303
+ `/api/v1/agents/${agent}`
304
+ );
305
+ return parseAgent(res.data);
306
+ }
307
+ const found = (await this.agents.list()).find((a) => a.name === agent);
308
+ if (!found) throw new NotFoundError(`Agent '${agent}'`);
309
+ return found;
310
+ },
311
+ /**
312
+ * Create the agent, or update it to match the definition if it exists,
313
+ * so code changes are always picked up. The worker does this on start.
314
+ */
315
+ ensure: async (definition) => {
316
+ const body = agentBody(definition);
317
+ const existing = (await this.agents.list()).find((a) => a.name === definition.name);
318
+ if (existing) {
319
+ const res2 = await this.#request(
320
+ "PUT",
321
+ `/api/v1/agents/${existing.id}`,
322
+ { body }
323
+ );
324
+ return parseAgent(res2.data);
325
+ }
326
+ const res = await this.#request("POST", "/api/v1/agents", {
327
+ body
328
+ });
329
+ return parseAgent(res.data);
330
+ }
331
+ };
332
+ // --- Messages ---
333
+ /**
334
+ * Send a message to an agent. Returns at once with the run id, or, with
335
+ * `wait`, once the run completes, fails, or parks on a question.
336
+ */
337
+ async sendMessage(agent, content, options = {}) {
338
+ const agentId = await this.#resolveAgentId(agent);
339
+ const runId = await this.#post(agentId, content, options);
340
+ if (!options.wait) {
341
+ return {
342
+ runId,
343
+ status: "accepted",
344
+ output: null,
345
+ conversationKey: options.conversationKey,
346
+ isWaiting: false,
347
+ waitingFor: null
348
+ };
349
+ }
350
+ const run = await this.#waitForRun(runId, options.timeoutMs ?? 3e4, options.signal);
351
+ return {
352
+ runId,
353
+ status: run.status,
354
+ output: run.output,
355
+ conversationKey: options.conversationKey,
356
+ isWaiting: run.status === "waiting",
357
+ waitingFor: run.waitingFor
358
+ };
359
+ }
360
+ /**
361
+ * Send a message and iterate the run's events as they happen, until it
362
+ * completes or errors.
363
+ *
364
+ * for await (const event of client.stream("researcher", "Summarise this")) {
365
+ * if (event.type === "completed") console.log(event.data.output);
366
+ * }
367
+ */
368
+ async *stream(agent, content, options = {}) {
369
+ const agentId = await this.#resolveAgentId(agent);
370
+ const runId = await this.#post(agentId, content, options);
371
+ yield* this.#watch(agentId, runId, options);
372
+ }
373
+ // --- Runs ---
374
+ runs = {
375
+ get: async (runId) => {
376
+ const res = await this.#request(
377
+ "GET",
378
+ `/api/v1/runs/${runId}`
379
+ );
380
+ return parseRun(res.data);
381
+ },
382
+ /**
383
+ * The tenant's runs, most recent first. Use it to find runs nobody holds
384
+ * a handle to, above all the ones parked on an `ask_human` question:
385
+ *
386
+ * for (const run of await client.runs.list({ status: "waiting" })) {
387
+ * console.log(run.id, run.waitingFor?.question);
388
+ * }
389
+ *
390
+ * `status` and `agentId` filter the page the server returned, so a
391
+ * narrow filter over a busy tenant may need a larger `limit`.
392
+ */
393
+ list: async (options = {}) => {
394
+ const res = await this.#request("GET", "/api/v1/runs", {
395
+ query: { limit: options.limit ?? 50 }
396
+ });
397
+ let runs = res.data.map(parseRun);
398
+ if (options.status) runs = runs.filter((r) => r.status === options.status);
399
+ if (options.agentId !== void 0) runs = runs.filter((r) => r.agentId === options.agentId);
400
+ return runs;
401
+ },
402
+ events: async (runId) => {
403
+ const res = await this.#request(
404
+ "GET",
405
+ `/api/v1/runs/${runId}/events`
406
+ );
407
+ return res.data.map(parseEvent);
408
+ },
409
+ /**
410
+ * Answer a run parked on an `ask_human` question. Sending the agent a
411
+ * normal message does the same and is usually what a chat client wants;
412
+ * this targets one specific run.
413
+ */
414
+ reply: async (runId, answer) => {
415
+ await this.#request("POST", `/api/v1/runs/${runId}/reply`, { body: { answer } });
416
+ },
417
+ /** Re-run a finished or failed run's message. Returns the new run id. */
418
+ retry: async (runId) => {
419
+ const res = await this.#request("POST", `/api/v1/runs/${runId}/retry`);
420
+ return res.run_id;
421
+ },
422
+ /** Iterate an existing run's events as they happen, until it finishes. */
423
+ watch: (runId, options = {}) => {
424
+ const self = this;
425
+ return (async function* () {
426
+ const run = await self.runs.get(runId);
427
+ yield* self.#watch(run.agentId, runId, options);
428
+ })();
429
+ }
430
+ };
431
+ // --- Conversations ---
432
+ conversations = {
433
+ list: async (agent) => {
434
+ const agentId = await this.#resolveAgentId(agent);
435
+ const res = await this.#request(
436
+ "GET",
437
+ `/api/v1/agents/${agentId}/conversations`
438
+ );
439
+ return res.data.map(parseConversation);
440
+ },
441
+ get: async (agent, key) => {
442
+ const agentId = await this.#resolveAgentId(agent);
443
+ const res = await this.#request(
444
+ "GET",
445
+ `/api/v1/agents/${agentId}/conversations/${encodeURIComponent(key)}`
446
+ );
447
+ return parseConversation(res.data);
448
+ },
449
+ /** Reset a conversation: its history is gone, the next message starts fresh. */
450
+ delete: async (agent, key) => {
451
+ const agentId = await this.#resolveAgentId(agent);
452
+ await this.#request(
453
+ "DELETE",
454
+ `/api/v1/agents/${agentId}/conversations/${encodeURIComponent(key)}`
455
+ );
456
+ }
457
+ };
458
+ // --- Internals ---
459
+ async #resolveAgentId(agent) {
460
+ return typeof agent === "number" ? agent : (await this.agents.get(agent)).id;
461
+ }
462
+ async #post(agentId, content, options) {
463
+ const body = { content };
464
+ if (options.conversationKey !== void 0) body.conversation_key = options.conversationKey;
465
+ if (options.context !== void 0) body.context = options.context;
466
+ if (options.gardId !== void 0) body.gard_id = options.gardId;
467
+ const res = await this.#request(
468
+ "POST",
469
+ `/api/v1/agents/${agentId}/messages`,
470
+ {
471
+ body,
472
+ signal: options.signal
473
+ }
474
+ );
475
+ return res.run_id;
476
+ }
477
+ // Poll until the run stops progressing. "waiting" is not terminal, but
478
+ // the agent is parked on a question and will not move until someone
479
+ // answers, so it ends the wait too.
480
+ async #waitForRun(runId, timeoutMs, signal) {
481
+ const deadline = Date.now() + timeoutMs;
482
+ let interval = 500;
483
+ while (Date.now() < deadline) {
484
+ const run = await this.runs.get(runId);
485
+ if (WAIT_STOPS.has(run.status)) return run;
486
+ await sleep(Math.min(interval, Math.max(deadline - Date.now(), 0)), signal);
487
+ interval = Math.min(interval * 1.5, 3e3);
488
+ }
489
+ throw new NornsTimeoutError(`Run ${runId} did not complete within ${timeoutMs}ms`);
490
+ }
491
+ async *#watch(agentId, runId, options) {
492
+ const idle = options.idleTimeoutMs ?? 12e4;
493
+ const queue = new AsyncQueue();
494
+ const socket = new import_phoenix.Socket(`${wsBase(this.url)}/socket`, {
495
+ params: { token: this.#apiKey },
496
+ ...this.#transport ? { transport: this.#transport } : {},
497
+ ...this.#socketOptions
498
+ });
499
+ let timer;
500
+ const touch = () => {
501
+ if (timer) clearTimeout(timer);
502
+ timer = setTimeout(
503
+ () => queue.fail(new NornsTimeoutError(`No event for ${idle}ms on run ${runId}`)),
504
+ idle
505
+ );
506
+ };
507
+ const onAbort = () => queue.fail(options.signal?.reason ?? abortError());
508
+ socket.onError((err) => queue.fail(new StreamClosedError(err)));
509
+ socket.onClose((ev) => queue.fail(new StreamClosedError(ev)));
510
+ const channel = socket.channel(`agent:${agentId}`, { run_id: runId });
511
+ channel.onMessage = (event, payload) => {
512
+ if (!event.startsWith("phx_") && !event.startsWith("chan_reply_") && event !== "heartbeat") {
513
+ if (payload?.run_id === void 0 || payload.run_id === runId) {
514
+ queue.push({ type: event, data: payload });
515
+ touch();
516
+ }
517
+ }
518
+ return payload;
519
+ };
520
+ channel.join().receive(
521
+ "error",
522
+ (resp) => queue.fail(new NornsError(`Could not join agent:${agentId}: ${JSON.stringify(resp)}`))
523
+ ).receive(
524
+ "timeout",
525
+ () => queue.fail(new NornsTimeoutError(`Joining agent:${agentId} timed out`))
526
+ );
527
+ if (options.signal?.aborted) onAbort();
528
+ options.signal?.addEventListener("abort", onAbort, { once: true });
529
+ socket.connect();
530
+ touch();
531
+ try {
532
+ for await (const event of queue) {
533
+ yield event;
534
+ if (TERMINAL_EVENTS.has(event.type)) return;
535
+ }
536
+ } finally {
537
+ if (timer) clearTimeout(timer);
538
+ options.signal?.removeEventListener("abort", onAbort);
539
+ queue.close();
540
+ socket.disconnect();
541
+ }
542
+ }
543
+ async #request(method, path, options = {}) {
544
+ const url = new URL(this.url + path);
545
+ for (const [k, v] of Object.entries(options.query ?? {})) url.searchParams.set(k, String(v));
546
+ const headers = {
547
+ authorization: `Bearer ${this.#apiKey}`,
548
+ accept: "application/json"
549
+ };
550
+ if (options.body !== void 0) headers["content-type"] = "application/json";
551
+ const res = await this.#fetch(url, {
552
+ method,
553
+ headers,
554
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
555
+ signal: options.signal
556
+ });
557
+ const text = await res.text();
558
+ const json = text === "" ? void 0 : safeParse(text);
559
+ if (!res.ok) {
560
+ if (res.status === 404) throw new NotFoundError(`${method} ${path}`, json ?? text);
561
+ throw new NornsApiError(res.status, json ?? text);
562
+ }
563
+ return json;
564
+ }
565
+ };
566
+ function resolveUrl(url) {
567
+ const value = url ?? process.env.NORNS_URL;
568
+ if (!value) throw new NornsError("Norns URL missing: pass `url` or set NORNS_URL");
569
+ return value.replace(/\/+$/, "");
570
+ }
571
+ function resolveApiKey(apiKey) {
572
+ const value = apiKey ?? process.env.NORNS_API_KEY;
573
+ if (!value) throw new NornsError("Norns API key missing: pass `apiKey` or set NORNS_API_KEY");
574
+ return value;
575
+ }
576
+ function wsBase(url) {
577
+ return url.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
578
+ }
579
+ function safeParse(text) {
580
+ try {
581
+ return JSON.parse(text);
582
+ } catch {
583
+ return text;
584
+ }
585
+ }
586
+ function abortError() {
587
+ return new DOMException("The operation was aborted", "AbortError");
588
+ }
589
+ function sleep(ms, signal) {
590
+ return new Promise((resolve, reject) => {
591
+ if (signal?.aborted) return reject(signal.reason ?? abortError());
592
+ const timer = setTimeout(() => {
593
+ signal?.removeEventListener("abort", onAbort);
594
+ resolve();
595
+ }, ms);
596
+ function onAbort() {
597
+ clearTimeout(timer);
598
+ reject(signal?.reason ?? abortError());
599
+ }
600
+ signal?.addEventListener("abort", onAbort, { once: true });
601
+ });
602
+ }
603
+ function parseWaitingFor(data) {
604
+ if (!data || typeof data !== "object") return null;
605
+ const w = data;
606
+ return {
607
+ question: String(w.question ?? ""),
608
+ toolCallId: String(w.tool_call_id ?? ""),
609
+ askedAt: typeof w.asked_at === "string" ? w.asked_at : void 0
610
+ };
611
+ }
612
+ function parseRun(r) {
613
+ return {
614
+ id: r.id,
615
+ agentId: r.agent_id,
616
+ conversationId: r.conversation_id ?? null,
617
+ parentRunId: r.parent_run_id ?? null,
618
+ gardId: r.gard_id ?? null,
619
+ status: r.status,
620
+ triggerType: r.trigger_type ?? "message",
621
+ input: r.input ?? {},
622
+ output: r.output ?? null,
623
+ waitingFor: parseWaitingFor(r.waiting_for),
624
+ failureMetadata: r.failure_metadata ?? {},
625
+ inputTokens: r.input_tokens ?? 0,
626
+ outputTokens: r.output_tokens ?? 0,
627
+ cost: r.cost ?? null,
628
+ insertedAt: r.inserted_at,
629
+ updatedAt: r.updated_at ?? r.inserted_at
630
+ };
631
+ }
632
+ function parseEvent(e) {
633
+ return {
634
+ id: e.id,
635
+ sequence: e.sequence,
636
+ eventType: e.event_type,
637
+ payload: e.payload ?? {},
638
+ source: e.source ?? "",
639
+ insertedAt: e.inserted_at
640
+ };
641
+ }
642
+ function parseAgent(a) {
643
+ return {
644
+ id: a.id,
645
+ name: a.name,
646
+ purpose: a.purpose ?? null,
647
+ status: a.status ?? "active",
648
+ model: a.model ?? "",
649
+ systemPrompt: a.system_prompt ?? "",
650
+ maxSteps: a.max_steps ?? 50,
651
+ contextStrategy: a.context_strategy ?? "sliding_window",
652
+ contextWindow: a.context_window ?? 20,
653
+ archivedAt: a.archived_at ?? null,
654
+ insertedAt: a.inserted_at,
655
+ updatedAt: a.updated_at ?? a.inserted_at
656
+ };
657
+ }
658
+ function parseConversation(c) {
659
+ return {
660
+ id: c.id,
661
+ agentId: c.agent_id,
662
+ key: c.key,
663
+ summary: c.summary ?? null,
664
+ messageCount: c.message_count ?? 0,
665
+ tokenEstimate: c.token_estimate ?? 0,
666
+ insertedAt: c.inserted_at,
667
+ updatedAt: c.updated_at ?? c.inserted_at
668
+ };
669
+ }
670
+
671
+ // src/logger.ts
672
+ var consoleLogger = {
673
+ debug() {
674
+ },
675
+ info: (message, ...rest) => console.log(`[norns] ${message}`, ...rest),
676
+ warn: (message, ...rest) => console.warn(`[norns] ${message}`, ...rest),
677
+ error: (message, ...rest) => console.error(`[norns] ${message}`, ...rest)
678
+ };
679
+ var silentLogger = {
680
+ debug() {
681
+ },
682
+ info() {
683
+ },
684
+ warn() {
685
+ },
686
+ error() {
687
+ }
688
+ };
689
+ function resolveLogger(logger) {
690
+ if (logger === null) return silentLogger;
691
+ return logger ?? consoleLogger;
692
+ }
693
+
694
+ // src/models.ts
695
+ var PROVIDER_PACKAGES = {
696
+ anthropic: "@ai-sdk/anthropic",
697
+ openai: "@ai-sdk/openai",
698
+ google: "@ai-sdk/google",
699
+ mistral: "@ai-sdk/mistral"
700
+ };
701
+ function inferProvider(model) {
702
+ const sep = model.search(/[/:]/);
703
+ if (sep > 0) {
704
+ const provider = model.slice(0, sep);
705
+ if (provider in PROVIDER_PACKAGES || /^[a-z][a-z0-9-]*$/.test(provider)) {
706
+ return { provider, modelId: model.slice(sep + 1) };
707
+ }
708
+ }
709
+ const lower = model.toLowerCase();
710
+ if (lower.startsWith("claude")) return { provider: "anthropic", modelId: model };
711
+ if (/^(gpt|o[1-9]|chatgpt|text-)/.test(lower)) return { provider: "openai", modelId: model };
712
+ if (lower.startsWith("gemini")) return { provider: "google", modelId: model };
713
+ if (/^(mistral|codestral|ministral|pixtral|magistral)/.test(lower)) {
714
+ return { provider: "mistral", modelId: model };
715
+ }
716
+ return { provider: "anthropic", modelId: model };
717
+ }
718
+ function defaultModelResolver() {
719
+ const cache = /* @__PURE__ */ new Map();
720
+ return (model) => {
721
+ let pending = cache.get(model);
722
+ if (!pending) {
723
+ pending = loadModel(model);
724
+ cache.set(model, pending);
725
+ pending.catch(() => cache.delete(model));
726
+ }
727
+ return pending;
728
+ };
729
+ }
730
+ async function loadModel(model) {
731
+ const { provider, modelId } = inferProvider(model);
732
+ const pkg = PROVIDER_PACKAGES[provider] ?? `@ai-sdk/${provider}`;
733
+ let mod;
734
+ try {
735
+ mod = await import(pkg);
736
+ } catch (err) {
737
+ throw new Error(
738
+ `No provider for model '${model}': install ${pkg} (npm install ${pkg}), or pass \`models\` to the worker to resolve models yourself`,
739
+ { cause: err }
740
+ );
741
+ }
742
+ const instance = findProvider(mod, provider);
743
+ if (!instance) {
744
+ throw new Error(`${pkg} exports no provider instance; pass \`models\` to the worker`);
745
+ }
746
+ return instance(modelId);
747
+ }
748
+ function findProvider(mod, provider) {
749
+ const named = mod[provider];
750
+ if (typeof named === "function") return named;
751
+ for (const value of Object.values(mod)) {
752
+ if (typeof value === "function" && "languageModel" in value) return value;
753
+ }
754
+ return void 0;
755
+ }
756
+ function toResolver(models) {
757
+ if (models === void 0) return defaultModelResolver();
758
+ if (typeof models === "function") return models;
759
+ return (id) => models.languageModel(id);
760
+ }
761
+
762
+ // src/tool.ts
763
+ var import_ai = require("ai");
764
+ var EMPTY_SCHEMA = { type: "object", properties: {} };
765
+ function tool(options) {
766
+ const name = options.name?.trim();
767
+ if (!name) throw new Error("tool(): name is required");
768
+ if (!options.description?.trim()) throw new Error(`tool('${name}'): description is required`);
769
+ if (typeof options.execute !== "function") {
770
+ throw new Error(`tool('${name}'): execute must be a function`);
771
+ }
772
+ let inputSchema = EMPTY_SCHEMA;
773
+ let validate;
774
+ if (options.input !== void 0) {
775
+ const schema = (0, import_ai.asSchema)(toFlexible(options.input));
776
+ const json = schema.jsonSchema;
777
+ if (isPromiseLike(json)) {
778
+ throw new Error(`tool('${name}'): input schema must resolve synchronously`);
779
+ }
780
+ inputSchema = stripMeta(json);
781
+ if (schema.validate) {
782
+ const run = schema.validate;
783
+ validate = async (value) => {
784
+ const result = await run(value);
785
+ return result.success ? { success: true, value: result.value } : { success: false, error: result.error };
786
+ };
787
+ }
788
+ }
789
+ const execute = options.execute;
790
+ return Object.freeze({
791
+ name,
792
+ description: options.description.trim(),
793
+ inputSchema,
794
+ sideEffect: options.sideEffect ?? false,
795
+ validate,
796
+ execute: async (input, context) => execute(input, context)
797
+ });
798
+ }
799
+ function toolRegistration(t) {
800
+ return {
801
+ name: t.name,
802
+ description: t.description,
803
+ input_schema: t.inputSchema,
804
+ side_effect: t.sideEffect
805
+ };
806
+ }
807
+ function stringifyToolOutput(value) {
808
+ if (value === void 0 || value === null) return "";
809
+ if (typeof value === "string") return value;
810
+ return JSON.stringify(value);
811
+ }
812
+ function toFlexible(input) {
813
+ if (isRawJsonSchema(input)) return (0, import_ai.jsonSchema)(input);
814
+ return input;
815
+ }
816
+ function isRawJsonSchema(value) {
817
+ if (typeof value !== "object" || value === null) return false;
818
+ if ("~standard" in value || "_zod" in value || "_def" in value || "jsonSchema" in value) {
819
+ return false;
820
+ }
821
+ return true;
822
+ }
823
+ function isPromiseLike(value) {
824
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
825
+ }
826
+ function stripMeta(schema) {
827
+ const { $schema: _ignored, ...rest } = schema;
828
+ return rest;
829
+ }
830
+
831
+ // src/worker.ts
832
+ var import_node_crypto = require("crypto");
833
+ var import_ai3 = require("ai");
834
+ var import_phoenix2 = require("phoenix");
835
+
836
+ // src/completed-calls.ts
837
+ var import_node_fs = require("fs");
838
+ var import_node_os = require("os");
839
+ var import_node_path = require("path");
840
+ var MAX_REMEMBERED_RESULTS = 512;
841
+ var CompletedCalls = class {
842
+ #path;
843
+ #limit;
844
+ #entries = /* @__PURE__ */ new Map();
845
+ #logger;
846
+ constructor(path, logger, limit = MAX_REMEMBERED_RESULTS) {
847
+ this.#path = path;
848
+ this.#limit = limit;
849
+ this.#logger = logger;
850
+ this.#load();
851
+ }
852
+ get path() {
853
+ return this.#path;
854
+ }
855
+ get size() {
856
+ return this.#entries.size;
857
+ }
858
+ has(key) {
859
+ return this.#entries.has(key);
860
+ }
861
+ get(key) {
862
+ const entry = this.#entries.get(key);
863
+ return entry ? { ...entry } : void 0;
864
+ }
865
+ add(key, result) {
866
+ this.#entries.delete(key);
867
+ this.#entries.set(key, result);
868
+ while (this.#entries.size > this.#limit) {
869
+ const oldest = this.#entries.keys().next().value;
870
+ if (oldest === void 0) break;
871
+ this.#entries.delete(oldest);
872
+ }
873
+ this.#save();
874
+ }
875
+ #load() {
876
+ if (!this.#path) return;
877
+ let raw;
878
+ try {
879
+ raw = (0, import_node_fs.readFileSync)(this.#path, "utf8");
880
+ } catch (err) {
881
+ if (err.code !== "ENOENT") {
882
+ this.#logger.warn(
883
+ `Could not read ${this.#path} (${err.message}); starting with no memory of completed calls`
884
+ );
885
+ }
886
+ return;
887
+ }
888
+ try {
889
+ const data = JSON.parse(raw);
890
+ for (const [key, value] of Object.entries(data)) {
891
+ if (value && typeof value === "object") this.#entries.set(key, value);
892
+ }
893
+ } catch (err) {
894
+ this.#logger.warn(
895
+ `Could not parse ${this.#path} (${err.message}); starting with no memory of completed calls`
896
+ );
897
+ }
898
+ }
899
+ #save() {
900
+ if (!this.#path) return;
901
+ try {
902
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(this.#path), { recursive: true });
903
+ const tmp = `${this.#path}.tmp`;
904
+ (0, import_node_fs.writeFileSync)(tmp, JSON.stringify(Object.fromEntries(this.#entries)));
905
+ (0, import_node_fs.renameSync)(tmp, this.#path);
906
+ } catch (err) {
907
+ this.#logger.warn(
908
+ `Could not write ${this.#path} (${err.message}); completed calls will not survive a restart`
909
+ );
910
+ this.#path = void 0;
911
+ }
912
+ }
913
+ };
914
+ function completedCallsPath(workerId, stateDir) {
915
+ const base = stateDir === void 0 ? process.env.NORNS_STATE_DIR : stateDir;
916
+ if (base === "" || base === null) return void 0;
917
+ const root = base ?? (0, import_node_path.join)((0, import_node_os.tmpdir)(), "norns-worker-state");
918
+ const safe = workerId.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 120);
919
+ return (0, import_node_path.join)(root, `${safe}.completed.json`);
920
+ }
921
+
922
+ // src/format.ts
923
+ var import_ai2 = require("ai");
924
+ function composeSystemPrompt(task) {
925
+ let prompt = task.system_prompt ?? "";
926
+ if (typeof task.summary === "string" && task.summary !== "") {
927
+ prompt += `
928
+
929
+ Summary of earlier conversation: ${task.summary}`;
930
+ }
931
+ if (task.date) prompt += `
932
+
933
+ Current date: ${task.date}.`;
934
+ return prompt;
935
+ }
936
+ function composeCompactionPrompt(task) {
937
+ let prompt = task.system_prompt ?? "";
938
+ if (typeof task.summary === "string" && task.summary !== "") {
939
+ prompt += `
940
+
941
+ Summary of earlier conversation: ${task.summary}`;
942
+ }
943
+ return prompt;
944
+ }
945
+ var COMPACTION_INSTRUCTION = "Write a summary of the conversation so far for your own future reference. You will continue the same task with only this summary and the most recent messages, so keep every fact you would need: the task and its constraints, decisions made and why, files and identifiers touched, what was tried and failed, what remains to be done, and anything the user asked for that is not finished. Fold in the earlier summary if there is one. Write prose or terse notes, no preamble, no commentary about summarising.";
946
+ function compactionMessages(task) {
947
+ return [
948
+ ...renderMessages(task.messages ?? []),
949
+ { role: "user", content: COMPACTION_INSTRUCTION }
950
+ ];
951
+ }
952
+ function renderMessage(msg) {
953
+ if (!msg.kind) return msg;
954
+ const { kind, data, ...rest } = msg;
955
+ return { ...rest, content: renderKind(kind, data ?? {}, msg.content) };
956
+ }
957
+ function renderMessages(messages) {
958
+ return messages.map(renderMessage);
959
+ }
960
+ function renderKind(kind, data, content) {
961
+ switch (kind) {
962
+ case "inherited_context":
963
+ return `[Inherited context from parent agent]
964
+ ${encode(content)}`;
965
+ case "timer_completed":
966
+ return "Timer completed.";
967
+ case "tool_denied":
968
+ return `Tool '${data.tool_name}' is not in this agent's allowed tools.`;
969
+ case "subagent_denied":
970
+ if (data.reason === "disabled") return "This agent is not permitted to launch sub-agents.";
971
+ if (data.reason === "max_depth") {
972
+ return `Sub-agent nesting limit reached (max depth ${data.max_depth}). Do the work in this agent instead of delegating further.`;
973
+ }
974
+ return `Agent '${data.agent_name}' is not in this agent's allowed sub-agents.`;
975
+ case "subagent_list_denied":
976
+ return "Listing agents is not permitted for this agent.";
977
+ case "subagent_not_found":
978
+ return `Agent '${data.agent_name}' not found`;
979
+ case "subagent_self":
980
+ return "Cannot launch self as a sub-agent";
981
+ case "subagent_missing":
982
+ return `Sub-agent run ${data.run_id} no longer exists, so its result cannot be recovered.`;
983
+ case "subagent_busy":
984
+ return `Agent '${data.agent_name}' is still working on an earlier task for this conversation. Wait for its result before giving it another.`;
985
+ case "subagent_launch_failed":
986
+ return `Failed to launch agent '${data.agent_name}': ${data.reason}`;
987
+ case "subagent_completed":
988
+ return JSON.stringify({ run_id: data.run_id, status: "completed", output: content ?? "" });
989
+ case "subagent_failed":
990
+ return JSON.stringify({ run_id: data.run_id, status: "failed", error: content ?? "" });
991
+ case "list_agents":
992
+ return JSON.stringify(data.agents ?? []);
993
+ default:
994
+ return content === void 0 || content === null || content === "" ? encode(data) : encode(content);
995
+ }
996
+ }
997
+ function encode(value) {
998
+ return typeof value === "string" ? value : JSON.stringify(value ?? null);
999
+ }
1000
+ var TOOL_RESULT_CAP = 200;
1001
+ function messagesForTask(task) {
1002
+ const rendered = renderMessages(task.messages ?? []);
1003
+ return task.context_policy ? rendered : elideOldToolResults(rendered);
1004
+ }
1005
+ function elideOldToolResults(messages) {
1006
+ if (messages.length <= 4) return messages;
1007
+ const old = messages.slice(0, -2);
1008
+ const recent = messages.slice(-2);
1009
+ return [
1010
+ ...old.map(
1011
+ (msg) => msg.role === "tool" && typeof msg.content === "string" && msg.content.length > TOOL_RESULT_CAP ? { ...msg, content: `${msg.content.slice(0, TOOL_RESULT_CAP)}...(truncated)` } : msg
1012
+ ),
1013
+ ...recent
1014
+ ];
1015
+ }
1016
+ var DEFAULT_MAX_TOKENS = 8192;
1017
+ function maxTokens(task) {
1018
+ const v = task.max_tokens;
1019
+ return typeof v === "number" && Number.isInteger(v) && v > 0 ? v : DEFAULT_MAX_TOKENS;
1020
+ }
1021
+ function finalOutput(messages, content) {
1022
+ const text = typeof content === "string" ? content : "";
1023
+ if (text.trim() !== "") return text;
1024
+ for (let i = messages.length - 1; i >= 0; i--) {
1025
+ const m = messages[i];
1026
+ if (m?.role === "assistant" && typeof m.content === "string" && m.content.trim() !== "") {
1027
+ return m.content;
1028
+ }
1029
+ }
1030
+ return text;
1031
+ }
1032
+ var CACHE_CONTROL = { anthropic: { cacheControl: { type: "ephemeral" } } };
1033
+ function toInstructions(system) {
1034
+ if (system === "") return void 0;
1035
+ return { role: "system", content: system, providerOptions: CACHE_CONTROL };
1036
+ }
1037
+ function toModelMessages(messages) {
1038
+ const out = [];
1039
+ for (const msg of messages) {
1040
+ const converted = toModelMessage(msg);
1041
+ const last2 = out[out.length - 1];
1042
+ if (converted.role === "tool" && last2?.role === "tool") {
1043
+ last2.content = [...last2.content, ...converted.content];
1044
+ } else {
1045
+ out.push(converted);
1046
+ }
1047
+ }
1048
+ const last = out[out.length - 1];
1049
+ if (last) last.providerOptions = CACHE_CONTROL;
1050
+ return out;
1051
+ }
1052
+ function toModelMessage(msg) {
1053
+ switch (msg.role) {
1054
+ case "system":
1055
+ return { role: "system", content: encode(msg.content ?? "") };
1056
+ case "assistant": {
1057
+ const text = typeof msg.content === "string" ? msg.content : encode(msg.content ?? "");
1058
+ const calls = msg.tool_calls ?? [];
1059
+ if (calls.length === 0) return { role: "assistant", content: text };
1060
+ return {
1061
+ role: "assistant",
1062
+ content: [
1063
+ ...text !== "" ? [{ type: "text", text }] : [],
1064
+ ...calls.map((tc) => ({
1065
+ type: "tool-call",
1066
+ toolCallId: tc.id,
1067
+ toolName: tc.name,
1068
+ input: parseArguments(tc.arguments)
1069
+ }))
1070
+ ]
1071
+ };
1072
+ }
1073
+ case "tool": {
1074
+ const value = encode(msg.content ?? "");
1075
+ return {
1076
+ role: "tool",
1077
+ content: [
1078
+ {
1079
+ type: "tool-result",
1080
+ toolCallId: msg.tool_call_id ?? "",
1081
+ toolName: msg.name ?? "",
1082
+ output: msg.is_error ? { type: "error-text", value } : { type: "text", value }
1083
+ }
1084
+ ]
1085
+ };
1086
+ }
1087
+ default:
1088
+ return { role: "user", content: encode(msg.content ?? "") };
1089
+ }
1090
+ }
1091
+ function parseArguments(args) {
1092
+ if (typeof args !== "string") return args ?? {};
1093
+ try {
1094
+ return JSON.parse(args);
1095
+ } catch {
1096
+ return { raw: args };
1097
+ }
1098
+ }
1099
+ function toToolSet(tools) {
1100
+ const set = {};
1101
+ for (const t of tools) {
1102
+ set[t.name] = (0, import_ai2.tool)({
1103
+ description: t.description ?? "",
1104
+ inputSchema: (0, import_ai2.jsonSchema)(t.parameters ?? t.input_schema ?? { type: "object", properties: {} })
1105
+ });
1106
+ }
1107
+ return set;
1108
+ }
1109
+ function fromGenerateResult(result) {
1110
+ const out = {
1111
+ status: "ok",
1112
+ content: result.text ?? "",
1113
+ finish_reason: neutralFinishReason(result.finishReason, result.toolCalls.length > 0),
1114
+ usage: neutralUsage(result.usage)
1115
+ };
1116
+ if (result.response?.modelId) out.model = result.response.modelId;
1117
+ if (result.toolCalls.length > 0) {
1118
+ out.tool_calls = result.toolCalls.map((tc) => ({
1119
+ id: tc.toolCallId,
1120
+ name: tc.toolName,
1121
+ arguments: tc.input ?? {}
1122
+ }));
1123
+ }
1124
+ return out;
1125
+ }
1126
+ function neutralFinishReason(reason, hasToolCalls) {
1127
+ if (hasToolCalls) return "tool_call";
1128
+ switch (reason) {
1129
+ case "tool-calls":
1130
+ return "tool_call";
1131
+ case "length":
1132
+ return "length";
1133
+ case "error":
1134
+ return "error";
1135
+ default:
1136
+ return "stop";
1137
+ }
1138
+ }
1139
+ function neutralUsage(usage) {
1140
+ const details = usage?.inputTokenDetails;
1141
+ const read = details?.cacheReadTokens ?? 0;
1142
+ const write = details?.cacheWriteTokens ?? 0;
1143
+ const total = usage?.inputTokens ?? 0;
1144
+ const noCache = details?.noCacheTokens;
1145
+ const input = noCache !== void 0 ? Math.max(total, noCache + read + write) : total;
1146
+ const out = { input_tokens: input, output_tokens: usage?.outputTokens ?? 0 };
1147
+ if (read > 0) out.cache_read_tokens = read;
1148
+ if (write > 0) out.cache_write_tokens = write;
1149
+ return out;
1150
+ }
1151
+
1152
+ // src/history-cache.ts
1153
+ var HistoryCache = class {
1154
+ #runs = /* @__PURE__ */ new Map();
1155
+ #maxRuns;
1156
+ constructor(maxRuns = 256) {
1157
+ this.#maxRuns = maxRuns;
1158
+ }
1159
+ /** The task with `messages` filled in, or `undefined` on a miss. */
1160
+ resolve(task) {
1161
+ if (task.purpose === "compact") return task;
1162
+ const runId = task.run_id;
1163
+ const delta = task.messages_delta;
1164
+ if (delta === void 0) {
1165
+ if (runId !== void 0 && Array.isArray(task.messages)) this.#remember(runId, task.messages);
1166
+ return task;
1167
+ }
1168
+ const previous = runId === void 0 ? void 0 : this.#runs.get(runId);
1169
+ if (runId === void 0 || previous === void 0 || previous.length !== delta.base_count) {
1170
+ return void 0;
1171
+ }
1172
+ const messages = [...previous.slice(delta.drop), ...delta.append];
1173
+ this.#remember(runId, messages);
1174
+ const resolved = { ...task, messages };
1175
+ delete resolved.messages_delta;
1176
+ return resolved;
1177
+ }
1178
+ /** Forget everything: a new connection gets the full history anyway. */
1179
+ clear() {
1180
+ this.#runs.clear();
1181
+ }
1182
+ /** How many runs are held; for tests. */
1183
+ get size() {
1184
+ return this.#runs.size;
1185
+ }
1186
+ #remember(runId, messages) {
1187
+ this.#runs.delete(runId);
1188
+ this.#runs.set(runId, messages);
1189
+ while (this.#runs.size > this.#maxRuns) {
1190
+ const oldest = this.#runs.keys().next().value;
1191
+ if (oldest === void 0) break;
1192
+ this.#runs.delete(oldest);
1193
+ }
1194
+ }
1195
+ };
1196
+
1197
+ // src/worker.ts
1198
+ var TOPIC = "worker:lobby";
1199
+ var NornsWorker = class {
1200
+ url;
1201
+ workerId;
1202
+ gard;
1203
+ #apiKey;
1204
+ #agents;
1205
+ #tools;
1206
+ #capabilities;
1207
+ #claimToken;
1208
+ #shutdownTimeoutMs;
1209
+ #handleSignals;
1210
+ #logger;
1211
+ #stateDir;
1212
+ #transport;
1213
+ #socketOptions;
1214
+ #client;
1215
+ #resolveModel;
1216
+ #inFlight = /* @__PURE__ */ new Set();
1217
+ #history = new HistoryCache();
1218
+ #state = "idle";
1219
+ #socket;
1220
+ #channel;
1221
+ #completed;
1222
+ #starting;
1223
+ #signalHandler;
1224
+ #stopped;
1225
+ #settleStopped;
1226
+ constructor(options) {
1227
+ this.url = resolveUrl(options.url);
1228
+ this.#apiKey = resolveApiKey(options.apiKey);
1229
+ this.#agents = agentList(options.agents);
1230
+ this.#tools = toolsByName(this.#agents);
1231
+ this.#capabilities = options.capabilities ?? ["llm", "tools"];
1232
+ if (this.#capabilities.length === 0)
1233
+ throw new NornsError("A worker needs at least one capability");
1234
+ this.#logger = resolveLogger(options.logger);
1235
+ this.#stateDir = options.stateDir;
1236
+ this.#transport = options.transport;
1237
+ this.#socketOptions = options.socketOptions ?? {};
1238
+ this.#handleSignals = options.handleSignals ?? true;
1239
+ this.#resolveModel = toResolver(options.models);
1240
+ this.workerId = options.workerId ?? process.env.NORNS_WORKER_ID ?? `ts-worker-${(0, import_node_crypto.randomBytes)(4).toString("hex")}`;
1241
+ this.gard = options.gard ?? process.env.NORNS_GARD;
1242
+ this.#claimToken = options.claimToken ?? process.env.NORNS_GARD_CLAIM_TOKEN;
1243
+ const envTimeout = Number(process.env.NORNS_SHUTDOWN_TIMEOUT);
1244
+ this.#shutdownTimeoutMs = options.shutdownTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout * 1e3 : 3e4);
1245
+ this.#client = new NornsClient({ url: this.url, apiKey: this.#apiKey, fetch: options.fetch });
1246
+ this.#stopped = new Promise((resolve, reject) => {
1247
+ this.#settleStopped = { resolve, reject };
1248
+ });
1249
+ this.#stopped.catch(() => {
1250
+ });
1251
+ }
1252
+ /** The agents this worker serves. */
1253
+ get agents() {
1254
+ return this.#agents;
1255
+ }
1256
+ get state() {
1257
+ return this.#state;
1258
+ }
1259
+ /**
1260
+ * Settles once the worker has stopped: resolves after a drain, rejects
1261
+ * with `JoinError` or `GardDestroyedError` when it was forced off.
1262
+ */
1263
+ get stopped() {
1264
+ return this.#stopped;
1265
+ }
1266
+ /**
1267
+ * Register the agents, connect, and join. Resolves once Norns has accepted
1268
+ * the worker; rejects with `JoinError` when it never will (bad claim
1269
+ * token, destroyed gard). A server that is down is retried until it is up.
1270
+ */
1271
+ start() {
1272
+ if (this.#state !== "idle") throw new NornsError("start() may only be called once");
1273
+ this.#state = "starting";
1274
+ this.#starting = this.#start().then(
1275
+ () => {
1276
+ if (this.#state === "starting") this.#state = "running";
1277
+ },
1278
+ (err) => {
1279
+ this.#fail(err);
1280
+ throw err;
1281
+ }
1282
+ );
1283
+ return this.#starting;
1284
+ }
1285
+ async #start() {
1286
+ for (const agent of this.#agents) {
1287
+ const info = await this.#client.agents.ensure(agent);
1288
+ this.#logger.info(`Registered agent '${agent.name}' (id=${info.id})`);
1289
+ }
1290
+ if (this.#capabilities.includes("llm")) {
1291
+ for (const model of new Set(this.#agents.map((a) => a.model))) {
1292
+ await this.#resolveModel(model);
1293
+ }
1294
+ }
1295
+ this.#completed = new CompletedCalls(
1296
+ completedCallsPath(this.workerId, this.#stateDir),
1297
+ this.#logger
1298
+ );
1299
+ await this.#connect();
1300
+ this.#installSignalHandlers();
1301
+ }
1302
+ #connect() {
1303
+ const socket = new import_phoenix2.Socket(`${wsBase(this.url)}/worker`, {
1304
+ params: { token: this.#apiKey },
1305
+ logger: (kind, msg, data) => this.#logger.debug(`phoenix ${kind}: ${msg}`, data),
1306
+ ...this.#transport ? { transport: this.#transport } : {},
1307
+ ...this.#socketOptions
1308
+ });
1309
+ this.#socket = socket;
1310
+ socket.onOpen(() => {
1311
+ this.#logger.info(`Connected to ${this.url}`);
1312
+ this.#history.clear();
1313
+ });
1314
+ socket.onError((err) => {
1315
+ if (this.#state === "starting" || this.#state === "running") {
1316
+ this.#logger.warn(`Connection error (${errorMessage(err)}); retrying`);
1317
+ }
1318
+ });
1319
+ socket.onClose(() => {
1320
+ if (this.#state === "running") {
1321
+ this.#logger.warn("Disconnected from Norns; reconnecting");
1322
+ this.#abortInFlight("socket closed");
1323
+ }
1324
+ });
1325
+ const channel = socket.channel(TOPIC, this.#joinParams());
1326
+ this.#channel = channel;
1327
+ channel.on(
1328
+ "llm_task",
1329
+ (task) => this.#spawn(task.task_id, (s) => this.#runLlm(task, s))
1330
+ );
1331
+ channel.on(
1332
+ "tool_task",
1333
+ (task) => this.#spawn(task.task_id, (s) => this.#runTool(task, s))
1334
+ );
1335
+ channel.on("gard_destroyed", () => {
1336
+ this.#logger.error("This worker's gard was destroyed; shutting down");
1337
+ this.#fail(new GardDestroyedError(this.gard));
1338
+ });
1339
+ channel.onError((reason) => {
1340
+ if (this.#state === "running")
1341
+ this.#logger.warn(`Channel error (${errorMessage(reason)}); rejoining`);
1342
+ });
1343
+ return new Promise((resolve, reject) => {
1344
+ let joined = false;
1345
+ channel.join().receive("ok", () => {
1346
+ if (joined) {
1347
+ this.#logger.info(`Worker ${this.workerId} rejoined`);
1348
+ return;
1349
+ }
1350
+ joined = true;
1351
+ this.#logger.info(
1352
+ this.gard !== void 0 ? `Worker ${this.workerId} ready (gard ${this.gard})` : `Worker ${this.workerId} ready`
1353
+ );
1354
+ resolve();
1355
+ }).receive("error", (resp) => {
1356
+ const reason = resp?.reason ?? "join rejected";
1357
+ if (reason === "already_claimed") {
1358
+ this.#logger.warn("Gard claim rejected: already_claimed; retrying");
1359
+ return;
1360
+ }
1361
+ const err = new JoinError(reason, resp?.code);
1362
+ this.#fail(err);
1363
+ reject(err);
1364
+ }).receive("timeout", () => this.#logger.warn("Join timed out; retrying"));
1365
+ socket.connect();
1366
+ });
1367
+ }
1368
+ #joinParams() {
1369
+ const params = {
1370
+ worker_id: this.workerId,
1371
+ tools: [...this.#tools.values()].map(toolRegistration),
1372
+ capabilities: this.#capabilities,
1373
+ // This worker keeps the history it was last sent per run, so Norns may
1374
+ // send later steps as the change only (HistoryCache).
1375
+ features: ["message_cache"]
1376
+ };
1377
+ if (this.gard !== void 0) {
1378
+ params.gard = this.gard;
1379
+ params.claim_token = this.#claimToken ?? null;
1380
+ }
1381
+ return params;
1382
+ }
1383
+ /**
1384
+ * Drain and stop: tell Norns to send no more work, finish the tasks in
1385
+ * flight (up to the shutdown timeout), report them, leave. Tasks still
1386
+ * running at the deadline are dropped and Norns re-dispatches them.
1387
+ */
1388
+ async stop() {
1389
+ if (this.#state === "idle") {
1390
+ this.#state = "stopped";
1391
+ this.#settleStopped.resolve();
1392
+ return;
1393
+ }
1394
+ if (this.#state === "starting") {
1395
+ try {
1396
+ await this.#starting;
1397
+ } catch {
1398
+ return;
1399
+ }
1400
+ }
1401
+ if (this.#state !== "running") return this.#stopped;
1402
+ this.#state = "draining";
1403
+ const channel = this.#channel;
1404
+ const socket = this.#socket;
1405
+ this.#logger.info(
1406
+ `Draining: ${this.#inFlight.size} in-flight task(s), up to ${this.#shutdownTimeoutMs / 1e3}s`
1407
+ );
1408
+ if (channel) {
1409
+ await new Promise((resolve) => {
1410
+ channel.push("drain", {}, 5e3).receive("ok", () => resolve()).receive("error", (resp) => {
1411
+ this.#logger.warn(
1412
+ `Could not send drain (${errorMessage(resp)}); Norns will re-dispatch on disconnect`
1413
+ );
1414
+ resolve();
1415
+ }).receive("timeout", () => resolve());
1416
+ });
1417
+ }
1418
+ const deadline = Date.now() + this.#shutdownTimeoutMs;
1419
+ while (this.#inFlight.size > 0 && Date.now() < deadline) {
1420
+ await Promise.race([
1421
+ Promise.allSettled([...this.#inFlight].map((t) => t.done)),
1422
+ sleep(deadline - Date.now())
1423
+ ]);
1424
+ }
1425
+ if (this.#inFlight.size > 0) {
1426
+ this.#logger.warn(
1427
+ `${this.#inFlight.size} task(s) still running at the deadline; dropping them, Norns will re-dispatch`
1428
+ );
1429
+ this.#abortInFlight("shutdown deadline");
1430
+ } else {
1431
+ this.#logger.info("Drained; leaving");
1432
+ }
1433
+ if (channel) {
1434
+ await new Promise((resolve) => {
1435
+ channel.leave(5e3).receive("ok", () => resolve()).receive("timeout", () => resolve());
1436
+ });
1437
+ }
1438
+ if (socket) await new Promise((resolve) => socket.disconnect(() => resolve()));
1439
+ this.#teardown();
1440
+ this.#state = "stopped";
1441
+ this.#logger.info("Worker stopped.");
1442
+ this.#settleStopped.resolve();
1443
+ }
1444
+ /**
1445
+ * Expose a service port for the dashboard. Only in a gard: the gard is
1446
+ * inferred from the connection. Call it from a tool after starting a
1447
+ * service.
1448
+ */
1449
+ async registerPort(internalPort, options = {}) {
1450
+ if (this.gard === void 0)
1451
+ throw new NornsError("registerPort requires the worker to be in a gard");
1452
+ const channel = this.#channel;
1453
+ if (channel?.state !== "joined") {
1454
+ throw new NornsError("registerPort requires a connected worker");
1455
+ }
1456
+ const payload = {
1457
+ internal_port: internalPort,
1458
+ name: options.name ?? "",
1459
+ protocol: options.protocol ?? "http"
1460
+ };
1461
+ if (options.url) payload.url = options.url;
1462
+ return new Promise((resolve, reject) => {
1463
+ channel.push("register_port", payload, 1e4).receive(
1464
+ "ok",
1465
+ (resp) => resolve({ id: resp.id, internalPort: resp.internal_port, url: resp.url ?? null })
1466
+ ).receive(
1467
+ "error",
1468
+ (resp) => reject(new NornsError(`register_port rejected: ${errorMessage(resp?.reason ?? resp)}`))
1469
+ ).receive("timeout", () => reject(new NornsError("register_port timed out")));
1470
+ });
1471
+ }
1472
+ // --- Tasks ---
1473
+ // Each task runs on its own: a slow tool or LLM call must not hold up the
1474
+ // others on this connection (parallel tool calls, other runs).
1475
+ #spawn(taskId, run) {
1476
+ const controller = new AbortController();
1477
+ const entry = { taskId, controller, done: Promise.resolve() };
1478
+ entry.done = (async () => {
1479
+ try {
1480
+ const result = await run(controller.signal);
1481
+ this.#deliver(taskId, result);
1482
+ } catch (err) {
1483
+ this.#logger.error(`Task ${taskId} failed unexpectedly: ${errorMessage(err)}`);
1484
+ this.#deliver(taskId, { status: "error", error: errorMessage(err) });
1485
+ } finally {
1486
+ this.#inFlight.delete(entry);
1487
+ }
1488
+ })();
1489
+ this.#inFlight.add(entry);
1490
+ }
1491
+ #deliver(taskId, result) {
1492
+ const channel = this.#channel;
1493
+ if (channel?.state !== "joined") {
1494
+ this.#logger.warn(
1495
+ `Could not deliver result for task ${taskId} (not joined); Norns re-dispatches it on reconnect`
1496
+ );
1497
+ return;
1498
+ }
1499
+ channel.push("tool_result", { ...result, task_id: taskId }).receive(
1500
+ "error",
1501
+ (resp) => this.#logger.warn(`Server rejected result for ${taskId}: ${errorMessage(resp)}`)
1502
+ );
1503
+ }
1504
+ async #runLlm(received, signal) {
1505
+ const task = this.#history.resolve(received);
1506
+ if (task === void 0) {
1507
+ this.#logger.info(
1508
+ "LLM task arrived as a delta on an unknown base; asking for the full history"
1509
+ );
1510
+ return { status: "error", error: "message_cache_miss" };
1511
+ }
1512
+ const model = task.model ?? "?";
1513
+ const tools = task.tools ?? [];
1514
+ this.#logger.info(
1515
+ `LLM call \u2192 ${model} (${task.messages?.length ?? 0} messages, ${tools.length} tools)`
1516
+ );
1517
+ const result = await this.#callModel(task, signal);
1518
+ this.#logger.info(
1519
+ `LLM done \u2192 ${result.status === "ok" ? result.finish_reason : `error: ${result.error}`}`
1520
+ );
1521
+ return result;
1522
+ }
1523
+ async #callModel(task, signal) {
1524
+ try {
1525
+ const model = await this.#resolveModel(
1526
+ task.model ?? this.#agents[0]?.model ?? "claude-sonnet-5"
1527
+ );
1528
+ if (task.purpose === "compact") {
1529
+ const result2 = await (0, import_ai3.generateText)({
1530
+ model,
1531
+ instructions: toInstructions(composeCompactionPrompt(task)),
1532
+ messages: toModelMessages(compactionMessages(task)),
1533
+ maxOutputTokens: maxTokens(task),
1534
+ abortSignal: signal
1535
+ });
1536
+ const out2 = fromGenerateResult(result2);
1537
+ return {
1538
+ status: "ok",
1539
+ content: out2.content,
1540
+ finish_reason: "stop",
1541
+ usage: out2.usage,
1542
+ model: out2.model
1543
+ };
1544
+ }
1545
+ const neutral = messagesForTask(task);
1546
+ const toolSet = toToolSet(task.tools ?? []);
1547
+ const result = await (0, import_ai3.generateText)({
1548
+ model,
1549
+ instructions: toInstructions(composeSystemPrompt(task)),
1550
+ messages: toModelMessages(neutral),
1551
+ tools: Object.keys(toolSet).length > 0 ? toolSet : void 0,
1552
+ maxOutputTokens: maxTokens(task),
1553
+ abortSignal: signal
1554
+ });
1555
+ const out = fromGenerateResult(result);
1556
+ out.final_output = finalOutput(neutral, out.content);
1557
+ return out;
1558
+ } catch (err) {
1559
+ this.#logger.error(`LLM error: ${errorMessage(err)}`);
1560
+ return { status: "error", error: errorMessage(err) };
1561
+ }
1562
+ }
1563
+ async #runTool(task, signal) {
1564
+ const name = task.tool_name ?? "?";
1565
+ this.#logger.info(`Tool call \u2192 ${name}`);
1566
+ const result = await this.#executeTool(task, signal);
1567
+ const preview = (result.status === "ok" ? result.result : result.error).slice(0, 80);
1568
+ this.#logger.info(`Tool done \u2192 ${name}: ${result.status} ${preview}`);
1569
+ return result;
1570
+ }
1571
+ // A key on the task means Norns considers the call side-effecting and has
1572
+ // named it. It cannot know whether the effect landed, so if the result
1573
+ // never reached it, it asks again; this worker answers from what it kept
1574
+ // rather than doing the thing twice.
1575
+ async #executeTool(task, signal) {
1576
+ const key = task.idempotency_key;
1577
+ const completed = this.#completed;
1578
+ if (key && completed?.has(key)) {
1579
+ this.#logger.info(`Tool ${task.tool_name}: already done (${key}), reusing the result`);
1580
+ return completed.get(key);
1581
+ }
1582
+ const tool2 = this.#tools.get(task.tool_name);
1583
+ if (!tool2) return { status: "error", error: `Unknown tool: ${task.tool_name}` };
1584
+ let input = task.input ?? {};
1585
+ if (tool2.validate) {
1586
+ const checked = await tool2.validate(input);
1587
+ if (!checked.success) {
1588
+ return {
1589
+ status: "error",
1590
+ error: `Invalid input for ${tool2.name}: ${checked.error.message}`
1591
+ };
1592
+ }
1593
+ input = checked.value;
1594
+ }
1595
+ const context = {
1596
+ taskId: task.task_id,
1597
+ runId: task.run_id,
1598
+ agentId: task.agent_id,
1599
+ idempotencyKey: key,
1600
+ signal,
1601
+ worker: this
1602
+ };
1603
+ try {
1604
+ const output = await tool2.execute(input, context);
1605
+ const result = { status: "ok", result: stringifyToolOutput(output) };
1606
+ if (key) completed?.add(key, { ...result, duplicate: true });
1607
+ return result;
1608
+ } catch (err) {
1609
+ this.#logger.error(`Tool ${tool2.name} failed: ${errorMessage(err)}`);
1610
+ return { status: "error", error: errorMessage(err) };
1611
+ }
1612
+ }
1613
+ // --- Lifecycle plumbing ---
1614
+ #abortInFlight(reason) {
1615
+ for (const t of this.#inFlight) t.controller.abort(new NornsError(reason));
1616
+ }
1617
+ // Forced off: a fatal join, a destroyed gard. Nothing to drain.
1618
+ #fail(err) {
1619
+ if (this.#state === "stopped") return;
1620
+ this.#state = "stopped";
1621
+ this.#abortInFlight("worker stopped");
1622
+ this.#teardown();
1623
+ this.#socket?.disconnect();
1624
+ this.#settleStopped.reject(err);
1625
+ }
1626
+ #teardown() {
1627
+ if (this.#signalHandler) {
1628
+ process.off("SIGINT", this.#signalHandler);
1629
+ process.off("SIGTERM", this.#signalHandler);
1630
+ this.#signalHandler = void 0;
1631
+ }
1632
+ }
1633
+ #installSignalHandlers() {
1634
+ if (!this.#handleSignals) return;
1635
+ this.#signalHandler = (signal) => {
1636
+ if (this.#state !== "running") {
1637
+ this.#logger.warn(`Received ${signal} again: exiting now`);
1638
+ process.exit(130);
1639
+ }
1640
+ this.#logger.info(`Received ${signal}: shutting down`);
1641
+ void this.stop();
1642
+ };
1643
+ process.on("SIGINT", this.#signalHandler);
1644
+ process.on("SIGTERM", this.#signalHandler);
1645
+ }
1646
+ };
1647
+ // Annotate the CommonJS export names for ESM import in node:
1648
+ 0 && (module.exports = {
1649
+ DEFAULT_MODEL,
1650
+ GardDestroyedError,
1651
+ JoinError,
1652
+ NornsApiError,
1653
+ NornsClient,
1654
+ NornsError,
1655
+ NornsTimeoutError,
1656
+ NornsWorker,
1657
+ NotFoundError,
1658
+ StreamClosedError,
1659
+ consoleLogger,
1660
+ defineAgent,
1661
+ inferProvider,
1662
+ jsonSchema,
1663
+ silentLogger,
1664
+ tool
1665
+ });
1666
+ //# sourceMappingURL=index.cjs.map