@voicethere/agent 0.4.0 → 0.5.1

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.
@@ -136,11 +136,52 @@ function isRecordingControlAckMessage(value) {
136
136
  const msg = value;
137
137
  return msg.type === "recording_control_ack" && typeof msg.requestId === "string";
138
138
  }
139
+ function isWebhookMessage(value) {
140
+ if (!value || typeof value !== "object")
141
+ return false;
142
+ const msg = value;
143
+ return msg.type === "webhook" && typeof msg.eventId === "string" && typeof msg.projectId === "string";
144
+ }
145
+ function coerceInboundBinary(value) {
146
+ if (!value)
147
+ return null;
148
+ if (Buffer.isBuffer(value))
149
+ return value;
150
+ if (value instanceof Uint8Array) {
151
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
152
+ }
153
+ if (value instanceof ArrayBuffer)
154
+ return Buffer.from(value);
155
+ if (ArrayBuffer.isView(value)) {
156
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
157
+ }
158
+ if (typeof value === "object") {
159
+ const maybeBufferLike = value;
160
+ if (maybeBufferLike.type === "Buffer" && Array.isArray(maybeBufferLike.data)) {
161
+ return Buffer.from(maybeBufferLike.data);
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+ function normalizeWebhookHeaders(value) {
167
+ if (!value || typeof value !== "object")
168
+ return {};
169
+ const out = {};
170
+ for (const [key, raw] of Object.entries(value)) {
171
+ if (typeof raw === "string") {
172
+ out[key] = raw;
173
+ }
174
+ }
175
+ return out;
176
+ }
139
177
  function isParentMessage(value) {
140
178
  if (!value || typeof value !== "object")
141
179
  return false;
142
180
  const msg = value;
143
- 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" || msg.type === "recording_control_ack";
181
+ 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" || msg.type === "recording_control_ack" || msg.type === "webhook";
182
+ }
183
+ function isSessionScopedParentMessage(value) {
184
+ return isParentMessage(value) && !isWebhookMessage(value);
144
185
  }
145
186
  function parseDataChannelPayload(raw) {
146
187
  try {
@@ -250,6 +291,49 @@ function resolveSessionStartInitDelayMs() {
250
291
  return 0;
251
292
  return parseNonNegativeIntegerEnv(process.env[SESSION_START_INIT_DELAY_MS_ENV], DEFAULT_SESSION_START_INIT_DELAY_MS);
252
293
  }
294
+ async function handleWebhookMessage(message, handlers) {
295
+ if (!handlers.onWebhook)
296
+ return;
297
+ const body = coerceInboundBinary(message.body);
298
+ if (!body) {
299
+ agentLog("warn", "webhook ipc dropped: body is not binary");
300
+ return;
301
+ }
302
+ const ctx = {
303
+ eventId: message.eventId,
304
+ projectId: message.projectId,
305
+ method: typeof message.method === "string" ? message.method : "POST",
306
+ path: typeof message.path === "string" ? message.path : "",
307
+ headers: normalizeWebhookHeaders(message.headers),
308
+ body,
309
+ contentType: typeof message.contentType === "string" ? message.contentType : null,
310
+ receivedAt: typeof message.receivedAt === "string" ? message.receivedAt : ""
311
+ };
312
+ try {
313
+ const started = Date.now();
314
+ await handlers.onWebhook(ctx);
315
+ sendParentMessage({
316
+ type: "webhook_handled",
317
+ projectId: message.projectId,
318
+ eventId: message.eventId,
319
+ durationMs: Date.now() - started
320
+ });
321
+ } catch (error) {
322
+ const err = error instanceof Error ? error : new Error(String(error));
323
+ await runErrorHook(handlers, {
324
+ sessionId: "",
325
+ projectId: message.projectId,
326
+ env: process.env,
327
+ error: err
328
+ });
329
+ sendParentMessage({
330
+ type: "agent_error",
331
+ sessionId: "",
332
+ message: err.message,
333
+ stack: err.stack
334
+ });
335
+ }
336
+ }
253
337
  async function handleParentMessage(message, handlers) {
254
338
  switch (message.type) {
255
339
  case "session_start":
@@ -318,7 +402,11 @@ function defineAgent(handlers) {
318
402
  handleRecordingControlAck(message);
319
403
  return;
320
404
  }
321
- if (!isParentMessage(message))
405
+ if (isWebhookMessage(message)) {
406
+ void agentStartReady.then(() => handleWebhookMessage(message, handlers));
407
+ return;
408
+ }
409
+ if (!isSessionScopedParentMessage(message))
322
410
  return;
323
411
  if (message.type === "session_end") {
324
412
  endedSessionIds.add(message.sessionId);
@@ -136,11 +136,52 @@ function isRecordingControlAckMessage(value) {
136
136
  const msg = value;
137
137
  return msg.type === "recording_control_ack" && typeof msg.requestId === "string";
138
138
  }
139
+ function isWebhookMessage(value) {
140
+ if (!value || typeof value !== "object")
141
+ return false;
142
+ const msg = value;
143
+ return msg.type === "webhook" && typeof msg.eventId === "string" && typeof msg.projectId === "string";
144
+ }
145
+ function coerceInboundBinary(value) {
146
+ if (!value)
147
+ return null;
148
+ if (Buffer.isBuffer(value))
149
+ return value;
150
+ if (value instanceof Uint8Array) {
151
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
152
+ }
153
+ if (value instanceof ArrayBuffer)
154
+ return Buffer.from(value);
155
+ if (ArrayBuffer.isView(value)) {
156
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
157
+ }
158
+ if (typeof value === "object") {
159
+ const maybeBufferLike = value;
160
+ if (maybeBufferLike.type === "Buffer" && Array.isArray(maybeBufferLike.data)) {
161
+ return Buffer.from(maybeBufferLike.data);
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+ function normalizeWebhookHeaders(value) {
167
+ if (!value || typeof value !== "object")
168
+ return {};
169
+ const out = {};
170
+ for (const [key, raw] of Object.entries(value)) {
171
+ if (typeof raw === "string") {
172
+ out[key] = raw;
173
+ }
174
+ }
175
+ return out;
176
+ }
139
177
  function isParentMessage(value) {
140
178
  if (!value || typeof value !== "object")
141
179
  return false;
142
180
  const msg = value;
143
- 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" || msg.type === "recording_control_ack";
181
+ 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" || msg.type === "recording_control_ack" || msg.type === "webhook";
182
+ }
183
+ function isSessionScopedParentMessage(value) {
184
+ return isParentMessage(value) && !isWebhookMessage(value);
144
185
  }
145
186
  function parseDataChannelPayload(raw) {
146
187
  try {
@@ -250,6 +291,49 @@ function resolveSessionStartInitDelayMs() {
250
291
  return 0;
251
292
  return parseNonNegativeIntegerEnv(process.env[SESSION_START_INIT_DELAY_MS_ENV], DEFAULT_SESSION_START_INIT_DELAY_MS);
252
293
  }
294
+ async function handleWebhookMessage(message, handlers) {
295
+ if (!handlers.onWebhook)
296
+ return;
297
+ const body = coerceInboundBinary(message.body);
298
+ if (!body) {
299
+ agentLog("warn", "webhook ipc dropped: body is not binary");
300
+ return;
301
+ }
302
+ const ctx = {
303
+ eventId: message.eventId,
304
+ projectId: message.projectId,
305
+ method: typeof message.method === "string" ? message.method : "POST",
306
+ path: typeof message.path === "string" ? message.path : "",
307
+ headers: normalizeWebhookHeaders(message.headers),
308
+ body,
309
+ contentType: typeof message.contentType === "string" ? message.contentType : null,
310
+ receivedAt: typeof message.receivedAt === "string" ? message.receivedAt : ""
311
+ };
312
+ try {
313
+ const started = Date.now();
314
+ await handlers.onWebhook(ctx);
315
+ sendParentMessage({
316
+ type: "webhook_handled",
317
+ projectId: message.projectId,
318
+ eventId: message.eventId,
319
+ durationMs: Date.now() - started
320
+ });
321
+ } catch (error) {
322
+ const err = error instanceof Error ? error : new Error(String(error));
323
+ await runErrorHook(handlers, {
324
+ sessionId: "",
325
+ projectId: message.projectId,
326
+ env: process.env,
327
+ error: err
328
+ });
329
+ sendParentMessage({
330
+ type: "agent_error",
331
+ sessionId: "",
332
+ message: err.message,
333
+ stack: err.stack
334
+ });
335
+ }
336
+ }
253
337
  async function handleParentMessage(message, handlers) {
254
338
  switch (message.type) {
255
339
  case "session_start":
@@ -318,7 +402,11 @@ function defineAgent(handlers) {
318
402
  handleRecordingControlAck(message);
319
403
  return;
320
404
  }
321
- if (!isParentMessage(message))
405
+ if (isWebhookMessage(message)) {
406
+ void agentStartReady.then(() => handleWebhookMessage(message, handlers));
407
+ return;
408
+ }
409
+ if (!isSessionScopedParentMessage(message))
322
410
  return;
323
411
  if (message.type === "session_end") {
324
412
  endedSessionIds.add(message.sessionId);
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC;AAE7C,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC;IACnB,mFAAmF;IACnF,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,eAAO,MAAM,eAAe,EAAE,SAAS,uBAAuB,EAyEpD,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;AAMrE,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,eAAe,CAEnE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,uBAAuB,CAMnE;AAED,wBAAgB,yBAAyB,IAAI,uBAAuB,EAAE,CAErE"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC;AAE7C,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC;IACnB,mFAAmF;IACnF,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,eAAO,MAAM,eAAe,EAAE,SAAS,uBAAuB,EA2FpD,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;AAMrE,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,eAAe,CAEnE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,uBAAuB,CAMnE;AAED,wBAAgB,yBAAyB,IAAI,uBAAuB,EAAE,CAErE"}
@@ -31,6 +31,22 @@ export const AGENT_TEMPLATES = [
31
31
  description: "Authoritative multi-object sync sample for real-time games and simulations.",
32
32
  sourceFiles: ["game-sync.ts"],
33
33
  },
34
+ {
35
+ id: "webhooks",
36
+ entry: "webhooks.ts",
37
+ kind: "product",
38
+ seedOnCreate: true,
39
+ description: "Inbound webhook handler — HMAC verify on raw body, then DataChannel + speak fan-out.",
40
+ sourceFiles: ["webhooks.ts"],
41
+ },
42
+ {
43
+ id: "webhooks-redis",
44
+ entry: "webhooks-redis.ts",
45
+ kind: "product",
46
+ seedOnCreate: true,
47
+ description: "Webhook handler with Redis atomic shared counter plus DataChannel fan-out.",
48
+ sourceFiles: ["webhooks-redis.ts"],
49
+ },
34
50
  {
35
51
  id: "echo-smoke",
36
52
  entry: "echo-smoke.ts",
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"AAcA,MAAM,CAAC,MAAM,eAAe,GAAuC;IACjE;QACE,EAAE,EAAE,MAAM;QACV,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,IAAI;QAClB,WAAW,EACT,8EAA8E;QAChF,WAAW,EAAE,CAAC,SAAS,CAAC;KACzB;IACD;QACE,EAAE,EAAE,SAAS;QACb,KAAK,EAAE,YAAY;QACnB,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,IAAI;QAClB,WAAW,EACT,qEAAqE;QACvE,WAAW,EAAE,CAAC,YAAY,CAAC;KAC5B;IACD;QACE,EAAE,EAAE,eAAe;QACnB,KAAK,EAAE,UAAU;QACjB,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,IAAI;QAClB,WAAW,EACT,uFAAuF;QACzF,WAAW,EAAE,CAAC,UAAU,CAAC;KAC1B;IACD;QACE,EAAE,EAAE,WAAW;QACf,KAAK,EAAE,cAAc;QACrB,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,IAAI;QAClB,WAAW,EACT,6EAA6E;QAC/E,WAAW,EAAE,CAAC,cAAc,CAAC;KAC9B;IACD;QACE,EAAE,EAAE,YAAY;QAChB,KAAK,EAAE,eAAe;QACtB,IAAI,EAAE,KAAK;QACX,YAAY,EAAE,KAAK;QACnB,WAAW,EACT,6EAA6E;QAC/E,WAAW,EAAE,CAAC,eAAe,CAAC;KAC/B;IACD;QACE,EAAE,EAAE,OAAO;QACX,KAAK,EAAE,UAAU;QACjB,IAAI,EAAE,KAAK;QACX,YAAY,EAAE,KAAK;QACnB,WAAW,EACT,4EAA4E;QAC9E,WAAW,EAAE,CAAC,UAAU,CAAC;KAC1B;IACD;QACE,EAAE,EAAE,iBAAiB;QACrB,KAAK,EAAE,oBAAoB;QAC3B,IAAI,EAAE,KAAK;QACX,YAAY,EAAE,KAAK;QACnB,WAAW,EACT,6EAA6E;QAC/E,WAAW,EAAE,CAAC,oBAAoB,CAAC;KACpC;IACD;QACE,EAAE,EAAE,YAAY;QAChB,KAAK,EAAE,qBAAqB;QAC5B,IAAI,EAAE,KAAK;QACX,YAAY,EAAE,KAAK;QACnB,WAAW,EACT,sFAAsF;QACxF,WAAW,EAAE,CAAC,qBAAqB,EAAE,4BAA4B,CAAC;KACnE;CACO,CAAC;AAIX,MAAM,cAAc,GAAG,IAAI,GAAG,CAC5B,eAAe,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC,CAC3D,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,EAAU;IAC1C,OAAO,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAU;IACxC,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACxC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,8BAA8B,EAAE,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,yBAAyB;IACvC,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;AACrE,CAAC"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"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"}
@@ -136,11 +136,52 @@ function isRecordingControlAckMessage(value) {
136
136
  const msg = value;
137
137
  return msg.type === "recording_control_ack" && typeof msg.requestId === "string";
138
138
  }
139
+ function isWebhookMessage(value) {
140
+ if (!value || typeof value !== "object")
141
+ return false;
142
+ const msg = value;
143
+ return msg.type === "webhook" && typeof msg.eventId === "string" && typeof msg.projectId === "string";
144
+ }
145
+ function coerceInboundBinary(value) {
146
+ if (!value)
147
+ return null;
148
+ if (Buffer.isBuffer(value))
149
+ return value;
150
+ if (value instanceof Uint8Array) {
151
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
152
+ }
153
+ if (value instanceof ArrayBuffer)
154
+ return Buffer.from(value);
155
+ if (ArrayBuffer.isView(value)) {
156
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
157
+ }
158
+ if (typeof value === "object") {
159
+ const maybeBufferLike = value;
160
+ if (maybeBufferLike.type === "Buffer" && Array.isArray(maybeBufferLike.data)) {
161
+ return Buffer.from(maybeBufferLike.data);
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+ function normalizeWebhookHeaders(value) {
167
+ if (!value || typeof value !== "object")
168
+ return {};
169
+ const out = {};
170
+ for (const [key, raw] of Object.entries(value)) {
171
+ if (typeof raw === "string") {
172
+ out[key] = raw;
173
+ }
174
+ }
175
+ return out;
176
+ }
139
177
  function isParentMessage(value) {
140
178
  if (!value || typeof value !== "object")
141
179
  return false;
142
180
  const msg = value;
143
- 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" || msg.type === "recording_control_ack";
181
+ 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" || msg.type === "recording_control_ack" || msg.type === "webhook";
182
+ }
183
+ function isSessionScopedParentMessage(value) {
184
+ return isParentMessage(value) && !isWebhookMessage(value);
144
185
  }
145
186
  function parseDataChannelPayload(raw) {
146
187
  try {
@@ -250,6 +291,49 @@ function resolveSessionStartInitDelayMs() {
250
291
  return 0;
251
292
  return parseNonNegativeIntegerEnv(process.env[SESSION_START_INIT_DELAY_MS_ENV], DEFAULT_SESSION_START_INIT_DELAY_MS);
252
293
  }
294
+ async function handleWebhookMessage(message, handlers) {
295
+ if (!handlers.onWebhook)
296
+ return;
297
+ const body = coerceInboundBinary(message.body);
298
+ if (!body) {
299
+ agentLog("warn", "webhook ipc dropped: body is not binary");
300
+ return;
301
+ }
302
+ const ctx = {
303
+ eventId: message.eventId,
304
+ projectId: message.projectId,
305
+ method: typeof message.method === "string" ? message.method : "POST",
306
+ path: typeof message.path === "string" ? message.path : "",
307
+ headers: normalizeWebhookHeaders(message.headers),
308
+ body,
309
+ contentType: typeof message.contentType === "string" ? message.contentType : null,
310
+ receivedAt: typeof message.receivedAt === "string" ? message.receivedAt : ""
311
+ };
312
+ try {
313
+ const started = Date.now();
314
+ await handlers.onWebhook(ctx);
315
+ sendParentMessage({
316
+ type: "webhook_handled",
317
+ projectId: message.projectId,
318
+ eventId: message.eventId,
319
+ durationMs: Date.now() - started
320
+ });
321
+ } catch (error) {
322
+ const err = error instanceof Error ? error : new Error(String(error));
323
+ await runErrorHook(handlers, {
324
+ sessionId: "",
325
+ projectId: message.projectId,
326
+ env: process.env,
327
+ error: err
328
+ });
329
+ sendParentMessage({
330
+ type: "agent_error",
331
+ sessionId: "",
332
+ message: err.message,
333
+ stack: err.stack
334
+ });
335
+ }
336
+ }
253
337
  async function handleParentMessage(message, handlers) {
254
338
  switch (message.type) {
255
339
  case "session_start":
@@ -318,7 +402,11 @@ function defineAgent(handlers) {
318
402
  handleRecordingControlAck(message);
319
403
  return;
320
404
  }
321
- if (!isParentMessage(message))
405
+ if (isWebhookMessage(message)) {
406
+ void agentStartReady.then(() => handleWebhookMessage(message, handlers));
407
+ return;
408
+ }
409
+ if (!isSessionScopedParentMessage(message))
322
410
  return;
323
411
  if (message.type === "session_end") {
324
412
  endedSessionIds.add(message.sessionId);