@voicethere/agent 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.js +6 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +13 -1
- package/dist/runtime.js.map +1 -1
- package/dist/templates/echo/agent.js +3 -0
- package/dist/templates/echo-dc/agent.js +3 -0
- package/dist/templates/game-sync/agent.js +67 -31
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +14 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/recording-consent/agent.js +1003 -0
- package/dist/templates/registry.d.ts.map +1 -1
- package/dist/templates/registry.js +12 -0
- package/dist/templates/registry.js.map +1 -1
- package/dist/templates/voice-showcase/agent.js +44 -19
- package/dist/templates/voice-starter/agent.js +3 -0
- package/dist/templates/webhooks/agent.js +3 -0
- package/dist/templates/webhooks-redis/agent.js +3 -0
- package/package.json +7 -1
- package/templates/README.md +12 -6
- package/templates/game-sync-world-layout.ts +45 -0
- package/templates/game-sync.ts +39 -24
- package/templates/recording-consent/agent.ts +162 -0
- package/templates/recording-consent/conversation.ts +300 -0
- package/templates/voice-showcase/agent.ts +21 -22
- package/templates/voice-showcase/delivery.ts +57 -0
|
@@ -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,EA6HpD,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"}
|
|
@@ -46,11 +46,23 @@ export const AGENT_TEMPLATES = [
|
|
|
46
46
|
sourceFiles: [
|
|
47
47
|
"voice-showcase/agent.ts",
|
|
48
48
|
"voice-showcase/conversation.ts",
|
|
49
|
+
"voice-showcase/delivery.ts",
|
|
49
50
|
"voice-showcase/weather.ts",
|
|
50
51
|
"voice-showcase/recipes.ts",
|
|
51
52
|
"voice-showcase/fun-facts.ts",
|
|
52
53
|
],
|
|
53
54
|
},
|
|
55
|
+
{
|
|
56
|
+
id: "recording-consent",
|
|
57
|
+
entry: "recording-consent/agent.ts",
|
|
58
|
+
kind: "product",
|
|
59
|
+
seedOnCreate: true,
|
|
60
|
+
description: "Recording consent flow — ask consent, pause capture for PII, resume when allowed.",
|
|
61
|
+
sourceFiles: [
|
|
62
|
+
"recording-consent/agent.ts",
|
|
63
|
+
"recording-consent/conversation.ts",
|
|
64
|
+
],
|
|
65
|
+
},
|
|
54
66
|
{
|
|
55
67
|
id: "webhooks",
|
|
56
68
|
entry: "webhooks.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;YACX,cAAc;YACd,uBAAuB;YACvB,2BAA2B;YAC3B,kBAAkB;YAClB,oBAAoB;SACrB;KACF;IACD;QACE,EAAE,EAAE,gBAAgB;QACpB,KAAK,EAAE,yBAAyB;QAChC,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,IAAI;QAClB,WAAW,EACT,0FAA0F;QAC5F,WAAW,EAAE;YACX,yBAAyB;YACzB,gCAAgC;YAChC,2BAA2B;YAC3B,2BAA2B;YAC3B,6BAA6B;SAC9B;KACF;IACD;QACE,EAAE,EAAE,UAAU;QACd,KAAK,EAAE,aAAa;QACpB,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,IAAI;QAClB,WAAW,EACT,sFAAsF;QACxF,WAAW,EAAE,CAAC,aAAa,CAAC;KAC7B;IACD;QACE,EAAE,EAAE,gBAAgB;QACpB,KAAK,EAAE,mBAAmB;QAC1B,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,IAAI;QAClB,WAAW,EACT,4EAA4E;QAC9E,WAAW,EAAE,CAAC,mBAAmB,CAAC;KACnC;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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"}
|
|
@@ -191,6 +191,7 @@ function parseDataChannelPayload(raw) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
var peerEnvBySessionId = /* @__PURE__ */ new Map();
|
|
194
|
+
var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
|
|
194
195
|
var endedSessionIds = /* @__PURE__ */ new Set();
|
|
195
196
|
var pendingRecordingAcks = /* @__PURE__ */ new Map();
|
|
196
197
|
function handleRecordingControlAck(message) {
|
|
@@ -340,6 +341,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
340
341
|
case "session_start":
|
|
341
342
|
endedSessionIds.delete(message.sessionId);
|
|
342
343
|
peerEnvBySessionId.set(message.sessionId, message.env);
|
|
344
|
+
recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
|
|
343
345
|
const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
|
|
344
346
|
if (sessionStartInitDelayMs > 0) {
|
|
345
347
|
await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
|
|
@@ -384,6 +386,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
384
386
|
case "session_end":
|
|
385
387
|
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
386
388
|
peerEnvBySessionId.delete(message.sessionId);
|
|
389
|
+
recordingAvailableBySessionId.delete(message.sessionId);
|
|
387
390
|
await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
|
|
388
391
|
sessionId: message.sessionId
|
|
389
392
|
});
|
|
@@ -1314,6 +1317,36 @@ async function resolveWeatherTurn(state, city, country, fetchFn) {
|
|
|
1314
1317
|
}
|
|
1315
1318
|
}
|
|
1316
1319
|
|
|
1320
|
+
// templates/voice-showcase/delivery.ts
|
|
1321
|
+
function spokenThenPlayOps(messages, speakLines) {
|
|
1322
|
+
const ops = [];
|
|
1323
|
+
for (const message of messages) {
|
|
1324
|
+
ops.push({ kind: "send", message });
|
|
1325
|
+
}
|
|
1326
|
+
for (const text of speakLines) {
|
|
1327
|
+
ops.push({ kind: "play", text });
|
|
1328
|
+
}
|
|
1329
|
+
return ops;
|
|
1330
|
+
}
|
|
1331
|
+
function greetingOps(sessionId, greeting) {
|
|
1332
|
+
return spokenThenPlayOps(
|
|
1333
|
+
[
|
|
1334
|
+
{ type: "agent_event", event: "session_start", sessionId },
|
|
1335
|
+
{ type: "chat_reply", text: greeting }
|
|
1336
|
+
],
|
|
1337
|
+
[greeting]
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
function applyOutboundOps(sessionId, ops, deps) {
|
|
1341
|
+
for (const op of ops) {
|
|
1342
|
+
if (op.kind === "send") {
|
|
1343
|
+
deps.sendToClient(sessionId, op.message);
|
|
1344
|
+
} else {
|
|
1345
|
+
deps.speak(sessionId, op.text);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1317
1350
|
// templates/voice-showcase/agent.ts
|
|
1318
1351
|
var sessions = /* @__PURE__ */ new Map();
|
|
1319
1352
|
function getState(sessionId) {
|
|
@@ -1332,20 +1365,16 @@ function relaySpeechEvent(sessionId, event) {
|
|
|
1332
1365
|
raw: event
|
|
1333
1366
|
});
|
|
1334
1367
|
}
|
|
1335
|
-
function
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
for (const line of lines) {
|
|
1342
|
-
speak(sessionId, line);
|
|
1343
|
-
}
|
|
1368
|
+
function deliverSpokenThenPlay(sessionId, result) {
|
|
1369
|
+
applyOutboundOps(
|
|
1370
|
+
sessionId,
|
|
1371
|
+
spokenThenPlayOps(result.messages, result.speakLines),
|
|
1372
|
+
{ sendToClient, speak }
|
|
1373
|
+
);
|
|
1344
1374
|
}
|
|
1345
1375
|
async function applyTurn(sessionId, result) {
|
|
1346
1376
|
sessions.set(sessionId, result.state);
|
|
1347
|
-
|
|
1348
|
-
deliverMessages(sessionId, result.messages);
|
|
1377
|
+
deliverSpokenThenPlay(sessionId, result);
|
|
1349
1378
|
if (result.pendingWeather) {
|
|
1350
1379
|
const weatherResult = await resolveWeatherTurn(
|
|
1351
1380
|
result.state,
|
|
@@ -1353,8 +1382,7 @@ async function applyTurn(sessionId, result) {
|
|
|
1353
1382
|
result.pendingWeather.country
|
|
1354
1383
|
);
|
|
1355
1384
|
sessions.set(sessionId, weatherResult.state);
|
|
1356
|
-
|
|
1357
|
-
deliverMessages(sessionId, weatherResult.messages);
|
|
1385
|
+
deliverSpokenThenPlay(sessionId, weatherResult);
|
|
1358
1386
|
}
|
|
1359
1387
|
}
|
|
1360
1388
|
async function onUserText(sessionId, text) {
|
|
@@ -1365,13 +1393,10 @@ async function onUserText(sessionId, text) {
|
|
|
1365
1393
|
defineAgent({
|
|
1366
1394
|
onSessionStart({ sessionId }) {
|
|
1367
1395
|
sessions.set(sessionId, createInitialState());
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
sessionId
|
|
1396
|
+
applyOutboundOps(sessionId, greetingOps(sessionId, GREETING), {
|
|
1397
|
+
sendToClient,
|
|
1398
|
+
speak
|
|
1372
1399
|
});
|
|
1373
|
-
speak(sessionId, GREETING);
|
|
1374
|
-
sendToClient(sessionId, { type: "chat_reply", text: GREETING });
|
|
1375
1400
|
agentLog("info", `voice-showcase session_start ${sessionId}`);
|
|
1376
1401
|
},
|
|
1377
1402
|
onSpeechEvent({ sessionId }, event) {
|
|
@@ -191,6 +191,7 @@ function parseDataChannelPayload(raw) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
var peerEnvBySessionId = /* @__PURE__ */ new Map();
|
|
194
|
+
var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
|
|
194
195
|
var endedSessionIds = /* @__PURE__ */ new Set();
|
|
195
196
|
var pendingRecordingAcks = /* @__PURE__ */ new Map();
|
|
196
197
|
function handleRecordingControlAck(message) {
|
|
@@ -340,6 +341,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
340
341
|
case "session_start":
|
|
341
342
|
endedSessionIds.delete(message.sessionId);
|
|
342
343
|
peerEnvBySessionId.set(message.sessionId, message.env);
|
|
344
|
+
recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
|
|
343
345
|
const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
|
|
344
346
|
if (sessionStartInitDelayMs > 0) {
|
|
345
347
|
await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
|
|
@@ -384,6 +386,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
384
386
|
case "session_end":
|
|
385
387
|
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
386
388
|
peerEnvBySessionId.delete(message.sessionId);
|
|
389
|
+
recordingAvailableBySessionId.delete(message.sessionId);
|
|
387
390
|
await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
|
|
388
391
|
sessionId: message.sessionId
|
|
389
392
|
});
|
|
@@ -194,6 +194,7 @@ function parseDataChannelPayload(raw) {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
var peerEnvBySessionId = /* @__PURE__ */ new Map();
|
|
197
|
+
var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
|
|
197
198
|
var endedSessionIds = /* @__PURE__ */ new Set();
|
|
198
199
|
var pendingRecordingAcks = /* @__PURE__ */ new Map();
|
|
199
200
|
function handleRecordingControlAck(message) {
|
|
@@ -343,6 +344,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
343
344
|
case "session_start":
|
|
344
345
|
endedSessionIds.delete(message.sessionId);
|
|
345
346
|
peerEnvBySessionId.set(message.sessionId, message.env);
|
|
347
|
+
recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
|
|
346
348
|
const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
|
|
347
349
|
if (sessionStartInitDelayMs > 0) {
|
|
348
350
|
await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
|
|
@@ -387,6 +389,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
387
389
|
case "session_end":
|
|
388
390
|
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
389
391
|
peerEnvBySessionId.delete(message.sessionId);
|
|
392
|
+
recordingAvailableBySessionId.delete(message.sessionId);
|
|
390
393
|
await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
|
|
391
394
|
sessionId: message.sessionId
|
|
392
395
|
});
|
|
@@ -10946,6 +10946,7 @@ function parseDataChannelPayload(raw) {
|
|
|
10946
10946
|
}
|
|
10947
10947
|
}
|
|
10948
10948
|
var peerEnvBySessionId = /* @__PURE__ */ new Map();
|
|
10949
|
+
var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
|
|
10949
10950
|
var endedSessionIds = /* @__PURE__ */ new Set();
|
|
10950
10951
|
var pendingRecordingAcks = /* @__PURE__ */ new Map();
|
|
10951
10952
|
function handleRecordingControlAck(message) {
|
|
@@ -11095,6 +11096,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
11095
11096
|
case "session_start":
|
|
11096
11097
|
endedSessionIds.delete(message.sessionId);
|
|
11097
11098
|
peerEnvBySessionId.set(message.sessionId, message.env);
|
|
11099
|
+
recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
|
|
11098
11100
|
const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
|
|
11099
11101
|
if (sessionStartInitDelayMs > 0) {
|
|
11100
11102
|
await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
|
|
@@ -11139,6 +11141,7 @@ async function handleParentMessage(message, handlers) {
|
|
|
11139
11141
|
case "session_end":
|
|
11140
11142
|
clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
|
|
11141
11143
|
peerEnvBySessionId.delete(message.sessionId);
|
|
11144
|
+
recordingAvailableBySessionId.delete(message.sessionId);
|
|
11142
11145
|
await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
|
|
11143
11146
|
sessionId: message.sessionId
|
|
11144
11147
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voicethere/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "VoiceThere customer agent SDK — IPC types and runtime helpers for sandboxed child bundles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,6 +28,12 @@
|
|
|
28
28
|
"require": "./dist/templates/index.js",
|
|
29
29
|
"default": "./dist/templates/index.js"
|
|
30
30
|
},
|
|
31
|
+
"./build": {
|
|
32
|
+
"types": "./dist/build-bundle.d.ts",
|
|
33
|
+
"import": "./dist/build-bundle.js",
|
|
34
|
+
"require": "./dist/build-bundle.js",
|
|
35
|
+
"default": "./dist/build-bundle.js"
|
|
36
|
+
},
|
|
31
37
|
"./templates/redis-sync/world-layout": {
|
|
32
38
|
"types": "./templates/redis-sync/world-layout.ts",
|
|
33
39
|
"import": "./templates/redis-sync/world-layout.ts",
|
package/templates/README.md
CHANGED
|
@@ -15,10 +15,10 @@ import {
|
|
|
15
15
|
|
|
16
16
|
## Product vs e2e
|
|
17
17
|
|
|
18
|
-
| Kind | Dashboard create
|
|
19
|
-
| ----------- |
|
|
20
|
-
| **product** | Yes (`echo`, `echo-dc`, `voice-starter`, `game-sync`, `voice-showcase`, `webhooks`, `webhooks-redis`) | Yes — `dist/templates/<id>/agent.js` | Platform project create |
|
|
21
|
-
| **e2e** | No
|
|
18
|
+
| Kind | Dashboard create | Prebuilt seed bundle | Typical consumer |
|
|
19
|
+
| ----------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ----------------------- |
|
|
20
|
+
| **product** | Yes (`echo`, `echo-dc`, `voice-starter`, `game-sync`, `voice-showcase`, `recording-consent`, `webhooks`, `webhooks-redis`) | Yes — `dist/templates/<id>/agent.js` | Platform project create |
|
|
21
|
+
| **e2e** | No | No — build from sources at test time | `voicethere/e2e` smokes |
|
|
22
22
|
|
|
23
23
|
Product templates always set `seedOnCreate: true`. CI fails if a product template is missing its prebuilt bundle after `npm run build`.
|
|
24
24
|
|
|
@@ -63,9 +63,15 @@ When `AGENT_REDIS_URL` is set (project Redis), the world blob is stored at `game
|
|
|
63
63
|
|
|
64
64
|
### `voice-showcase/` (`voice-showcase`)
|
|
65
65
|
|
|
66
|
-
Conversational voice demo for landing and dashboard previews — greets the user, asks for a name, then offers a menu: weather (Open-Meteo, no API key), count 1–10, short recipes, and rotating fun facts. Typed chat and voice finals share the same handler. Sends structured `menu` payloads plus `chat_reply` for the chat log.
|
|
66
|
+
Conversational voice demo for landing and dashboard previews — greets the user, asks for a name, then offers a menu: weather (Open-Meteo, no API key), count 1–10, short recipes, and rotating fun facts. Typed chat and voice finals share the same handler. Sends structured `menu` payloads plus `chat_reply` for the chat log, then triggers TTS play so the client has the spoken text before audio starts.
|
|
67
67
|
|
|
68
|
-
Sources: `voice-showcase/agent.ts` (defineAgent wiring), `conversation.ts` (pure state machine), `weather.ts`, `recipes.ts`, `fun-facts.ts`.
|
|
68
|
+
Sources: `voice-showcase/agent.ts` (defineAgent wiring), `conversation.ts` (pure state machine), `delivery.ts` (send-then-play order), `weather.ts`, `recipes.ts`, `fun-facts.ts`.
|
|
69
|
+
|
|
70
|
+
### `recording-consent/` (`recording-consent`)
|
|
71
|
+
|
|
72
|
+
Demonstrates conversation recording consent on connect: asks whether recording is OK when the project has recording enabled, pauses capture while collecting name and date of birth, then resumes only if the customer consented. When project recording is off, logs a warning and never calls `startRecording`. Voice finals and typed chat share the same handler.
|
|
73
|
+
|
|
74
|
+
Sources: `recording-consent/agent.ts` (defineAgent wiring), `conversation.ts` (pure state machine).
|
|
69
75
|
|
|
70
76
|
### `webhooks.ts` (`webhooks`)
|
|
71
77
|
|
|
@@ -162,6 +162,51 @@ export function collectActiveObjectIds(world: Float32Array): number[] {
|
|
|
162
162
|
return ids;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
export interface LiveWorldObjectInfo {
|
|
166
|
+
objectId: number;
|
|
167
|
+
ownerSessionId: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Snapshot occupancy from live world slots; ownerSessionId from map or empty string. */
|
|
171
|
+
export function liveWorldSnapshot(
|
|
172
|
+
world: Float32Array,
|
|
173
|
+
owners: ReadonlyMap<number, string>,
|
|
174
|
+
): LiveWorldObjectInfo[] {
|
|
175
|
+
const ids = collectActiveObjectIds(world);
|
|
176
|
+
ids.sort((a, b) => a - b);
|
|
177
|
+
return ids.map((objectId) => ({
|
|
178
|
+
objectId,
|
|
179
|
+
ownerSessionId: owners.get(objectId) ?? "",
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function planRedisSimTick(options: {
|
|
184
|
+
lockAcquired: boolean;
|
|
185
|
+
connectedSessions: ReadonlySet<string>;
|
|
186
|
+
}): "simulate" | "relay" | "noop" {
|
|
187
|
+
if (options.lockAcquired) {
|
|
188
|
+
return "simulate";
|
|
189
|
+
}
|
|
190
|
+
if (options.connectedSessions.size > 0) {
|
|
191
|
+
return "relay";
|
|
192
|
+
}
|
|
193
|
+
return "noop";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Finite positive elapsed seconds, clamped to [1/fallbackHz, 0.05]. */
|
|
197
|
+
export function clampSimulationDtSec(
|
|
198
|
+
elapsedMs: number,
|
|
199
|
+
fallbackHz: number,
|
|
200
|
+
): number {
|
|
201
|
+
const minDt = 1 / fallbackHz;
|
|
202
|
+
const maxDt = 0.05;
|
|
203
|
+
if (!Number.isFinite(elapsedMs) || elapsedMs <= 0) {
|
|
204
|
+
return minDt;
|
|
205
|
+
}
|
|
206
|
+
const elapsedSec = elapsedMs / 1000;
|
|
207
|
+
return Math.min(maxDt, Math.max(minDt, elapsedSec));
|
|
208
|
+
}
|
|
209
|
+
|
|
165
210
|
/**
|
|
166
211
|
* Decode a Redis / Node Buffer into a fixed-size world Float32Array.
|
|
167
212
|
*/
|
package/templates/game-sync.ts
CHANGED
|
@@ -61,15 +61,17 @@ import {
|
|
|
61
61
|
simulateWorldStep,
|
|
62
62
|
} from "./game-sync-sim.js";
|
|
63
63
|
import {
|
|
64
|
+
clampSimulationDtSec,
|
|
64
65
|
collectActiveObjectIds,
|
|
65
|
-
commitSimulatedWorld,
|
|
66
66
|
countLiveObjects,
|
|
67
67
|
createEmptyWorldBuffer,
|
|
68
68
|
findFirstEmptySlot,
|
|
69
|
+
liveWorldSnapshot,
|
|
69
70
|
markSlotFree,
|
|
70
71
|
normalizeWorldBuffer,
|
|
71
72
|
objectIdToSlot,
|
|
72
73
|
OBJECT_SLOT_BYTE_LENGTH,
|
|
74
|
+
planRedisSimTick,
|
|
73
75
|
REDIS_SIM_LOCK_KEY,
|
|
74
76
|
REDIS_WORLD_KEY,
|
|
75
77
|
slotToObjectId,
|
|
@@ -92,6 +94,7 @@ let worldState = createEmptyWorldBuffer();
|
|
|
92
94
|
let redis: Redis | null = null;
|
|
93
95
|
let broadcastTimer: NodeJS.Timeout | null = null;
|
|
94
96
|
let worldMutationChain: Promise<void> = Promise.resolve();
|
|
97
|
+
let lastTickTimeMs = 0;
|
|
95
98
|
|
|
96
99
|
function withWorldMutation<T>(fn: () => Promise<T>): Promise<T> {
|
|
97
100
|
const run = worldMutationChain.then(fn);
|
|
@@ -140,11 +143,6 @@ async function withRedisSimLock<T>(
|
|
|
140
143
|
}
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
interface TrackedObjectInfo {
|
|
144
|
-
objectId: number;
|
|
145
|
-
ownerSessionId: string;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
146
|
function rand(min: number, max: number): number {
|
|
149
147
|
return min + Math.random() * (max - min);
|
|
150
148
|
}
|
|
@@ -315,13 +313,11 @@ async function unregisterObject(
|
|
|
315
313
|
return { ok: true, objectId: target.objectId };
|
|
316
314
|
}
|
|
317
315
|
|
|
318
|
-
function
|
|
319
|
-
const objects
|
|
320
|
-
for (const
|
|
321
|
-
|
|
316
|
+
function broadcastWorldSnapshot(): void {
|
|
317
|
+
const objects = liveWorldSnapshot(worldState, objectOwners);
|
|
318
|
+
for (const sessionId of connectedSessions) {
|
|
319
|
+
sendToClient(sessionId, { type: "world_snapshot", objects });
|
|
322
320
|
}
|
|
323
|
-
objects.sort((a, b) => a.objectId - b.objectId);
|
|
324
|
-
return objects;
|
|
325
321
|
}
|
|
326
322
|
|
|
327
323
|
function notifyObjectRegistered(
|
|
@@ -375,33 +371,47 @@ async function saveWorldToRedis(world: Float32Array): Promise<void> {
|
|
|
375
371
|
}
|
|
376
372
|
|
|
377
373
|
async function runSimulationTick(): Promise<void> {
|
|
374
|
+
const now = Date.now();
|
|
375
|
+
const dt = clampSimulationDtSec(
|
|
376
|
+
lastTickTimeMs === 0 ? 0 : now - lastTickTimeMs,
|
|
377
|
+
BROADCAST_HZ,
|
|
378
|
+
);
|
|
379
|
+
lastTickTimeMs = now;
|
|
380
|
+
|
|
378
381
|
if (!redis) {
|
|
379
382
|
await withWorldMutation(async () => {
|
|
380
|
-
|
|
381
|
-
simulateWorldStep(worldState, 1 / BROADCAST_HZ, activeObjectIds);
|
|
383
|
+
simulateWorldStep(worldState, dt, collectActiveObjectIds(worldState));
|
|
382
384
|
});
|
|
383
385
|
broadcastWorldBuffer(worldState);
|
|
384
386
|
return;
|
|
385
387
|
}
|
|
386
388
|
|
|
387
389
|
await withWorldMutation(async () => {
|
|
388
|
-
await withRedisSimLock(
|
|
390
|
+
const lockResult = await withRedisSimLock(
|
|
389
391
|
async () => {
|
|
390
392
|
const world = await loadWorldFromRedis();
|
|
391
393
|
const activeObjectIds = collectActiveObjectIds(world);
|
|
392
|
-
simulateWorldStep(world,
|
|
393
|
-
const latestRedis = await loadWorldFromRedis();
|
|
394
|
-
commitSimulatedWorld(world, latestRedis);
|
|
394
|
+
simulateWorldStep(world, dt, activeObjectIds);
|
|
395
395
|
await saveWorldToRedis(world);
|
|
396
396
|
worldState = world;
|
|
397
|
+
broadcastWorldBuffer(world);
|
|
398
|
+
return true;
|
|
397
399
|
},
|
|
398
400
|
{ retryUntilAcquired: false },
|
|
399
401
|
);
|
|
400
|
-
});
|
|
401
402
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
403
|
+
if (lockResult === null) {
|
|
404
|
+
const plan = planRedisSimTick({
|
|
405
|
+
lockAcquired: false,
|
|
406
|
+
connectedSessions,
|
|
407
|
+
});
|
|
408
|
+
if (plan === "relay") {
|
|
409
|
+
const world = await loadWorldFromRedis();
|
|
410
|
+
worldState = world;
|
|
411
|
+
broadcastWorldBuffer(world);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
405
415
|
}
|
|
406
416
|
|
|
407
417
|
function startBroadcastLoopIfNeeded(): void {
|
|
@@ -466,12 +476,15 @@ defineAgent({
|
|
|
466
476
|
agentLog("info", "game-sync agent connected to project Redis world buffer");
|
|
467
477
|
},
|
|
468
478
|
|
|
469
|
-
onClientJoin({ sessionId }) {
|
|
479
|
+
async onClientJoin({ sessionId }) {
|
|
470
480
|
connectedSessions.add(sessionId);
|
|
471
481
|
startBroadcastLoopIfNeeded();
|
|
482
|
+
if (redis) {
|
|
483
|
+
worldState = await loadWorldFromRedis();
|
|
484
|
+
}
|
|
472
485
|
sendToClient(sessionId, {
|
|
473
486
|
type: "world_snapshot",
|
|
474
|
-
objects:
|
|
487
|
+
objects: liveWorldSnapshot(worldState, objectOwners),
|
|
475
488
|
});
|
|
476
489
|
agentLog("info", `join ${sessionId} connected=${connectedSessions.size}`);
|
|
477
490
|
},
|
|
@@ -516,6 +529,7 @@ defineAgent({
|
|
|
516
529
|
}
|
|
517
530
|
sendToClient(ctx.sessionId, { type: "register_ack", objectId });
|
|
518
531
|
notifyObjectRegistered(objectId, ctx.sessionId);
|
|
532
|
+
broadcastWorldSnapshot();
|
|
519
533
|
agentLog(
|
|
520
534
|
"info",
|
|
521
535
|
`register session=${ctx.sessionId} objectId=${objectId}`,
|
|
@@ -541,6 +555,7 @@ defineAgent({
|
|
|
541
555
|
type: "unregister_ack",
|
|
542
556
|
objectId: result.objectId,
|
|
543
557
|
});
|
|
558
|
+
broadcastWorldSnapshot();
|
|
544
559
|
agentLog(
|
|
545
560
|
"info",
|
|
546
561
|
`unregister_ack session=${ctx.sessionId} objectId=${result.objectId}`,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recording consent template — ask consent, pause for PII, resume when allowed.
|
|
3
|
+
*
|
|
4
|
+
* Build:
|
|
5
|
+
* npx @voicethere/agent build --entry templates/recording-consent/agent.ts
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
agentLog,
|
|
9
|
+
defineAgent,
|
|
10
|
+
parseChatText,
|
|
11
|
+
pauseRecording,
|
|
12
|
+
resumeRecording,
|
|
13
|
+
sendToClient,
|
|
14
|
+
speak,
|
|
15
|
+
stopRecording,
|
|
16
|
+
type SpeechEvent,
|
|
17
|
+
} from "@voicethere/agent";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
beginSession,
|
|
21
|
+
handleUtterance,
|
|
22
|
+
type ConversationState,
|
|
23
|
+
type ConversationTurnResult,
|
|
24
|
+
type OutboundMessage,
|
|
25
|
+
type RecordingAction,
|
|
26
|
+
} from "./conversation.js";
|
|
27
|
+
|
|
28
|
+
let sessions = new Map<string, ConversationState>();
|
|
29
|
+
/** Per-session consent flag (shared-child safe — not one process-wide boolean). */
|
|
30
|
+
let consentBySessionId = new Map<string, boolean>();
|
|
31
|
+
|
|
32
|
+
function getState(sessionId: string): ConversationState {
|
|
33
|
+
const state = sessions.get(sessionId);
|
|
34
|
+
if (!state) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`recording-consent: missing session state for ${sessionId}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function relaySpeechEvent(sessionId: string, event: SpeechEvent): void {
|
|
43
|
+
sendToClient(sessionId, {
|
|
44
|
+
type: "agent_event",
|
|
45
|
+
event: event.type,
|
|
46
|
+
text: event.text,
|
|
47
|
+
raw: event,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function deliverMessages(sessionId: string, messages: OutboundMessage[]): void {
|
|
52
|
+
for (const message of messages) {
|
|
53
|
+
sendToClient(sessionId, message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function speakLines(sessionId: string, lines: string[]): void {
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
speak(sessionId, line);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function applyRecordingAction(
|
|
64
|
+
sessionId: string,
|
|
65
|
+
action: RecordingAction,
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
if (!action) return;
|
|
68
|
+
switch (action) {
|
|
69
|
+
case "pause":
|
|
70
|
+
await pauseRecording(sessionId);
|
|
71
|
+
break;
|
|
72
|
+
case "stop":
|
|
73
|
+
await stopRecording(sessionId);
|
|
74
|
+
break;
|
|
75
|
+
case "start":
|
|
76
|
+
case "resume":
|
|
77
|
+
await resumeRecording(sessionId);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function applyTurn(
|
|
83
|
+
sessionId: string,
|
|
84
|
+
result: ConversationTurnResult,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
sessions.set(sessionId, result.state);
|
|
87
|
+
if (result.state.consent !== undefined) {
|
|
88
|
+
consentBySessionId.set(sessionId, result.state.consent);
|
|
89
|
+
}
|
|
90
|
+
if (result.warnRecordingDisabled) {
|
|
91
|
+
agentLog(
|
|
92
|
+
"warn",
|
|
93
|
+
"Project conversation recording is disabled; skipping consent and will not call startRecording",
|
|
94
|
+
sessionId,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
speakLines(sessionId, result.speakLines);
|
|
98
|
+
deliverMessages(sessionId, result.messages);
|
|
99
|
+
await applyRecordingAction(sessionId, result.recordingAction);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function onUserText(sessionId: string, text: string): Promise<void> {
|
|
103
|
+
const state = getState(sessionId);
|
|
104
|
+
const result = handleUtterance(state, text);
|
|
105
|
+
await applyTurn(sessionId, result);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
defineAgent({
|
|
109
|
+
onAgentStart() {
|
|
110
|
+
sessions = new Map();
|
|
111
|
+
consentBySessionId = new Map();
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
onSessionStart({ sessionId, recordingAvailable }) {
|
|
115
|
+
const result = beginSession(recordingAvailable);
|
|
116
|
+
sessions.set(sessionId, result.state);
|
|
117
|
+
if (result.warnRecordingDisabled) {
|
|
118
|
+
agentLog(
|
|
119
|
+
"warn",
|
|
120
|
+
"Project conversation recording is disabled; skipping consent and will not call startRecording",
|
|
121
|
+
sessionId,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
speakLines(sessionId, result.speakLines);
|
|
125
|
+
deliverMessages(sessionId, result.messages);
|
|
126
|
+
sendToClient(sessionId, {
|
|
127
|
+
type: "agent_event",
|
|
128
|
+
event: "session_start",
|
|
129
|
+
sessionId,
|
|
130
|
+
});
|
|
131
|
+
agentLog(
|
|
132
|
+
"info",
|
|
133
|
+
`recording-consent session_start ${sessionId} recordingAvailable=${recordingAvailable}`,
|
|
134
|
+
sessionId,
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
onSpeechEvent({ sessionId }, event: SpeechEvent) {
|
|
139
|
+
relaySpeechEvent(sessionId, event);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
onUserSpeechFinal({ sessionId, text }) {
|
|
143
|
+
void onUserText(sessionId, text);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
onDataChannelMessage(ctx) {
|
|
147
|
+
const text = parseChatText(ctx.message);
|
|
148
|
+
if (!text) return;
|
|
149
|
+
void onUserText(ctx.sessionId, text);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
onSessionEnd({ sessionId }) {
|
|
153
|
+
sessions.delete(sessionId);
|
|
154
|
+
consentBySessionId.delete(sessionId);
|
|
155
|
+
sendToClient(sessionId, {
|
|
156
|
+
type: "agent_event",
|
|
157
|
+
event: "session_end",
|
|
158
|
+
sessionId,
|
|
159
|
+
});
|
|
160
|
+
agentLog("info", `recording-consent session_end ${sessionId}`, sessionId);
|
|
161
|
+
},
|
|
162
|
+
});
|