@voicethere/agent 0.1.9 → 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 +12 -0
- package/dist/agent.js +142 -46
- 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 +31 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +3 -0
- package/dist/protocol.js.map +1 -1
- package/dist/runtime.d.ts +40 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +154 -46
- package/dist/runtime.js.map +1 -1
- package/dist/sandbox/sandbox.d.ts +2 -0
- package/dist/sandbox/sandbox.d.ts.map +1 -1
- package/dist/sandbox/sandbox.js +23 -4
- package/dist/sandbox/sandbox.js.map +1 -1
- package/dist/sandbox/start-child.d.ts.map +1 -1
- package/dist/sandbox/start-child.js +7 -2
- package/dist/sandbox/start-child.js.map +1 -1
- package/dist/session-serial-queue.d.ts +11 -0
- package/dist/session-serial-queue.d.ts.map +1 -0
- package/dist/session-serial-queue.js +23 -0
- package/dist/session-serial-queue.js.map +1 -0
- package/package.json +1 -1
- package/templates/agent.ts +16 -0
- package/templates/game-sync.ts +68 -0
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
|
-
|
|
92
|
+
inboundBySession.enqueue(message.sessionId, async () => {
|
|
18
93
|
try {
|
|
19
|
-
|
|
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
|
|
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:
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -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
|
package/dist/protocol.d.ts.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
|
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
|
package/dist/protocol.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
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
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
83
|
+
inboundBySession.enqueue(message.sessionId, async () => {
|
|
28
84
|
try {
|
|
29
|
-
|
|
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
|
|
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:
|
|
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?.({
|
|
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")
|
package/dist/runtime.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Keep in sync with the agent runner `src/child/sandbox.ts`.
|
|
5
5
|
*/
|
|
6
|
+
/** Collect unique fs-read allowlist paths (resolved + realpath for /var/run → /run symlinks). */
|
|
7
|
+
export declare function collectAllowFsReadDirs(paths: string[]): string[];
|
|
6
8
|
/**
|
|
7
9
|
* Node --permission flags for the customer child: no subprocesses, no fs writes.
|
|
8
10
|
* `fs` read is limited to the loader + bundle directories (no `--allow-child-process`).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/sandbox/sandbox.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/sandbox/sandbox.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,iGAAiG;AACjG,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAchE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,EAAE,CAWX"}
|
package/dist/sandbox/sandbox.js
CHANGED
|
@@ -3,19 +3,38 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Keep in sync with the agent runner `src/child/sandbox.ts`.
|
|
5
5
|
*/
|
|
6
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
6
7
|
import { dirname, resolve } from "node:path";
|
|
8
|
+
/** Collect unique fs-read allowlist paths (resolved + realpath for /var/run → /run symlinks). */
|
|
9
|
+
export function collectAllowFsReadDirs(paths) {
|
|
10
|
+
const readDirs = new Set();
|
|
11
|
+
for (const raw of paths) {
|
|
12
|
+
const resolved = resolve(raw);
|
|
13
|
+
readDirs.add(resolved);
|
|
14
|
+
try {
|
|
15
|
+
if (existsSync(resolved)) {
|
|
16
|
+
readDirs.add(realpathSync(resolved));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// ignore
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [...readDirs];
|
|
24
|
+
}
|
|
7
25
|
/**
|
|
8
26
|
* Node --permission flags for the customer child: no subprocesses, no fs writes.
|
|
9
27
|
* `fs` read is limited to the loader + bundle directories (no `--allow-child-process`).
|
|
10
28
|
*/
|
|
11
29
|
export function buildChildExecArgv(options) {
|
|
12
|
-
const readDirs =
|
|
13
|
-
|
|
14
|
-
|
|
30
|
+
const readDirs = collectAllowFsReadDirs([
|
|
31
|
+
options.loaderDir,
|
|
32
|
+
dirname(options.bundlePath),
|
|
33
|
+
options.bundlePath,
|
|
15
34
|
]);
|
|
16
35
|
return [
|
|
17
36
|
"--permission",
|
|
18
|
-
...
|
|
37
|
+
...readDirs.map((dir) => `--allow-fs-read=${dir}`),
|
|
19
38
|
];
|
|
20
39
|
}
|
|
21
40
|
//# sourceMappingURL=sandbox.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../src/sandbox/sandbox.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE7C
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../src/sandbox/sandbox.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE7C,iGAAiG;AACjG,MAAM,UAAU,sBAAsB,CAAC,KAAe;IACpD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACvB,IAAI,CAAC;YACH,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACzB,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;YACvC,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC;AACvB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAGlC;IACC,MAAM,QAAQ,GAAG,sBAAsB,CAAC;QACtC,OAAO,CAAC,SAAS;QACjB,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC;QAC3B,OAAO,CAAC,UAAU;KACnB,CAAC,CAAC;IAEH,OAAO;QACL,cAAc;QACd,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,mBAAmB,GAAG,EAAE,CAAC;KACnD,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-child.d.ts","sourceRoot":"","sources":["../../src/sandbox/start-child.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AAMxB,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC;IAC7C,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IACpC,SAAS,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACxE,MAAM,CACJ,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,KAAK,IAAI,GACpE,MAAM,IAAI,CAAC;CACf;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"start-child.d.ts","sourceRoot":"","sources":["../../src/sandbox/start-child.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AAMxB,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC;IAC7C,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IACpC,SAAS,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACxE,MAAM,CACJ,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,KAAK,IAAI,GACpE,MAAM,IAAI,CAAC;CACf;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAU5D;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,0BAA0B,GAClC,cAAc,CAiEhB"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Keep behavior aligned with runner `src/child/starter.ts`.
|
|
5
5
|
*/
|
|
6
6
|
import { fork } from "node:child_process";
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
7
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
8
8
|
import { dirname, join, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { ALLOWED_CHILD_ENV_KEYS, } from "../protocol.js";
|
|
@@ -16,7 +16,12 @@ export function resolveBundlePath(bundlePath) {
|
|
|
16
16
|
if (!existsSync(resolved)) {
|
|
17
17
|
throw new Error(`Bundle not found: ${resolved}`);
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
try {
|
|
20
|
+
return realpathSync(resolved);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return resolved;
|
|
24
|
+
}
|
|
20
25
|
}
|
|
21
26
|
export function startSandboxedChild(options) {
|
|
22
27
|
const bundlePath = resolveBundlePath(options.bundlePath);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-child.js","sourceRoot":"","sources":["../../src/sandbox/start-child.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAqB,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"start-child.js","sourceRoot":"","sources":["../../src/sandbox/start-child.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
package/templates/agent.ts
CHANGED
|
@@ -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
|
+
});
|