@voicethere/agent 0.3.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 +61 -57
- package/dist/agent.js +118 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +69 -5
- 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 +31 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +184 -2
- package/dist/runtime.js.map +1 -1
- package/dist/templates/echo/agent.js +127 -3
- package/dist/templates/echo-dc/agent.js +127 -3
- package/dist/templates/game-sync/agent.js +127 -3
- 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 +127 -3
- package/dist/templates/webhooks/agent.js +674 -0
- package/dist/templates/webhooks-redis/agent.js +11449 -0
- package/package.json +4 -3
- package/templates/README.md +9 -1
- package/templates/webhooks-redis.ts +135 -0
- package/templates/webhooks.ts +111 -0
|
@@ -3,6 +3,7 @@ const require = createRequire(import.meta.url);
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
// dist/runtime.js
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
8
|
|
|
8
9
|
// dist/session-serial-queue.js
|
|
@@ -129,11 +130,58 @@ var SessionSerialQueue = class {
|
|
|
129
130
|
var SESSION_START_INIT_DELAY_ENABLED_ENV = "AGENT_SESSION_START_INIT_DELAY_ENABLED";
|
|
130
131
|
var SESSION_START_INIT_DELAY_MS_ENV = "AGENT_SESSION_START_INIT_DELAY_MS";
|
|
131
132
|
var DEFAULT_SESSION_START_INIT_DELAY_MS = 500;
|
|
133
|
+
function isRecordingControlAckMessage(value) {
|
|
134
|
+
if (!value || typeof value !== "object")
|
|
135
|
+
return false;
|
|
136
|
+
const msg = value;
|
|
137
|
+
return msg.type === "recording_control_ack" && typeof msg.requestId === "string";
|
|
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
|
+
}
|
|
132
177
|
function isParentMessage(value) {
|
|
133
178
|
if (!value || typeof value !== "object")
|
|
134
179
|
return false;
|
|
135
180
|
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";
|
|
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);
|
|
137
185
|
}
|
|
138
186
|
function parseDataChannelPayload(raw) {
|
|
139
187
|
try {
|
|
@@ -144,6 +192,28 @@ function parseDataChannelPayload(raw) {
|
|
|
144
192
|
}
|
|
145
193
|
var peerEnvBySessionId = /* @__PURE__ */ new Map();
|
|
146
194
|
var endedSessionIds = /* @__PURE__ */ new Set();
|
|
195
|
+
var pendingRecordingAcks = /* @__PURE__ */ new Map();
|
|
196
|
+
function handleRecordingControlAck(message) {
|
|
197
|
+
const pending = pendingRecordingAcks.get(message.requestId);
|
|
198
|
+
if (!pending)
|
|
199
|
+
return;
|
|
200
|
+
clearTimeout(pending.timer);
|
|
201
|
+
pendingRecordingAcks.delete(message.requestId);
|
|
202
|
+
pending.resolve({
|
|
203
|
+
ok: message.ok,
|
|
204
|
+
reason: message.reason,
|
|
205
|
+
requestId: message.requestId
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function clearPendingRecordingAcksForSession(sessionId, reason) {
|
|
209
|
+
for (const [requestId, pending] of pendingRecordingAcks) {
|
|
210
|
+
if (pending.sessionId !== sessionId)
|
|
211
|
+
continue;
|
|
212
|
+
clearTimeout(pending.timer);
|
|
213
|
+
pendingRecordingAcks.delete(requestId);
|
|
214
|
+
pending.resolve({ ok: false, reason, requestId });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
147
217
|
var sessionExecutionContext = new AsyncLocalStorage();
|
|
148
218
|
var agentLogSessionContext = new AsyncLocalStorage();
|
|
149
219
|
var inboundQueueAuthority = null;
|
|
@@ -221,6 +291,49 @@ function resolveSessionStartInitDelayMs() {
|
|
|
221
291
|
return 0;
|
|
222
292
|
return parseNonNegativeIntegerEnv(process.env[SESSION_START_INIT_DELAY_MS_ENV], DEFAULT_SESSION_START_INIT_DELAY_MS);
|
|
223
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
|
+
}
|
|
224
337
|
async function handleParentMessage(message, handlers) {
|
|
225
338
|
switch (message.type) {
|
|
226
339
|
case "session_start":
|
|
@@ -232,7 +345,8 @@ async function handleParentMessage(message, handlers) {
|
|
|
232
345
|
}
|
|
233
346
|
await (handlers.onClientJoin ?? handlers.onSessionStart)?.({
|
|
234
347
|
sessionId: message.sessionId,
|
|
235
|
-
env: message.env
|
|
348
|
+
env: message.env,
|
|
349
|
+
recordingAvailable: message.recordingAvailable ?? false
|
|
236
350
|
});
|
|
237
351
|
sendParentMessage({
|
|
238
352
|
type: "session_start_ack",
|
|
@@ -267,6 +381,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
267
381
|
});
|
|
268
382
|
break;
|
|
269
383
|
case "session_end":
|
|
384
|
+
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
270
385
|
peerEnvBySessionId.delete(message.sessionId);
|
|
271
386
|
await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
|
|
272
387
|
sessionId: message.sessionId
|
|
@@ -283,10 +398,19 @@ function defineAgent(handlers) {
|
|
|
283
398
|
inboundQueueAuthority = inboundBySession;
|
|
284
399
|
const agentStartReady = runAgentStartHook(handlers);
|
|
285
400
|
process.on("message", (message) => {
|
|
286
|
-
if (
|
|
401
|
+
if (isRecordingControlAckMessage(message)) {
|
|
402
|
+
handleRecordingControlAck(message);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (isWebhookMessage(message)) {
|
|
406
|
+
void agentStartReady.then(() => handleWebhookMessage(message, handlers));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!isSessionScopedParentMessage(message))
|
|
287
410
|
return;
|
|
288
411
|
if (message.type === "session_end") {
|
|
289
412
|
endedSessionIds.add(message.sessionId);
|
|
413
|
+
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
290
414
|
inboundBySession.clear(message.sessionId);
|
|
291
415
|
}
|
|
292
416
|
if (message.type === "session_start") {
|
|
@@ -3,6 +3,7 @@ const require = createRequire(import.meta.url);
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
// dist/runtime.js
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
8
|
|
|
8
9
|
// dist/session-serial-queue.js
|
|
@@ -129,11 +130,58 @@ var SessionSerialQueue = class {
|
|
|
129
130
|
var SESSION_START_INIT_DELAY_ENABLED_ENV = "AGENT_SESSION_START_INIT_DELAY_ENABLED";
|
|
130
131
|
var SESSION_START_INIT_DELAY_MS_ENV = "AGENT_SESSION_START_INIT_DELAY_MS";
|
|
131
132
|
var DEFAULT_SESSION_START_INIT_DELAY_MS = 500;
|
|
133
|
+
function isRecordingControlAckMessage(value) {
|
|
134
|
+
if (!value || typeof value !== "object")
|
|
135
|
+
return false;
|
|
136
|
+
const msg = value;
|
|
137
|
+
return msg.type === "recording_control_ack" && typeof msg.requestId === "string";
|
|
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
|
+
}
|
|
132
177
|
function isParentMessage(value) {
|
|
133
178
|
if (!value || typeof value !== "object")
|
|
134
179
|
return false;
|
|
135
180
|
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";
|
|
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);
|
|
137
185
|
}
|
|
138
186
|
function parseDataChannelPayload(raw) {
|
|
139
187
|
try {
|
|
@@ -144,6 +192,28 @@ function parseDataChannelPayload(raw) {
|
|
|
144
192
|
}
|
|
145
193
|
var peerEnvBySessionId = /* @__PURE__ */ new Map();
|
|
146
194
|
var endedSessionIds = /* @__PURE__ */ new Set();
|
|
195
|
+
var pendingRecordingAcks = /* @__PURE__ */ new Map();
|
|
196
|
+
function handleRecordingControlAck(message) {
|
|
197
|
+
const pending = pendingRecordingAcks.get(message.requestId);
|
|
198
|
+
if (!pending)
|
|
199
|
+
return;
|
|
200
|
+
clearTimeout(pending.timer);
|
|
201
|
+
pendingRecordingAcks.delete(message.requestId);
|
|
202
|
+
pending.resolve({
|
|
203
|
+
ok: message.ok,
|
|
204
|
+
reason: message.reason,
|
|
205
|
+
requestId: message.requestId
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function clearPendingRecordingAcksForSession(sessionId, reason) {
|
|
209
|
+
for (const [requestId, pending] of pendingRecordingAcks) {
|
|
210
|
+
if (pending.sessionId !== sessionId)
|
|
211
|
+
continue;
|
|
212
|
+
clearTimeout(pending.timer);
|
|
213
|
+
pendingRecordingAcks.delete(requestId);
|
|
214
|
+
pending.resolve({ ok: false, reason, requestId });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
147
217
|
var sessionExecutionContext = new AsyncLocalStorage();
|
|
148
218
|
var agentLogSessionContext = new AsyncLocalStorage();
|
|
149
219
|
var inboundQueueAuthority = null;
|
|
@@ -221,6 +291,49 @@ function resolveSessionStartInitDelayMs() {
|
|
|
221
291
|
return 0;
|
|
222
292
|
return parseNonNegativeIntegerEnv(process.env[SESSION_START_INIT_DELAY_MS_ENV], DEFAULT_SESSION_START_INIT_DELAY_MS);
|
|
223
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
|
+
}
|
|
224
337
|
async function handleParentMessage(message, handlers) {
|
|
225
338
|
switch (message.type) {
|
|
226
339
|
case "session_start":
|
|
@@ -232,7 +345,8 @@ async function handleParentMessage(message, handlers) {
|
|
|
232
345
|
}
|
|
233
346
|
await (handlers.onClientJoin ?? handlers.onSessionStart)?.({
|
|
234
347
|
sessionId: message.sessionId,
|
|
235
|
-
env: message.env
|
|
348
|
+
env: message.env,
|
|
349
|
+
recordingAvailable: message.recordingAvailable ?? false
|
|
236
350
|
});
|
|
237
351
|
sendParentMessage({
|
|
238
352
|
type: "session_start_ack",
|
|
@@ -267,6 +381,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
267
381
|
});
|
|
268
382
|
break;
|
|
269
383
|
case "session_end":
|
|
384
|
+
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
270
385
|
peerEnvBySessionId.delete(message.sessionId);
|
|
271
386
|
await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
|
|
272
387
|
sessionId: message.sessionId
|
|
@@ -283,10 +398,19 @@ function defineAgent(handlers) {
|
|
|
283
398
|
inboundQueueAuthority = inboundBySession;
|
|
284
399
|
const agentStartReady = runAgentStartHook(handlers);
|
|
285
400
|
process.on("message", (message) => {
|
|
286
|
-
if (
|
|
401
|
+
if (isRecordingControlAckMessage(message)) {
|
|
402
|
+
handleRecordingControlAck(message);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (isWebhookMessage(message)) {
|
|
406
|
+
void agentStartReady.then(() => handleWebhookMessage(message, handlers));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!isSessionScopedParentMessage(message))
|
|
287
410
|
return;
|
|
288
411
|
if (message.type === "session_end") {
|
|
289
412
|
endedSessionIds.add(message.sessionId);
|
|
413
|
+
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
290
414
|
inboundBySession.clear(message.sessionId);
|
|
291
415
|
}
|
|
292
416
|
if (message.type === "session_start") {
|
|
@@ -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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"}
|
|
@@ -3,6 +3,7 @@ const require = createRequire(import.meta.url);
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
// dist/runtime.js
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
8
|
|
|
8
9
|
// dist/session-serial-queue.js
|
|
@@ -129,11 +130,58 @@ var SessionSerialQueue = class {
|
|
|
129
130
|
var SESSION_START_INIT_DELAY_ENABLED_ENV = "AGENT_SESSION_START_INIT_DELAY_ENABLED";
|
|
130
131
|
var SESSION_START_INIT_DELAY_MS_ENV = "AGENT_SESSION_START_INIT_DELAY_MS";
|
|
131
132
|
var DEFAULT_SESSION_START_INIT_DELAY_MS = 500;
|
|
133
|
+
function isRecordingControlAckMessage(value) {
|
|
134
|
+
if (!value || typeof value !== "object")
|
|
135
|
+
return false;
|
|
136
|
+
const msg = value;
|
|
137
|
+
return msg.type === "recording_control_ack" && typeof msg.requestId === "string";
|
|
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
|
+
}
|
|
132
177
|
function isParentMessage(value) {
|
|
133
178
|
if (!value || typeof value !== "object")
|
|
134
179
|
return false;
|
|
135
180
|
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";
|
|
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);
|
|
137
185
|
}
|
|
138
186
|
function parseDataChannelPayload(raw) {
|
|
139
187
|
try {
|
|
@@ -144,6 +192,28 @@ function parseDataChannelPayload(raw) {
|
|
|
144
192
|
}
|
|
145
193
|
var peerEnvBySessionId = /* @__PURE__ */ new Map();
|
|
146
194
|
var endedSessionIds = /* @__PURE__ */ new Set();
|
|
195
|
+
var pendingRecordingAcks = /* @__PURE__ */ new Map();
|
|
196
|
+
function handleRecordingControlAck(message) {
|
|
197
|
+
const pending = pendingRecordingAcks.get(message.requestId);
|
|
198
|
+
if (!pending)
|
|
199
|
+
return;
|
|
200
|
+
clearTimeout(pending.timer);
|
|
201
|
+
pendingRecordingAcks.delete(message.requestId);
|
|
202
|
+
pending.resolve({
|
|
203
|
+
ok: message.ok,
|
|
204
|
+
reason: message.reason,
|
|
205
|
+
requestId: message.requestId
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function clearPendingRecordingAcksForSession(sessionId, reason) {
|
|
209
|
+
for (const [requestId, pending] of pendingRecordingAcks) {
|
|
210
|
+
if (pending.sessionId !== sessionId)
|
|
211
|
+
continue;
|
|
212
|
+
clearTimeout(pending.timer);
|
|
213
|
+
pendingRecordingAcks.delete(requestId);
|
|
214
|
+
pending.resolve({ ok: false, reason, requestId });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
147
217
|
var sessionExecutionContext = new AsyncLocalStorage();
|
|
148
218
|
var agentLogSessionContext = new AsyncLocalStorage();
|
|
149
219
|
var inboundQueueAuthority = null;
|
|
@@ -221,6 +291,49 @@ function resolveSessionStartInitDelayMs() {
|
|
|
221
291
|
return 0;
|
|
222
292
|
return parseNonNegativeIntegerEnv(process.env[SESSION_START_INIT_DELAY_MS_ENV], DEFAULT_SESSION_START_INIT_DELAY_MS);
|
|
223
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
|
+
}
|
|
224
337
|
async function handleParentMessage(message, handlers) {
|
|
225
338
|
switch (message.type) {
|
|
226
339
|
case "session_start":
|
|
@@ -232,7 +345,8 @@ async function handleParentMessage(message, handlers) {
|
|
|
232
345
|
}
|
|
233
346
|
await (handlers.onClientJoin ?? handlers.onSessionStart)?.({
|
|
234
347
|
sessionId: message.sessionId,
|
|
235
|
-
env: message.env
|
|
348
|
+
env: message.env,
|
|
349
|
+
recordingAvailable: message.recordingAvailable ?? false
|
|
236
350
|
});
|
|
237
351
|
sendParentMessage({
|
|
238
352
|
type: "session_start_ack",
|
|
@@ -267,6 +381,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
267
381
|
});
|
|
268
382
|
break;
|
|
269
383
|
case "session_end":
|
|
384
|
+
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
270
385
|
peerEnvBySessionId.delete(message.sessionId);
|
|
271
386
|
await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
|
|
272
387
|
sessionId: message.sessionId
|
|
@@ -283,10 +398,19 @@ function defineAgent(handlers) {
|
|
|
283
398
|
inboundQueueAuthority = inboundBySession;
|
|
284
399
|
const agentStartReady = runAgentStartHook(handlers);
|
|
285
400
|
process.on("message", (message) => {
|
|
286
|
-
if (
|
|
401
|
+
if (isRecordingControlAckMessage(message)) {
|
|
402
|
+
handleRecordingControlAck(message);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (isWebhookMessage(message)) {
|
|
406
|
+
void agentStartReady.then(() => handleWebhookMessage(message, handlers));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!isSessionScopedParentMessage(message))
|
|
287
410
|
return;
|
|
288
411
|
if (message.type === "session_end") {
|
|
289
412
|
endedSessionIds.add(message.sessionId);
|
|
413
|
+
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
290
414
|
inboundBySession.clear(message.sessionId);
|
|
291
415
|
}
|
|
292
416
|
if (message.type === "session_start") {
|