@voicethere/agent 0.1.10 → 0.1.11

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 CHANGED
@@ -94,6 +94,18 @@ Forwarded from the runner voice pipeline as SDK `SpeechEvent` payloads on `speec
94
94
 
95
95
  Copy [`templates/agent.ts`](./templates/agent.ts) as a starting point — exhaustive `switch` over all 14 `SpeechEvent` types with per-peer state stubs and `agentLog` tracing.
96
96
 
97
+ ## Multiplayer / shared state
98
+
99
+ Each end-user connection must call **`startSession()`** once — one orchestrator session id per client (one signaling room per client). Do **not** join multiple browsers to the same session credentials.
100
+
101
+ When the VoiceThere project has **`shared_child_per_session`** enabled, every session on a runner pod shares **one** sandboxed `agent.js` process. Your handlers receive `sessionId` equal to the orchestrator session id (see `onClientJoin` / `onSessionStart`). Use that id with `speak`, `sendToClient`, and `broadcastToClients` to target the right WebRTC client.
102
+
103
+ The runtime processes parent IPC **in order per `sessionId`** while different sessions run independently — safe for greet-on-join and load with many concurrent connections.
104
+
105
+ For isolated voice agents (default), leave **`shared_child_per_session`** disabled — each session gets its own child process.
106
+
107
+ See [`templates/game-sync.ts`](./templates/game-sync.ts) for a data-only authoritative server example.
108
+
97
109
  ## Building your agent bundle
98
110
 
99
111
  **Recommended:** bundle with the package CLI (same esbuild settings VoiceThere uses in production):
package/dist/agent.js CHANGED
@@ -1,8 +1,31 @@
1
+ // src/session-serial-queue.ts
2
+ var SessionSerialQueue = class {
3
+ tails = /* @__PURE__ */ new Map();
4
+ enqueue(sessionId, task) {
5
+ const previous = this.tails.get(sessionId)?.catch(() => void 0) ?? Promise.resolve();
6
+ const next = previous.then(() => Promise.resolve(task()));
7
+ this.tails.set(
8
+ sessionId,
9
+ next.finally(() => {
10
+ if (this.tails.get(sessionId) === next) {
11
+ this.tails.delete(sessionId);
12
+ }
13
+ })
14
+ );
15
+ }
16
+ clear(sessionId) {
17
+ this.tails.delete(sessionId);
18
+ }
19
+ clearAll() {
20
+ this.tails.clear();
21
+ }
22
+ };
23
+
1
24
  // src/runtime.ts
2
25
  function isParentMessage(value) {
3
26
  if (!value || typeof value !== "object") return false;
4
27
  const msg = value;
5
- return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary";
28
+ 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";
6
29
  }
7
30
  function parseDataChannelPayload(raw) {
8
31
  try {
@@ -11,63 +34,136 @@ function parseDataChannelPayload(raw) {
11
34
  return raw;
12
35
  }
13
36
  }
37
+ var peerEnvBySessionId = /* @__PURE__ */ new Map();
38
+ async function handleParentMessage(message, handlers) {
39
+ switch (message.type) {
40
+ case "session_start":
41
+ peerEnvBySessionId.set(message.sessionId, message.env);
42
+ await (handlers.onClientJoin ?? handlers.onSessionStart)?.({
43
+ sessionId: message.sessionId,
44
+ env: message.env
45
+ });
46
+ break;
47
+ case "speech_event":
48
+ await handlers.onSpeechEvent?.(
49
+ { sessionId: message.sessionId },
50
+ message.event
51
+ );
52
+ if (message.event.type === "user_speech_final" && typeof message.event.text === "string" && message.event.text.trim()) {
53
+ await handlers.onUserSpeechFinal?.({
54
+ sessionId: message.sessionId,
55
+ text: message.event.text.trim()
56
+ });
57
+ }
58
+ break;
59
+ case "data_channel_message":
60
+ await handlers.onDataChannelMessage?.({
61
+ sessionId: message.sessionId,
62
+ message: parseDataChannelPayload(message.payload),
63
+ raw: message.payload,
64
+ rawBinary: null,
65
+ channel: "control"
66
+ });
67
+ break;
68
+ case "data_channel_binary":
69
+ await handlers.onDataChannelBinary?.({
70
+ sessionId: message.sessionId,
71
+ message: null,
72
+ raw: null,
73
+ rawBinary: message.data,
74
+ channel: message.channel ?? "sync"
75
+ });
76
+ break;
77
+ case "session_end":
78
+ peerEnvBySessionId.delete(message.sessionId);
79
+ await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
80
+ sessionId: message.sessionId
81
+ });
82
+ break;
83
+ case "idle_timeout":
84
+ await runIdleTimeoutHook(handlers, message);
85
+ break;
86
+ }
87
+ }
14
88
  function defineAgent(handlers) {
89
+ const inboundBySession = new SessionSerialQueue();
15
90
  process.on("message", (message) => {
16
91
  if (!isParentMessage(message)) return;
17
- void (async () => {
92
+ inboundBySession.enqueue(message.sessionId, async () => {
18
93
  try {
19
- switch (message.type) {
20
- case "session_start":
21
- await handlers.onSessionStart?.({
22
- sessionId: message.sessionId,
23
- env: message.env
24
- });
25
- break;
26
- case "speech_event":
27
- await handlers.onSpeechEvent?.(
28
- { sessionId: message.sessionId },
29
- message.event
30
- );
31
- if (message.event.type === "user_speech_final" && typeof message.event.text === "string" && message.event.text.trim()) {
32
- await handlers.onUserSpeechFinal?.({
33
- sessionId: message.sessionId,
34
- text: message.event.text.trim()
35
- });
36
- }
37
- break;
38
- case "data_channel_message":
39
- await handlers.onDataChannelMessage?.({
40
- sessionId: message.sessionId,
41
- message: parseDataChannelPayload(message.payload),
42
- raw: message.payload,
43
- rawBinary: null,
44
- channel: "control"
45
- });
46
- break;
47
- case "data_channel_binary":
48
- await handlers.onDataChannelBinary?.({
49
- sessionId: message.sessionId,
50
- message: null,
51
- raw: null,
52
- rawBinary: message.data,
53
- channel: message.channel ?? "sync"
54
- });
55
- break;
56
- case "session_end":
57
- await handlers.onSessionEnd?.({ sessionId: message.sessionId });
58
- break;
59
- }
94
+ await handleParentMessage(message, handlers);
60
95
  } catch (error) {
61
- const errMessage = error instanceof Error ? error.message : String(error);
96
+ const err = error instanceof Error ? error : new Error(String(error));
97
+ const env = peerEnvBySessionId.get(message.sessionId) ?? buildIdleEnv(message.sessionId);
98
+ await runErrorHook(handlers, {
99
+ sessionId: message.sessionId,
100
+ projectId: env.PROJECT_ID,
101
+ buildId: env.BUILD_ID,
102
+ env,
103
+ error: err,
104
+ customerContext: parseCustomerContext(env.AGENT_CUSTOMER_CONTEXT)
105
+ });
62
106
  process.send?.({
63
107
  type: "agent_error",
64
108
  sessionId: message.sessionId,
65
- message: errMessage
109
+ message: err.message,
110
+ stack: err.stack
66
111
  });
67
112
  }
68
- })();
113
+ });
114
+ });
115
+ }
116
+ function parseCustomerContext(raw) {
117
+ if (!raw?.trim()) return void 0;
118
+ try {
119
+ const parsed = JSON.parse(raw);
120
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
121
+ return parsed;
122
+ }
123
+ } catch {
124
+ }
125
+ return void 0;
126
+ }
127
+ async function runErrorHook(handlers, ctx) {
128
+ if (!handlers.errorHook) return;
129
+ try {
130
+ await handlers.errorHook(ctx);
131
+ } catch (hookError) {
132
+ const message = hookError instanceof Error ? hookError.message : String(hookError);
133
+ agentLog("error", `errorHook failed: ${message}`);
134
+ }
135
+ }
136
+ async function runIdleTimeoutHook(handlers, message) {
137
+ const env = peerEnvBySessionId.get(message.sessionId) ?? buildIdleEnv(message.sessionId);
138
+ const idleTimeoutSeconds = Number(env.IDLE_TIMEOUT_SEC) || 0;
139
+ const ctx = {
140
+ sessionId: message.sessionId,
141
+ projectId: env.PROJECT_ID,
142
+ buildId: env.BUILD_ID,
143
+ env,
144
+ idleTimeoutSeconds
145
+ };
146
+ let error;
147
+ try {
148
+ await handlers.onIdleTimeout?.(ctx);
149
+ } catch (hookError) {
150
+ error = hookError instanceof Error ? hookError.message : String(hookError);
151
+ agentLog("error", `onIdleTimeout failed: ${error}`);
152
+ }
153
+ process.send?.({
154
+ type: "idle_timeout_done",
155
+ sessionId: message.sessionId,
156
+ error
69
157
  });
70
158
  }
159
+ function buildIdleEnv(sessionId) {
160
+ return {
161
+ SESSION_ID: sessionId,
162
+ ...process.env.PROJECT_ID ? { PROJECT_ID: process.env.PROJECT_ID } : {},
163
+ ...process.env.BUILD_ID ? { BUILD_ID: process.env.BUILD_ID } : {},
164
+ ...process.env.IDLE_TIMEOUT_SEC ? { IDLE_TIMEOUT_SEC: process.env.IDLE_TIMEOUT_SEC } : {}
165
+ };
166
+ }
71
167
  function speak(sessionId, text) {
72
168
  process.send?.({ type: "speak", sessionId, text });
73
169
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export type { SpeechEvent, SpeechEventListener, SpeechEventName, SpeechEventType, } from "@node-webrtc-rust/sdk/voice";
2
- export { ALLOWED_CHILD_ENV_KEYS, type AgentErrorMessage, type AgentLogMessage, type AllowedChildEnvKey, type ChildToParentMessage, type ParentToChildMessage, type SessionEndMessage, type SessionStartMessage, type SendToClientMessage, type DataChannelMessageMessage, type DataChannelBinaryMessage, type DataChannelKind, type SendBinaryToClientMessage, type SpeakMessage, type SpeechEventMessage, } from "./protocol.js";
3
- export { agentLog, defineAgent, parseChatText, sendToClient, sendBinaryToClient, speak, type AgentHandlers, type DataChannelContext, type SessionContext, type SpeechContext, type SpeechEventContext, } from "./runtime.js";
2
+ export { ALLOWED_CHILD_ENV_KEYS, type AgentErrorMessage, type AgentLogMessage, type AllowedChildEnvKey, type ChildToParentMessage, type ParentToChildMessage, type SessionEndMessage, type SessionStartMessage, type SendToClientMessage, type DataChannelMessageMessage, type DataChannelBinaryMessage, type DataChannelKind, type DisconnectClientMessage, type IdleTimeoutDoneMessage, type IdleTimeoutMessage, type SendBinaryToClientMessage, type SpeakMessage, type SpeechEventMessage, } from "./protocol.js";
3
+ export { agentLog, defineAgent, disconnectClient, parseChatText, sendToClient, sendBinaryToClient, speak, broadcastToClients, type AgentHandlers, type AgentErrorContext, type DataChannelContext, type IdleTimeoutContext, type SessionContext, type SpeechContext, type SpeechEventContext, } from "./runtime.js";
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,eAAe,GAChB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,sBAAsB,EACtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,KAAK,eAAe,EACpB,KAAK,yBAAyB,EAC9B,KAAK,YAAY,EACjB,KAAK,kBAAkB,GACxB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,QAAQ,EACR,WAAW,EACX,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,KAAK,EACL,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,eAAe,GAChB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,sBAAsB,EACtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,KAAK,YAAY,EACjB,KAAK,kBAAkB,GACxB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,QAAQ,EACR,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,KAAK,EACL,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { ALLOWED_CHILD_ENV_KEYS, } from "./protocol.js";
2
- export { agentLog, defineAgent, parseChatText, sendToClient, sendBinaryToClient, speak, } from "./runtime.js";
2
+ export { agentLog, defineAgent, disconnectClient, parseChatText, sendToClient, sendBinaryToClient, speak, broadcastToClients, } from "./runtime.js";
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,sBAAsB,GAevB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,QAAQ,EACR,WAAW,EACX,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,KAAK,GAMN,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,sBAAsB,GAkBvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,QAAQ,EACR,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,KAAK,EACL,kBAAkB,GAQnB,MAAM,cAAc,CAAC"}
@@ -15,13 +15,13 @@ import type { SpeechEvent } from "@node-webrtc-rust/sdk/voice";
15
15
  * Register handlers via {@link defineAgent} in `@voicethere/agent` — do not read
16
16
  * `process.on('message')` directly in customer bundles.
17
17
  */
18
- export type ParentToChildMessage = SessionStartMessage | SpeechEventMessage | SessionEndMessage | DataChannelMessageMessage | DataChannelBinaryMessage;
18
+ export type ParentToChildMessage = SessionStartMessage | SpeechEventMessage | SessionEndMessage | DataChannelMessageMessage | DataChannelBinaryMessage | IdleTimeoutMessage;
19
19
  /**
20
20
  * Messages the customer child may send back to the runner parent.
21
21
  *
22
22
  * Prefer {@link speak}, {@link sendToClient}, {@link sendBinaryToClient}, and {@link agentLog} helpers over raw `process.send`.
23
23
  */
24
- export type ChildToParentMessage = SpeakMessage | AgentLogMessage | AgentErrorMessage | SendToClientMessage | SendBinaryToClientMessage;
24
+ export type ChildToParentMessage = SpeakMessage | AgentLogMessage | AgentErrorMessage | SendToClientMessage | SendBinaryToClientMessage | IdleTimeoutDoneMessage | DisconnectClientMessage;
25
25
  /** Which WebRTC data channel carried a binary IPC payload. */
26
26
  export type DataChannelKind = "control" | "sync";
27
27
  /**
@@ -119,6 +119,8 @@ export interface AgentErrorMessage {
119
119
  sessionId: string;
120
120
  /** Human-readable error summary (no stack traces required). */
121
121
  message: string;
122
+ /** Optional stack trace from handler throw. */
123
+ stack?: string;
122
124
  }
123
125
  /**
124
126
  * Send a JSON-serializable payload to the browser peer over the WebRTC data channel.
@@ -135,13 +137,39 @@ export interface SendBinaryToClientMessage {
135
137
  data: Buffer;
136
138
  channel?: DataChannelKind;
137
139
  }
140
+ /**
141
+ * Idle timeout fired — run {@link AgentHandlers.onIdleTimeout} before disconnect.
142
+ */
143
+ export interface IdleTimeoutMessage {
144
+ type: "idle_timeout";
145
+ sessionId: string;
146
+ /** Wall-clock grace for the customer callback (default 30000). */
147
+ maxGraceMs: number;
148
+ }
149
+ /**
150
+ * Customer callback finished (or failed) after {@link IdleTimeoutMessage}.
151
+ */
152
+ export interface IdleTimeoutDoneMessage {
153
+ type: "idle_timeout_done";
154
+ sessionId: string;
155
+ /** Set when the customer hook threw or rejected. */
156
+ error?: string;
157
+ }
158
+ /**
159
+ * Ask the runner to disconnect a browser peer (customer-initiated).
160
+ */
161
+ export interface DisconnectClientMessage {
162
+ type: "disconnect_client";
163
+ sessionId: string;
164
+ reason?: string;
165
+ }
138
166
  /**
139
167
  * Environment variable names the runner may inject into {@link SessionStartMessage.env}.
140
168
  *
141
169
  * The runner may add more project-specific keys over time; customer bundles must
142
170
  * not read `process.env` directly — only the `env` object on session start.
143
171
  */
144
- export declare const ALLOWED_CHILD_ENV_KEYS: readonly ["SESSION_ID", "PROJECT_ID", "BUILD_ID"];
172
+ export declare const ALLOWED_CHILD_ENV_KEYS: readonly ["SESSION_ID", "PROJECT_ID", "BUILD_ID", "IDLE_TIMEOUT_SEC", "AGENT_CUSTOMER_CONTEXT"];
145
173
  /** Union of allowlisted env key names. */
146
174
  export type AllowedChildEnvKey = (typeof ALLOWED_CHILD_ENV_KEYS)[number];
147
175
  //# sourceMappingURL=protocol.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAC5B,mBAAmB,GACnB,kBAAkB,GAClB,iBAAiB,GACjB,yBAAyB,GACzB,wBAAwB,CAAC;AAE7B;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B,YAAY,GACZ,eAAe,GACf,iBAAiB,GACjB,mBAAmB,GACnB,yBAAyB,CAAC;AAE9B,8DAA8D;AAC9D,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,MAAM,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,cAAc,CAAC;IACrB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,KAAK,EAAE,WAAW,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,aAAa,CAAC;IACpB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,aAAa,CAAC;IACpB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,gBAAgB,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qEAAqE;AACrE,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,mDAIzB,CAAC;AAEX,0CAA0C;AAC1C,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC"}
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAC5B,mBAAmB,GACnB,kBAAkB,GAClB,iBAAiB,GACjB,yBAAyB,GACzB,wBAAwB,GACxB,kBAAkB,CAAC;AAEvB;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B,YAAY,GACZ,eAAe,GACf,iBAAiB,GACjB,mBAAmB,GACnB,yBAAyB,GACzB,sBAAsB,GACtB,uBAAuB,CAAC;AAE5B,8DAA8D;AAC9D,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,MAAM,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,cAAc,CAAC;IACrB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,KAAK,EAAE,WAAW,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,aAAa,CAAC;IACpB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,aAAa,CAAC;IACpB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,gBAAgB,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qEAAqE;AACrE,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,iGAOzB,CAAC;AAEX,0CAA0C;AAC1C,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC"}
package/dist/protocol.js CHANGED
@@ -18,5 +18,8 @@ export const ALLOWED_CHILD_ENV_KEYS = [
18
18
  "SESSION_ID",
19
19
  "PROJECT_ID",
20
20
  "BUILD_ID",
21
+ "IDLE_TIMEOUT_SEC",
22
+ /** JSON string — opaque customer context from browser session start. */
23
+ "AGENT_CUSTOMER_CONTEXT",
21
24
  ];
22
25
  //# sourceMappingURL=protocol.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAyJH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,YAAY;IACZ,YAAY;IACZ,UAAU;CACF,CAAC"}
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA2LH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,YAAY;IACZ,YAAY;IACZ,UAAU;IACV,kBAAkB;IAClB,wEAAwE;IACxE,wBAAwB;CAChB,CAAC"}
package/dist/runtime.d.ts CHANGED
@@ -21,11 +21,17 @@ export interface DataChannelContext {
21
21
  channel: DataChannelKind;
22
22
  }
23
23
  export interface AgentHandlers {
24
+ /** Alias for {@link AgentHandlers.onSessionStart}. */
25
+ onClientJoin?: (ctx: SessionContext) => void | Promise<void>;
24
26
  onSessionStart?: (ctx: SessionContext) => void | Promise<void>;
25
27
  /** Fired for every speech lifecycle event from the parent voice pipeline. */
26
28
  onSpeechEvent?: (ctx: SpeechEventContext, event: SpeechEvent) => void | Promise<void>;
27
29
  /** Convenience handler — also invoked when `speech.type` is `user_speech_final`. */
28
30
  onUserSpeechFinal?: (ctx: SpeechContext) => void | Promise<void>;
31
+ /** Alias for {@link AgentHandlers.onSessionEnd}. */
32
+ onClientLeave?: (ctx: {
33
+ sessionId: string;
34
+ }) => void | Promise<void>;
29
35
  onSessionEnd?: (ctx: {
30
36
  sessionId: string;
31
37
  }) => void | Promise<void>;
@@ -33,10 +39,38 @@ export interface AgentHandlers {
33
39
  onDataChannelMessage?: (ctx: DataChannelContext) => void | Promise<void>;
34
40
  /** Browser data channel binary (game state, custom framing). */
35
41
  onDataChannelBinary?: (ctx: DataChannelContext) => void | Promise<void>;
42
+ /**
43
+ * Idle timeout fired — run cleanup before the runner disconnects the peer.
44
+ * Must not throw; errors are logged and reported as session errors.
45
+ */
46
+ onIdleTimeout?: (ctx: IdleTimeoutContext) => void | Promise<void>;
47
+ /**
48
+ * Optional — runs when handler code throws before runner crash handling.
49
+ * Must not throw; hook errors are logged via {@link agentLog}.
50
+ */
51
+ errorHook?: (ctx: AgentErrorContext) => void | Promise<void>;
52
+ }
53
+ export interface IdleTimeoutContext {
54
+ sessionId: string;
55
+ projectId?: string;
56
+ buildId?: string;
57
+ env: Record<string, string>;
58
+ idleTimeoutSeconds: number;
59
+ }
60
+ export interface AgentErrorContext {
61
+ sessionId: string;
62
+ projectId?: string;
63
+ buildId?: string;
64
+ env: Record<string, string>;
65
+ error: Error;
66
+ customerContext?: Record<string, unknown>;
36
67
  }
37
68
  /**
38
69
  * Register IPC handlers for a customer agent child process.
39
70
  * Call once at bundle entry; runner parent sends {@link ParentToChildMessage} events.
71
+ *
72
+ * Parent messages for the same `sessionId` are handled strictly in arrival order;
73
+ * different sessions run independently (shared-child / load-safe).
40
74
  */
41
75
  export declare function defineAgent(handlers: AgentHandlers): void;
42
76
  /** Ask the runner parent to synthesize speech for the session. */
@@ -45,8 +79,14 @@ export declare function speak(sessionId: string, text: string): void;
45
79
  export declare function sendToClient(sessionId: string, payload: unknown): void;
46
80
  /** Send raw bytes to the browser peer via the runner parent. */
47
81
  export declare function sendBinaryToClient(sessionId: string, data: Buffer | Uint8Array, channel?: DataChannelKind): void;
82
+ /** Send the same JSON payload to one or more browser peers. */
83
+ export declare function broadcastToClients(payload: unknown, sessionIds: readonly string[]): void;
48
84
  /** Structured log forwarded to the runner parent. */
49
85
  export declare function agentLog(level: "info" | "error", message: string): void;
86
+ /** Ask the runner to disconnect a browser peer (customer-initiated). */
87
+ export declare function disconnectClient(sessionId: string, options?: {
88
+ reason?: string;
89
+ }): void;
50
90
  /** Extract chat text from a parsed data channel message. */
51
91
  export declare function parseChatText(message: unknown): string | null;
52
92
  //# sourceMappingURL=runtime.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAE/D,OAAO,KAAK,EACV,eAAe,EAEhB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,qEAAqE;IACrE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,eAAe,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,6EAA6E;IAC7E,aAAa,CAAC,EAAE,CACd,GAAG,EAAE,kBAAkB,EACvB,KAAK,EAAE,WAAW,KACf,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,6DAA6D;IAC7D,oBAAoB,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,gEAAgE;IAChE,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE;AAsBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CA8DzD;AAED,kEAAkE;AAClE,wBAAgB,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAEtE;AAED,gEAAgE;AAChE,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GAAG,UAAU,EACzB,OAAO,GAAE,eAAwB,GAChC,IAAI,CAGN;AAED,qDAAqD;AACrD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEvE;AAED,4DAA4D;AAC5D,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAM7D"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
package/dist/runtime.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { SessionSerialQueue } from "./session-serial-queue.js";
1
2
  function isParentMessage(value) {
2
3
  if (!value || typeof value !== "object")
3
4
  return false;
@@ -6,7 +7,8 @@ function isParentMessage(value) {
6
7
  msg.type === "speech_event" ||
7
8
  msg.type === "session_end" ||
8
9
  msg.type === "data_channel_message" ||
9
- msg.type === "data_channel_binary");
10
+ msg.type === "data_channel_binary" ||
11
+ msg.type === "idle_timeout");
10
12
  }
11
13
  function parseDataChannelPayload(raw) {
12
14
  try {
@@ -16,68 +18,155 @@ function parseDataChannelPayload(raw) {
16
18
  return raw;
17
19
  }
18
20
  }
21
+ const peerEnvBySessionId = new Map();
22
+ async function handleParentMessage(message, handlers) {
23
+ switch (message.type) {
24
+ case "session_start":
25
+ peerEnvBySessionId.set(message.sessionId, message.env);
26
+ await (handlers.onClientJoin ?? handlers.onSessionStart)?.({
27
+ sessionId: message.sessionId,
28
+ env: message.env,
29
+ });
30
+ break;
31
+ case "speech_event":
32
+ await handlers.onSpeechEvent?.({ sessionId: message.sessionId }, message.event);
33
+ if (message.event.type === "user_speech_final" &&
34
+ typeof message.event.text === "string" &&
35
+ message.event.text.trim()) {
36
+ await handlers.onUserSpeechFinal?.({
37
+ sessionId: message.sessionId,
38
+ text: message.event.text.trim(),
39
+ });
40
+ }
41
+ break;
42
+ case "data_channel_message":
43
+ await handlers.onDataChannelMessage?.({
44
+ sessionId: message.sessionId,
45
+ message: parseDataChannelPayload(message.payload),
46
+ raw: message.payload,
47
+ rawBinary: null,
48
+ channel: "control",
49
+ });
50
+ break;
51
+ case "data_channel_binary":
52
+ await handlers.onDataChannelBinary?.({
53
+ sessionId: message.sessionId,
54
+ message: null,
55
+ raw: null,
56
+ rawBinary: message.data,
57
+ channel: message.channel ?? "sync",
58
+ });
59
+ break;
60
+ case "session_end":
61
+ peerEnvBySessionId.delete(message.sessionId);
62
+ await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
63
+ sessionId: message.sessionId,
64
+ });
65
+ break;
66
+ case "idle_timeout":
67
+ await runIdleTimeoutHook(handlers, message);
68
+ break;
69
+ }
70
+ }
19
71
  /**
20
72
  * Register IPC handlers for a customer agent child process.
21
73
  * Call once at bundle entry; runner parent sends {@link ParentToChildMessage} events.
74
+ *
75
+ * Parent messages for the same `sessionId` are handled strictly in arrival order;
76
+ * different sessions run independently (shared-child / load-safe).
22
77
  */
23
78
  export function defineAgent(handlers) {
79
+ const inboundBySession = new SessionSerialQueue();
24
80
  process.on("message", (message) => {
25
81
  if (!isParentMessage(message))
26
82
  return;
27
- void (async () => {
83
+ inboundBySession.enqueue(message.sessionId, async () => {
28
84
  try {
29
- switch (message.type) {
30
- case "session_start":
31
- await handlers.onSessionStart?.({
32
- sessionId: message.sessionId,
33
- env: message.env,
34
- });
35
- break;
36
- case "speech_event":
37
- await handlers.onSpeechEvent?.({ sessionId: message.sessionId }, message.event);
38
- if (message.event.type === "user_speech_final" &&
39
- typeof message.event.text === "string" &&
40
- message.event.text.trim()) {
41
- await handlers.onUserSpeechFinal?.({
42
- sessionId: message.sessionId,
43
- text: message.event.text.trim(),
44
- });
45
- }
46
- break;
47
- case "data_channel_message":
48
- await handlers.onDataChannelMessage?.({
49
- sessionId: message.sessionId,
50
- message: parseDataChannelPayload(message.payload),
51
- raw: message.payload,
52
- rawBinary: null,
53
- channel: "control",
54
- });
55
- break;
56
- case "data_channel_binary":
57
- await handlers.onDataChannelBinary?.({
58
- sessionId: message.sessionId,
59
- message: null,
60
- raw: null,
61
- rawBinary: message.data,
62
- channel: message.channel ?? "sync",
63
- });
64
- break;
65
- case "session_end":
66
- await handlers.onSessionEnd?.({ sessionId: message.sessionId });
67
- break;
68
- }
85
+ await handleParentMessage(message, handlers);
69
86
  }
70
87
  catch (error) {
71
- const errMessage = error instanceof Error ? error.message : String(error);
88
+ const err = error instanceof Error ? error : new Error(String(error));
89
+ const env = peerEnvBySessionId.get(message.sessionId) ??
90
+ buildIdleEnv(message.sessionId);
91
+ await runErrorHook(handlers, {
92
+ sessionId: message.sessionId,
93
+ projectId: env.PROJECT_ID,
94
+ buildId: env.BUILD_ID,
95
+ env,
96
+ error: err,
97
+ customerContext: parseCustomerContext(env.AGENT_CUSTOMER_CONTEXT),
98
+ });
72
99
  process.send?.({
73
100
  type: "agent_error",
74
101
  sessionId: message.sessionId,
75
- message: errMessage,
102
+ message: err.message,
103
+ stack: err.stack,
76
104
  });
77
105
  }
78
- })();
106
+ });
79
107
  });
80
108
  }
109
+ function parseCustomerContext(raw) {
110
+ if (!raw?.trim())
111
+ return undefined;
112
+ try {
113
+ const parsed = JSON.parse(raw);
114
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
115
+ return parsed;
116
+ }
117
+ }
118
+ catch {
119
+ // ignore malformed context
120
+ }
121
+ return undefined;
122
+ }
123
+ async function runErrorHook(handlers, ctx) {
124
+ if (!handlers.errorHook)
125
+ return;
126
+ try {
127
+ await handlers.errorHook(ctx);
128
+ }
129
+ catch (hookError) {
130
+ const message = hookError instanceof Error ? hookError.message : String(hookError);
131
+ agentLog("error", `errorHook failed: ${message}`);
132
+ }
133
+ }
134
+ async function runIdleTimeoutHook(handlers, message) {
135
+ const env = peerEnvBySessionId.get(message.sessionId) ??
136
+ buildIdleEnv(message.sessionId);
137
+ const idleTimeoutSeconds = Number(env.IDLE_TIMEOUT_SEC) || 0;
138
+ const ctx = {
139
+ sessionId: message.sessionId,
140
+ projectId: env.PROJECT_ID,
141
+ buildId: env.BUILD_ID,
142
+ env,
143
+ idleTimeoutSeconds,
144
+ };
145
+ let error;
146
+ try {
147
+ await handlers.onIdleTimeout?.(ctx);
148
+ }
149
+ catch (hookError) {
150
+ error =
151
+ hookError instanceof Error ? hookError.message : String(hookError);
152
+ agentLog("error", `onIdleTimeout failed: ${error}`);
153
+ }
154
+ process.send?.({
155
+ type: "idle_timeout_done",
156
+ sessionId: message.sessionId,
157
+ error,
158
+ });
159
+ }
160
+ function buildIdleEnv(sessionId) {
161
+ return {
162
+ SESSION_ID: sessionId,
163
+ ...(process.env.PROJECT_ID ? { PROJECT_ID: process.env.PROJECT_ID } : {}),
164
+ ...(process.env.BUILD_ID ? { BUILD_ID: process.env.BUILD_ID } : {}),
165
+ ...(process.env.IDLE_TIMEOUT_SEC
166
+ ? { IDLE_TIMEOUT_SEC: process.env.IDLE_TIMEOUT_SEC }
167
+ : {}),
168
+ };
169
+ }
81
170
  /** Ask the runner parent to synthesize speech for the session. */
82
171
  export function speak(sessionId, text) {
83
172
  process.send?.({ type: "speak", sessionId, text });
@@ -89,12 +178,31 @@ export function sendToClient(sessionId, payload) {
89
178
  /** Send raw bytes to the browser peer via the runner parent. */
90
179
  export function sendBinaryToClient(sessionId, data, channel = "sync") {
91
180
  const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
92
- process.send?.({ type: "send_binary_to_client", sessionId, data: buffer, channel });
181
+ process.send?.({
182
+ type: "send_binary_to_client",
183
+ sessionId,
184
+ data: buffer,
185
+ channel,
186
+ });
187
+ }
188
+ /** Send the same JSON payload to one or more browser peers. */
189
+ export function broadcastToClients(payload, sessionIds) {
190
+ for (const sessionId of sessionIds) {
191
+ sendToClient(sessionId, payload);
192
+ }
93
193
  }
94
194
  /** Structured log forwarded to the runner parent. */
95
195
  export function agentLog(level, message) {
96
196
  process.send?.({ type: "log", level, message });
97
197
  }
198
+ /** Ask the runner to disconnect a browser peer (customer-initiated). */
199
+ export function disconnectClient(sessionId, options) {
200
+ process.send?.({
201
+ type: "disconnect_client",
202
+ sessionId,
203
+ reason: options?.reason,
204
+ });
205
+ }
98
206
  /** Extract chat text from a parsed data channel message. */
99
207
  export function parseChatText(message) {
100
208
  if (!message || typeof message !== "object")
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Per-session FIFO task chain. Different sessionIds run independently; the same
3
+ * sessionId is processed strictly in enqueue order (no overlapping handlers).
4
+ */
5
+ export declare class SessionSerialQueue {
6
+ private readonly tails;
7
+ enqueue(sessionId: string, task: () => void | Promise<void>): void;
8
+ clear(sessionId: string): void;
9
+ clearAll(): void;
10
+ }
11
+ //# sourceMappingURL=session-serial-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-serial-queue.d.ts","sourceRoot":"","sources":["../src/session-serial-queue.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;IAE1D,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAclE,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI9B,QAAQ,IAAI,IAAI;CAGjB"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Per-session FIFO task chain. Different sessionIds run independently; the same
3
+ * sessionId is processed strictly in enqueue order (no overlapping handlers).
4
+ */
5
+ export class SessionSerialQueue {
6
+ tails = new Map();
7
+ enqueue(sessionId, task) {
8
+ const previous = this.tails.get(sessionId)?.catch(() => undefined) ?? Promise.resolve();
9
+ const next = previous.then(() => Promise.resolve(task()));
10
+ this.tails.set(sessionId, next.finally(() => {
11
+ if (this.tails.get(sessionId) === next) {
12
+ this.tails.delete(sessionId);
13
+ }
14
+ }));
15
+ }
16
+ clear(sessionId) {
17
+ this.tails.delete(sessionId);
18
+ }
19
+ clearAll() {
20
+ this.tails.clear();
21
+ }
22
+ }
23
+ //# sourceMappingURL=session-serial-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-serial-queue.js","sourceRoot":"","sources":["../src/session-serial-queue.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,OAAO,kBAAkB;IACZ,KAAK,GAAG,IAAI,GAAG,EAAyB,CAAC;IAE1D,OAAO,CAAC,SAAiB,EAAE,IAAgC;QACzD,MAAM,QAAQ,GACZ,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACzE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC1D,IAAI,CAAC,KAAK,CAAC,GAAG,CACZ,SAAS,EACT,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;YAChB,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC;gBACvC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,SAAiB;QACrB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC/B,CAAC;IAED,QAAQ;QACN,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voicethere/agent",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "VoiceThere customer agent SDK — IPC types and runtime helpers for sandboxed child bundles",
5
5
  "type": "module",
6
6
  "exports": {
@@ -18,6 +18,7 @@
18
18
  import {
19
19
  agentLog,
20
20
  defineAgent,
21
+ disconnectClient,
21
22
  speak,
22
23
  type SpeechEvent,
23
24
  } from "@voicethere/agent";
@@ -173,4 +174,19 @@ defineAgent({
173
174
  peers.delete(sessionId);
174
175
  agentLog("info", `session_end ${sessionId}`);
175
176
  },
177
+
178
+ // async onIdleTimeout({ sessionId, idleTimeoutSeconds }) {
179
+ // agentLog("info", `idle timeout after ${idleTimeoutSeconds}s for ${sessionId}`);
180
+ // // Optional: notify client, persist state — runner disconnects after this returns (max 30s).
181
+ // },
182
+
183
+ // async errorHook({ sessionId, error, customerContext }) {
184
+ // agentLog("error", `handler failed for ${sessionId}: ${error.message}`);
185
+ // // Optional: sendToClient(sessionId, { type: "alert", message: error.message });
186
+ // },
187
+
188
+ // Example: disconnect a stale multiplayer client from agent code:
189
+ // onDataChannelMessage({ sessionId, message }) {
190
+ // if (isStaleGameState(message)) disconnectClient(sessionId, { reason: "stale_state" });
191
+ // },
176
192
  });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Authoritative tick counter — shared child broadcasts `{ type: 'state', tick, players }`.
3
+ *
4
+ * Build:
5
+ * npx @voicethere/agent build --entry templates/game-sync.ts
6
+ */
7
+ import {
8
+ agentLog,
9
+ broadcastToClients,
10
+ defineAgent,
11
+ sendToClient,
12
+ type DataChannelContext,
13
+ } from "@voicethere/agent";
14
+
15
+ let tick = 0;
16
+ const connected = new Set<string>();
17
+
18
+ function playerCount(): number {
19
+ return connected.size;
20
+ }
21
+
22
+ function broadcastState(excludeSessionId?: string): void {
23
+ const payload = { type: "state" as const, tick, players: playerCount() };
24
+ const targets = excludeSessionId
25
+ ? [...connected].filter((id) => id !== excludeSessionId)
26
+ : [...connected];
27
+ broadcastToClients(payload, targets);
28
+ }
29
+
30
+ function handleTickMessage(ctx: DataChannelContext): void {
31
+ const message = ctx.message;
32
+ if (!message || typeof message !== "object") return;
33
+ const record = message as { type?: string };
34
+ if (record.type !== "tick") return;
35
+ tick += 1;
36
+ agentLog(
37
+ "info",
38
+ `tick=${tick} players=${playerCount()} from=${ctx.sessionId}`,
39
+ );
40
+ broadcastToClients({ type: "state", tick, players: playerCount() }, [
41
+ ...connected,
42
+ ]);
43
+ }
44
+
45
+ defineAgent({
46
+ onClientJoin({ sessionId }) {
47
+ connected.add(sessionId);
48
+ sendToClient(sessionId, {
49
+ type: "state",
50
+ tick,
51
+ players: playerCount(),
52
+ });
53
+ broadcastState(sessionId);
54
+ agentLog("info", `join ${sessionId} players=${playerCount()}`);
55
+ },
56
+
57
+ onClientLeave({ sessionId }) {
58
+ connected.delete(sessionId);
59
+ broadcastToClients({ type: "state", tick, players: playerCount() }, [
60
+ ...connected,
61
+ ]);
62
+ agentLog("info", `leave ${sessionId} players=${playerCount()}`);
63
+ },
64
+
65
+ onDataChannelMessage(ctx) {
66
+ handleTickMessage(ctx);
67
+ },
68
+ });