@voicethere/agent 0.2.16 → 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.
@@ -0,0 +1,598 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+
4
+
5
+ // dist/runtime.js
6
+ import { AsyncLocalStorage } from "node:async_hooks";
7
+
8
+ // dist/session-serial-queue.js
9
+ var SessionSerialQueueCancellationError = class extends Error {
10
+ sessionId;
11
+ generation;
12
+ code = "SESSION_SERIAL_QUEUE_CANCELLED";
13
+ constructor(sessionId, generation) {
14
+ super(`session serial queue cancelled (sessionId=${sessionId}, generation=${generation})`);
15
+ this.sessionId = sessionId;
16
+ this.generation = generation;
17
+ this.name = "SessionSerialQueueCancellationError";
18
+ }
19
+ };
20
+ var SessionSerialQueue = class {
21
+ sessions = /* @__PURE__ */ new Map();
22
+ /** Monotonic token source — never reused across clear/enqueue cycles. */
23
+ nextGeneration = 1;
24
+ /**
25
+ * Enqueue work for `sessionId`. Returns a promise that settles when this
26
+ * task finishes, is skipped as stale, or is cancelled by {@link clear}.
27
+ * Fire-and-forget callers may ignore the return value (compat).
28
+ */
29
+ enqueue(sessionId, task) {
30
+ const state = this.ensureState(sessionId);
31
+ const captured = state;
32
+ const generation = captured.generation;
33
+ captured.pending += 1;
34
+ const previous = captured.tail;
35
+ let taskAbort = null;
36
+ const settled = previous.catch(() => void 0).then(async () => {
37
+ if (this.sessions.get(sessionId) !== captured) {
38
+ return "cancelled";
39
+ }
40
+ const abort = new AbortController();
41
+ taskAbort = abort;
42
+ captured.abort = abort;
43
+ const context = {
44
+ sessionId,
45
+ generation,
46
+ signal: abort.signal
47
+ };
48
+ try {
49
+ await Promise.resolve(task(abort.signal, context));
50
+ if (this.sessions.get(sessionId) !== captured) {
51
+ return "cancelled";
52
+ }
53
+ return "completed";
54
+ } catch (error) {
55
+ if (abort.signal.aborted || this.sessions.get(sessionId) !== captured || error instanceof SessionSerialQueueCancellationError) {
56
+ return "cancelled";
57
+ }
58
+ return "failed";
59
+ } finally {
60
+ if (captured.abort === abort) {
61
+ captured.abort = null;
62
+ }
63
+ }
64
+ }).finally(() => {
65
+ captured.pending = Math.max(0, captured.pending - 1);
66
+ if (taskAbort && captured.abort === taskAbort) {
67
+ captured.abort = null;
68
+ }
69
+ });
70
+ captured.tail = settled;
71
+ return settled;
72
+ }
73
+ /**
74
+ * Invalidate queued + running work for `sessionId`. Aborts the live AbortSignal,
75
+ * drops the map entry, and leaves a fresh identity for the next enqueue.
76
+ * Old in-flight `finally` blocks only touch their captured state object.
77
+ */
78
+ clear(sessionId) {
79
+ const old = this.sessions.get(sessionId);
80
+ if (!old)
81
+ return;
82
+ old.abort?.abort();
83
+ old.abort = null;
84
+ this.sessions.delete(sessionId);
85
+ }
86
+ clearAll() {
87
+ for (const sessionId of [...this.sessions.keys()]) {
88
+ this.clear(sessionId);
89
+ }
90
+ }
91
+ hasPending(sessionId) {
92
+ return (this.sessions.get(sessionId)?.pending ?? 0) > 0;
93
+ }
94
+ /** Live generation token for a session, or `undefined` when cleared. */
95
+ generationOf(sessionId) {
96
+ return this.sessions.get(sessionId)?.generation;
97
+ }
98
+ /** True when `generation` is the live map entry for `sessionId`. */
99
+ isCurrentGeneration(sessionId, generation) {
100
+ return this.sessions.get(sessionId)?.generation === generation;
101
+ }
102
+ /**
103
+ * True when a queue row exists for the session (registered since first
104
+ * enqueue, until {@link clear}). Not the same as {@link hasPending}.
105
+ */
106
+ isLive(sessionId) {
107
+ return this.sessions.has(sessionId);
108
+ }
109
+ /** Registered session rows (must not grow without matching `clear` calls). */
110
+ get activeSessionCount() {
111
+ return this.sessions.size;
112
+ }
113
+ ensureState(sessionId) {
114
+ const existing = this.sessions.get(sessionId);
115
+ if (existing)
116
+ return existing;
117
+ const created = {
118
+ generation: this.nextGeneration++,
119
+ tail: Promise.resolve(),
120
+ abort: null,
121
+ pending: 0
122
+ };
123
+ this.sessions.set(sessionId, created);
124
+ return created;
125
+ }
126
+ };
127
+
128
+ // dist/runtime.js
129
+ var SESSION_START_INIT_DELAY_ENABLED_ENV = "AGENT_SESSION_START_INIT_DELAY_ENABLED";
130
+ var SESSION_START_INIT_DELAY_MS_ENV = "AGENT_SESSION_START_INIT_DELAY_MS";
131
+ var DEFAULT_SESSION_START_INIT_DELAY_MS = 500;
132
+ function isParentMessage(value) {
133
+ if (!value || typeof value !== "object")
134
+ return false;
135
+ const msg = value;
136
+ return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary" || msg.type === "idle_timeout";
137
+ }
138
+ function parseDataChannelPayload(raw) {
139
+ try {
140
+ return JSON.parse(raw);
141
+ } catch {
142
+ return raw;
143
+ }
144
+ }
145
+ var peerEnvBySessionId = /* @__PURE__ */ new Map();
146
+ var endedSessionIds = /* @__PURE__ */ new Set();
147
+ var sessionExecutionContext = new AsyncLocalStorage();
148
+ var agentLogSessionContext = new AsyncLocalStorage();
149
+ var inboundQueueAuthority = null;
150
+ var childUnhandledRejectionGuardInstalled = false;
151
+ function normalizeRejectionReason(reason) {
152
+ return reason instanceof Error ? reason : new Error(String(reason));
153
+ }
154
+ function installChildUnhandledRejectionGuard() {
155
+ if (childUnhandledRejectionGuardInstalled) {
156
+ return;
157
+ }
158
+ childUnhandledRejectionGuardInstalled = true;
159
+ process.on("unhandledRejection", (reason) => {
160
+ const err = normalizeRejectionReason(reason);
161
+ const store = sessionExecutionContext.getStore();
162
+ const sessionId = store?.sessionId ?? agentLogSessionContext.getStore() ?? "";
163
+ if (!allowOutboundForSession(sessionId || void 0)) {
164
+ return;
165
+ }
166
+ agentLog("error", `unhandledRejection: ${err.message}`, sessionId || void 0);
167
+ sendParentMessage({
168
+ type: "agent_error",
169
+ sessionId,
170
+ message: err.message,
171
+ stack: err.stack
172
+ });
173
+ });
174
+ }
175
+ function allowOutboundForSession(sessionId) {
176
+ if (!sessionId) {
177
+ return true;
178
+ }
179
+ const store = sessionExecutionContext.getStore();
180
+ const queue = inboundQueueAuthority;
181
+ if (store && store.sessionId === sessionId) {
182
+ if (!queue)
183
+ return !endedSessionIds.has(sessionId);
184
+ return queue.isCurrentGeneration(sessionId, store.generation);
185
+ }
186
+ if (endedSessionIds.has(sessionId))
187
+ return false;
188
+ if (!queue)
189
+ return true;
190
+ return queue.isLive(sessionId);
191
+ }
192
+ function sendParentMessage(message) {
193
+ const sessionId = message && typeof message === "object" && "sessionId" in message && typeof message.sessionId === "string" ? message.sessionId : void 0;
194
+ if (!allowOutboundForSession(sessionId))
195
+ return;
196
+ process.send?.(message);
197
+ }
198
+ function parseBooleanEnv(value, defaultValue) {
199
+ if (value === void 0)
200
+ return defaultValue;
201
+ const normalized = value.trim().toLowerCase();
202
+ if (normalized === "")
203
+ return defaultValue;
204
+ if (["0", "false", "off", "no"].includes(normalized))
205
+ return false;
206
+ if (["1", "true", "on", "yes"].includes(normalized))
207
+ return true;
208
+ return defaultValue;
209
+ }
210
+ function parseNonNegativeIntegerEnv(value, defaultValue) {
211
+ if (value === void 0)
212
+ return defaultValue;
213
+ const parsed = Number(value);
214
+ if (!Number.isFinite(parsed) || parsed < 0)
215
+ return defaultValue;
216
+ return Math.floor(parsed);
217
+ }
218
+ function resolveSessionStartInitDelayMs() {
219
+ const enabled = parseBooleanEnv(process.env[SESSION_START_INIT_DELAY_ENABLED_ENV], true);
220
+ if (!enabled)
221
+ return 0;
222
+ return parseNonNegativeIntegerEnv(process.env[SESSION_START_INIT_DELAY_MS_ENV], DEFAULT_SESSION_START_INIT_DELAY_MS);
223
+ }
224
+ async function handleParentMessage(message, handlers) {
225
+ switch (message.type) {
226
+ case "session_start":
227
+ endedSessionIds.delete(message.sessionId);
228
+ peerEnvBySessionId.set(message.sessionId, message.env);
229
+ const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
230
+ if (sessionStartInitDelayMs > 0) {
231
+ await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
232
+ }
233
+ await (handlers.onClientJoin ?? handlers.onSessionStart)?.({
234
+ sessionId: message.sessionId,
235
+ env: message.env
236
+ });
237
+ sendParentMessage({
238
+ type: "session_start_ack",
239
+ sessionId: message.sessionId
240
+ });
241
+ break;
242
+ case "speech_event":
243
+ await handlers.onSpeechEvent?.({ sessionId: message.sessionId }, message.event);
244
+ if (message.event.type === "user_speech_final" && typeof message.event.text === "string" && message.event.text.trim()) {
245
+ await handlers.onUserSpeechFinal?.({
246
+ sessionId: message.sessionId,
247
+ text: message.event.text.trim()
248
+ });
249
+ }
250
+ break;
251
+ case "data_channel_message":
252
+ await handlers.onDataChannelMessage?.({
253
+ sessionId: message.sessionId,
254
+ message: parseDataChannelPayload(message.payload),
255
+ raw: message.payload,
256
+ rawBinary: null,
257
+ channel: "control"
258
+ });
259
+ break;
260
+ case "data_channel_binary":
261
+ await handlers.onDataChannelBinary?.({
262
+ sessionId: message.sessionId,
263
+ message: null,
264
+ raw: null,
265
+ rawBinary: message.data,
266
+ channel: message.channel ?? "sync"
267
+ });
268
+ break;
269
+ case "session_end":
270
+ peerEnvBySessionId.delete(message.sessionId);
271
+ await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
272
+ sessionId: message.sessionId
273
+ });
274
+ break;
275
+ case "idle_timeout":
276
+ await runIdleTimeoutHook(handlers, message);
277
+ break;
278
+ }
279
+ }
280
+ function defineAgent(handlers) {
281
+ installChildUnhandledRejectionGuard();
282
+ const inboundBySession = new SessionSerialQueue();
283
+ inboundQueueAuthority = inboundBySession;
284
+ const agentStartReady = runAgentStartHook(handlers);
285
+ process.on("message", (message) => {
286
+ if (!isParentMessage(message))
287
+ return;
288
+ if (message.type === "session_end") {
289
+ endedSessionIds.add(message.sessionId);
290
+ inboundBySession.clear(message.sessionId);
291
+ }
292
+ if (message.type === "session_start") {
293
+ endedSessionIds.delete(message.sessionId);
294
+ inboundBySession.clear(message.sessionId);
295
+ }
296
+ inboundBySession.enqueue(message.sessionId, async (_signal, context) => {
297
+ await agentStartReady;
298
+ try {
299
+ await sessionExecutionContext.run({
300
+ sessionId: message.sessionId,
301
+ generation: context.generation
302
+ }, async () => agentLogSessionContext.run(message.sessionId, async () => {
303
+ try {
304
+ await handleParentMessage(message, handlers);
305
+ } catch (error) {
306
+ const err = error instanceof Error ? error : new Error(String(error));
307
+ const env = peerEnvBySessionId.get(message.sessionId) ?? buildIdleEnv(message.sessionId);
308
+ await runErrorHook(handlers, {
309
+ sessionId: message.sessionId,
310
+ projectId: env.PROJECT_ID,
311
+ buildId: env.BUILD_ID,
312
+ env,
313
+ error: err,
314
+ customerContext: parseCustomerContext(env.AGENT_CUSTOMER_CONTEXT)
315
+ });
316
+ sendParentMessage({
317
+ type: "agent_error",
318
+ sessionId: message.sessionId,
319
+ message: err.message,
320
+ stack: err.stack
321
+ });
322
+ }
323
+ }));
324
+ } finally {
325
+ if (message.type === "session_end" && inboundBySession.isCurrentGeneration(message.sessionId, context.generation)) {
326
+ inboundBySession.clear(message.sessionId);
327
+ }
328
+ }
329
+ });
330
+ });
331
+ }
332
+ async function runAgentStartHook(handlers) {
333
+ if (!handlers.onAgentStart)
334
+ return;
335
+ try {
336
+ await handlers.onAgentStart({
337
+ env: process.env
338
+ });
339
+ } catch (error) {
340
+ const err = error instanceof Error ? error : new Error(String(error));
341
+ agentLog("error", `onAgentStart failed: ${err.message}`);
342
+ sendParentMessage({
343
+ type: "agent_error",
344
+ sessionId: "",
345
+ message: err.message,
346
+ stack: err.stack
347
+ });
348
+ }
349
+ }
350
+ function parseCustomerContext(raw) {
351
+ if (!raw?.trim())
352
+ return void 0;
353
+ try {
354
+ const parsed = JSON.parse(raw);
355
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
356
+ return parsed;
357
+ }
358
+ } catch {
359
+ }
360
+ return void 0;
361
+ }
362
+ async function runErrorHook(handlers, ctx) {
363
+ if (!handlers.errorHook)
364
+ return;
365
+ try {
366
+ await handlers.errorHook(ctx);
367
+ } catch (hookError) {
368
+ const message = hookError instanceof Error ? hookError.message : String(hookError);
369
+ agentLog("error", `errorHook failed: ${message}`, ctx.sessionId);
370
+ }
371
+ }
372
+ async function runIdleTimeoutHook(handlers, message) {
373
+ const onIdleTimeout = handlers.onIdleTimeout;
374
+ agentLog("info", `idle_timeout ipc received (maxGraceMs=${message.maxGraceMs}, onIdleTimeout=${typeof onIdleTimeout === "function"})`, message.sessionId);
375
+ if (!onIdleTimeout) {
376
+ sendParentMessage({
377
+ type: "idle_timeout_done",
378
+ sessionId: message.sessionId
379
+ });
380
+ agentLog("info", "idle_timeout_done ipc sent (no onIdleTimeout handler)", message.sessionId);
381
+ return;
382
+ }
383
+ const env = peerEnvBySessionId.get(message.sessionId) ?? buildIdleEnv(message.sessionId);
384
+ const idleTimeoutSeconds = Number(env.IDLE_TIMEOUT_SEC) || 0;
385
+ const ctx = {
386
+ sessionId: message.sessionId,
387
+ projectId: env.PROJECT_ID,
388
+ buildId: env.BUILD_ID,
389
+ env,
390
+ idleTimeoutSeconds
391
+ };
392
+ let error;
393
+ try {
394
+ await onIdleTimeout(ctx);
395
+ } catch (hookError) {
396
+ error = hookError instanceof Error ? hookError.message : String(hookError);
397
+ agentLog("error", `onIdleTimeout failed: ${error}`, message.sessionId);
398
+ }
399
+ sendParentMessage({
400
+ type: "idle_timeout_done",
401
+ sessionId: message.sessionId,
402
+ error
403
+ });
404
+ agentLog("info", error ? `idle_timeout_done ipc sent (onIdleTimeout error: ${error})` : "idle_timeout_done ipc sent (onIdleTimeout completed)", message.sessionId);
405
+ }
406
+ function buildIdleEnv(sessionId) {
407
+ return {
408
+ SESSION_ID: sessionId,
409
+ ...process.env.PROJECT_ID ? { PROJECT_ID: process.env.PROJECT_ID } : {},
410
+ ...process.env.BUILD_ID ? { BUILD_ID: process.env.BUILD_ID } : {},
411
+ ...process.env.IDLE_TIMEOUT_SEC ? { IDLE_TIMEOUT_SEC: process.env.IDLE_TIMEOUT_SEC } : {}
412
+ };
413
+ }
414
+ function speak(sessionId, text) {
415
+ sendParentMessage({ type: "speak", sessionId, text });
416
+ }
417
+ var AGENT_LOG_MESSAGE_MAX_CHARS = 2048;
418
+ var AGENT_LOG_FIELDS_MAX_CHARS = 8192;
419
+ function truncateAgentLogMessage(message) {
420
+ if (message.length <= AGENT_LOG_MESSAGE_MAX_CHARS) {
421
+ return message;
422
+ }
423
+ const suffix = "\u2026[truncated]";
424
+ return message.slice(0, AGENT_LOG_MESSAGE_MAX_CHARS - suffix.length) + suffix;
425
+ }
426
+ function isPlainObject(value) {
427
+ return typeof value === "object" && value !== null && !Array.isArray(value);
428
+ }
429
+ function sanitizeAgentLogFields(fields) {
430
+ if (Object.keys(fields).length === 0) {
431
+ return void 0;
432
+ }
433
+ try {
434
+ const serialized = JSON.stringify(fields);
435
+ if (serialized.length <= AGENT_LOG_FIELDS_MAX_CHARS) {
436
+ return fields;
437
+ }
438
+ return {
439
+ _agentLogFieldsTruncated: true,
440
+ _originalBytes: serialized.length,
441
+ _preview: serialized.slice(0, AGENT_LOG_FIELDS_MAX_CHARS - 80) + "\u2026[truncated]"
442
+ };
443
+ } catch {
444
+ return { _agentLogFieldsError: "not_serializable" };
445
+ }
446
+ }
447
+ function buildAgentLogPayload(level, message, fields, sessionId) {
448
+ const resolvedSessionId = sessionId ?? sessionExecutionContext.getStore()?.sessionId ?? agentLogSessionContext.getStore();
449
+ const sanitizedFields = fields ? sanitizeAgentLogFields(fields) : void 0;
450
+ return {
451
+ type: "log",
452
+ level,
453
+ message: truncateAgentLogMessage(message),
454
+ ts: Date.now(),
455
+ ...resolvedSessionId ? { sessionId: resolvedSessionId } : {},
456
+ ...sanitizedFields ? { fields: sanitizedFields } : {}
457
+ };
458
+ }
459
+ function agentLog(level, message, fieldsOrSessionId, sessionId) {
460
+ let fields;
461
+ let resolvedSessionId;
462
+ if (typeof fieldsOrSessionId === "string") {
463
+ resolvedSessionId = fieldsOrSessionId;
464
+ } else if (isPlainObject(fieldsOrSessionId)) {
465
+ fields = fieldsOrSessionId;
466
+ resolvedSessionId = sessionId;
467
+ } else {
468
+ resolvedSessionId = sessionId;
469
+ }
470
+ sendParentMessage(buildAgentLogPayload(level, message, fields, resolvedSessionId));
471
+ }
472
+
473
+ // templates/agent.ts
474
+ var peers = /* @__PURE__ */ new Map();
475
+ function peerState(sessionId) {
476
+ let state = peers.get(sessionId);
477
+ if (!state) {
478
+ state = {
479
+ userSpeaking: false,
480
+ agentSpeaking: false,
481
+ lastPartial: "",
482
+ sttActive: false
483
+ };
484
+ peers.set(sessionId, state);
485
+ }
486
+ return state;
487
+ }
488
+ function formatSpeechDetail(speech) {
489
+ if (speech.text) return `"${speech.text}"`;
490
+ if (speech.error) return speech.error;
491
+ return "";
492
+ }
493
+ function handleSpeechEvent(sessionId, speech) {
494
+ const state = peerState(sessionId);
495
+ const detail = formatSpeechDetail(speech);
496
+ switch (speech.type) {
497
+ // --- User turn (VAD + STT) ---
498
+ case "user_speaking_start":
499
+ state.userSpeaking = true;
500
+ agentLog("info", `[${sessionId}] user_speaking_start`);
501
+ break;
502
+ case "user_speaking_end":
503
+ state.userSpeaking = false;
504
+ agentLog("info", `[${sessionId}] user_speaking_end`);
505
+ break;
506
+ case "vad_triggered":
507
+ agentLog(
508
+ "info",
509
+ `[${sessionId}] vad_triggered \u2014 STT listen window opened`
510
+ );
511
+ break;
512
+ case "stt_stream_start":
513
+ agentLog("info", `[${sessionId}] stt_stream_start`);
514
+ break;
515
+ case "stt_stream_end":
516
+ agentLog("info", `[${sessionId}] stt_stream_end`);
517
+ break;
518
+ case "user_stt_start":
519
+ state.sttActive = true;
520
+ state.lastPartial = "";
521
+ agentLog("info", `[${sessionId}] user_stt_start`);
522
+ break;
523
+ case "user_stt_end":
524
+ state.sttActive = false;
525
+ agentLog("info", `[${sessionId}] user_stt_end`);
526
+ break;
527
+ case "user_stt_not_found":
528
+ agentLog(
529
+ "info",
530
+ `[${sessionId}] user_stt_not_found \u2014 no speech recognized in listen window`
531
+ );
532
+ break;
533
+ case "user_speech_partial":
534
+ state.lastPartial = speech.text ?? "";
535
+ agentLog("info", `[${sessionId}] user_speech_partial ${detail}`);
536
+ break;
537
+ case "user_speech_final":
538
+ state.lastPartial = "";
539
+ agentLog("info", `[${sessionId}] user_speech_final ${detail}`);
540
+ break;
541
+ // --- Agent playback (TTS) ---
542
+ case "agent_speaking_start":
543
+ state.agentSpeaking = true;
544
+ agentLog("info", `[${sessionId}] agent_speaking_start`);
545
+ break;
546
+ case "agent_speaking_end":
547
+ state.agentSpeaking = false;
548
+ agentLog("info", `[${sessionId}] agent_speaking_end`);
549
+ break;
550
+ case "barge_in":
551
+ agentLog(
552
+ "info",
553
+ `[${sessionId}] barge_in \u2014 user interrupted agent playback`
554
+ );
555
+ break;
556
+ case "error":
557
+ agentLog(
558
+ "error",
559
+ `[${sessionId}] pipeline error: ${speech.error ?? "unknown"}`
560
+ );
561
+ break;
562
+ default: {
563
+ const _exhaustive = speech.type;
564
+ agentLog(
565
+ "error",
566
+ `[${sessionId}] unhandled speech event: ${String(_exhaustive)}`
567
+ );
568
+ }
569
+ }
570
+ }
571
+ defineAgent({
572
+ onSessionStart({ sessionId, env }) {
573
+ agentLog("info", `session_start ${sessionId} env=${JSON.stringify(env)}`);
574
+ speak(sessionId, "Hello! How can I help?");
575
+ },
576
+ onSpeechEvent({ sessionId }, speech) {
577
+ handleSpeechEvent(sessionId, speech);
578
+ },
579
+ onUserSpeechFinal({ sessionId, text }) {
580
+ speak(sessionId, `You said: ${text}`);
581
+ },
582
+ onSessionEnd({ sessionId }) {
583
+ peers.delete(sessionId);
584
+ agentLog("info", `session_end ${sessionId}`);
585
+ }
586
+ // async onIdleTimeout({ sessionId, idleTimeoutSeconds }) {
587
+ // agentLog("info", `idle timeout after ${idleTimeoutSeconds}s for ${sessionId}`);
588
+ // // Optional: notify client, persist state — runner disconnects after this returns (max 30s).
589
+ // },
590
+ // async errorHook({ sessionId, error, customerContext }) {
591
+ // agentLog("error", `handler failed for ${sessionId}: ${error.message}`);
592
+ // // Optional: sendToClient(sessionId, { type: "alert", message: error.message });
593
+ // },
594
+ // Example: disconnect a stale multiplayer client from agent code:
595
+ // onDataChannelMessage({ sessionId, message }) {
596
+ // if (isStaleGameState(message)) disconnectClient(sessionId, { reason: "stale_state" });
597
+ // },
598
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voicethere/agent",
3
- "version": "0.2.16",
3
+ "version": "0.3.0",
4
4
  "description": "VoiceThere customer agent SDK — IPC types and runtime helpers for sandboxed child bundles",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,6 +21,18 @@
21
21
  "import": "./dist/verify/run-verify.js",
22
22
  "require": "./dist/verify/run-verify.js",
23
23
  "default": "./dist/verify/run-verify.js"
24
+ },
25
+ "./templates": {
26
+ "types": "./dist/templates/index.d.ts",
27
+ "import": "./dist/templates/index.js",
28
+ "require": "./dist/templates/index.js",
29
+ "default": "./dist/templates/index.js"
30
+ },
31
+ "./templates/redis-sync/world-layout": {
32
+ "types": "./templates/redis-sync/world-layout.ts",
33
+ "import": "./templates/redis-sync/world-layout.ts",
34
+ "require": "./templates/redis-sync/world-layout.ts",
35
+ "default": "./templates/redis-sync/world-layout.ts"
24
36
  }
25
37
  },
26
38
  "bin": "./dist/cli.js",
@@ -32,8 +44,9 @@
32
44
  ],
33
45
  "scripts": {
34
46
  "build:lib": "tsc -p tsconfig.json && mkdir -p dist/sandbox && cp src/sandbox/loader-entry.js dist/sandbox/",
47
+ "build:templates": "node dist/templates/build-seed-bundles-cli.js",
35
48
  "build:example": "node dist/cli.js build --entry examples/agent.ts --outfile dist/agent.js",
36
- "build": "npm run build:lib && npm run build:example",
49
+ "build": "npm run build:lib && npm run build:templates && npm run build:example",
37
50
  "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
38
51
  "test": "vitest run",
39
52
  "test:ci": "npm run typecheck && npm run build && npm run test && node scripts/ci/verify-package-exports.mjs",
@@ -75,9 +88,9 @@
75
88
  "@node-webrtc-rust/sdk": ">=0.6.5"
76
89
  },
77
90
  "devDependencies": {
78
- "@node-webrtc-rust/helpers": "^0.6.24",
79
- "@node-webrtc-rust/sdk": "0.6.24",
80
- "@node-webrtc-rust/signaling": "0.6.24",
91
+ "@node-webrtc-rust/helpers": "^0.7.0",
92
+ "@node-webrtc-rust/sdk": "0.7.0",
93
+ "@node-webrtc-rust/signaling": "0.7.0",
81
94
  "@types/node": "^22.10.0",
82
95
  "http-server": "^14.1.1",
83
96
  "ioredis": "^5.11.1",