@voicethere/agent 0.4.0 → 0.5.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/README.md +3 -1
- package/dist/agent.js +84 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +33 -4
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +3 -2
- package/dist/protocol.js.map +1 -1
- package/dist/runtime.d.ts +18 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +94 -2
- package/dist/runtime.js.map +1 -1
- package/dist/templates/echo/agent.js +90 -2
- package/dist/templates/echo-dc/agent.js +90 -2
- package/dist/templates/game-sync/agent.js +90 -2
- package/dist/templates/registry.d.ts.map +1 -1
- package/dist/templates/registry.js +16 -0
- package/dist/templates/registry.js.map +1 -1
- package/dist/templates/voice-starter/agent.js +90 -2
- package/dist/templates/webhooks/agent.js +674 -0
- package/dist/templates/webhooks-redis/agent.js +11449 -0
- package/package.json +1 -1
- package/templates/README.md +9 -1
- package/templates/webhooks-redis.ts +135 -0
- package/templates/webhooks.ts +111 -0
|
@@ -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 (
|
|
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 (
|
|
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,
|
|
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 (
|
|
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);
|