@voicethere/agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +251 -0
- package/dist/agent.js +148 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +112 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +22 -0
- package/dist/protocol.js.map +1 -0
- package/dist/runtime.d.ts +32 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +61 -0
- package/dist/runtime.js.map +1 -0
- package/package.json +60 -0
- package/templates/README.md +26 -0
- package/templates/agent.ts +173 -0
package/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# @voicethere/agent
|
|
2
|
+
|
|
3
|
+
VoiceThere **customer agent SDK** — TypeScript types and runtime helpers for sandboxed child bundles running inside [`voicethere/runner`](https://github.com/voicethere/runner).
|
|
4
|
+
|
|
5
|
+
**npm:** `@voicethere/agent`
|
|
6
|
+
**Repo:** [`voicethere/agent`](https://github.com/voicethere/agent)
|
|
7
|
+
|
|
8
|
+
## Role
|
|
9
|
+
|
|
10
|
+
| Layer | Package | Runs in |
|
|
11
|
+
| --------- | ----------------------------------------------------------- | ------------------------------------ |
|
|
12
|
+
| Parent | [`voicethere/runner`](https://github.com/voicethere/runner) | Trusted Node + WebRTC + speech stack |
|
|
13
|
+
| **Child** | **`@voicethere/agent`** | Sandboxed customer `agent.js` bundle |
|
|
14
|
+
|
|
15
|
+
The child receives speech lifecycle events over IPC (same shapes as `@node-webrtc-rust/sdk/voice`) and calls `speak()` to request TTS from the parent.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/voicethere/agent.git
|
|
21
|
+
cd agent
|
|
22
|
+
npm install
|
|
23
|
+
npm run build
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Run with a local [runner](https://github.com/voicethere/runner) (clone that repo alongside this one):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd ../runner
|
|
30
|
+
AGENT_BUNDLE_PATH=../agent/dist/agent.js npm run start
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Open the runner URL in a browser, connect, and speak.
|
|
34
|
+
|
|
35
|
+
## Verify locally (sandbox, no WebRTC)
|
|
36
|
+
|
|
37
|
+
Before deploying to VoiceThere, confirm your bundle loads and responds under the **same Node sandbox** the runner uses:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm run verify:local
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Script | When to use |
|
|
44
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| `npm run verify:local` | **Default** — `npm run build`, then fork `dist/agent.js` in the sandbox and assert a `speak` reply to `user_speech_final` |
|
|
46
|
+
| `npm run verify:local:only` | Re-run smoke after build; optional `AGENT_BUNDLE_PATH=./dist/agent.js` or `--bundle <path>` |
|
|
47
|
+
|
|
48
|
+
This checks bundle load, IPC, and Node permission flags. It does **not** replace a voice roundtrip — use [`voicethere/runner`](https://github.com/voicethere/runner) for mic/WebRTC E2E.
|
|
49
|
+
|
|
50
|
+
Harness: [`scripts/sandbox/`](./scripts/sandbox/) (aligned with [`voicethere/runner`](https://github.com/voicethere/runner) child launcher).
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import {
|
|
56
|
+
agentLog,
|
|
57
|
+
defineAgent,
|
|
58
|
+
speak,
|
|
59
|
+
type SpeechEvent,
|
|
60
|
+
} from '@voicethere/agent'
|
|
61
|
+
import { SPEECH_EVENT_TYPE } from '@node-webrtc-rust/sdk/voice'
|
|
62
|
+
|
|
63
|
+
defineAgent({
|
|
64
|
+
onSessionStart({ sessionId }) {
|
|
65
|
+
speak(sessionId, 'Hello!')
|
|
66
|
+
},
|
|
67
|
+
onUserSpeechFinal({ sessionId, text }) {
|
|
68
|
+
speak(sessionId, `You said: ${text}`)
|
|
69
|
+
},
|
|
70
|
+
onSpeechEvent({ sessionId }, speech: SpeechEvent) {
|
|
71
|
+
if (speech.type === SPEECH_EVENT_TYPE.bargeIn) {
|
|
72
|
+
agentLog('info', `User interrupted on ${sessionId}`)
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Export | Purpose |
|
|
79
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
80
|
+
| `defineAgent` | Register `onSessionStart`, `onSpeechEvent`, `onUserSpeechFinal`, `onSessionEnd` |
|
|
81
|
+
| `SpeechEvent`, `SpeechEventType` | Re-exported **types** from `@node-webrtc-rust/sdk/voice` |
|
|
82
|
+
| `SPEECH_EVENT_TYPE` | Import from `@node-webrtc-rust/sdk/voice` (runtime constants; not bundled into child) |
|
|
83
|
+
| `speak` | Request parent TTS |
|
|
84
|
+
| `agentLog` | Forward structured logs to parent |
|
|
85
|
+
| `ParentToChildMessage` / `ChildToParentMessage` | IPC contract shared with [`voicethere/runner`](https://github.com/voicethere/runner) |
|
|
86
|
+
|
|
87
|
+
### Speech events (parent → child)
|
|
88
|
+
|
|
89
|
+
Forwarded from the runner voice pipeline as SDK `SpeechEvent` payloads on `speech_event.event` (`event.type`, optional `text` / `error`):
|
|
90
|
+
|
|
91
|
+
| Event | Typical use in custom agent |
|
|
92
|
+
| --------------------------------------------- | ------------------------------------------------------- |
|
|
93
|
+
| `user_speaking_start` / `user_speaking_end` | UI state, turn-taking |
|
|
94
|
+
| `user_speech_partial` | Live captions, early barge-in logic |
|
|
95
|
+
| `user_speech_final` | Primary turn boundary (`onUserSpeechFinal` convenience) |
|
|
96
|
+
| `agent_speaking_start` / `agent_speaking_end` | Know when TTS playback starts/stops |
|
|
97
|
+
| `barge_in` | User interrupted agent playback |
|
|
98
|
+
| `vad_triggered`, `stt_stream_*`, `user_stt_*` | Low-level pipeline hooks |
|
|
99
|
+
| `error` | Vendor or pipeline failure |
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
## Building your agent bundle
|
|
104
|
+
|
|
105
|
+
**Recommended:** single ESM bundle (esbuild or similar):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm install @voicethere/agent esbuild
|
|
109
|
+
npx esbuild agent.ts --bundle --platform=node --format=esm --outfile=dist/agent.js
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Upload `dist/agent.js` (or point `AGENT_BUNDLE_PATH` at it locally). Inlining dependencies avoids runtime `node_modules` resolution inside the sandbox.
|
|
113
|
+
|
|
114
|
+
## Sandbox and security model
|
|
115
|
+
|
|
116
|
+
Customer code runs in a **forked child process**, separate from the trusted runner parent (Sherpa, WebRTC, TTS). Security is layered:
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
120
|
+
│ Runner parent (trusted) — full Node, WebRTC, speech stack │
|
|
121
|
+
│ fork(loader-entry.js, execArgv: [--permission, …]) │
|
|
122
|
+
│ │ IPC (process.send / on('message')) │
|
|
123
|
+
│ ▼ │
|
|
124
|
+
│ Customer child — Node Permission Model + stripped env │
|
|
125
|
+
│ loader-entry.js → import(your agent.js) │
|
|
126
|
+
└──────────────────────────────────────────────────────────────┘
|
|
127
|
+
│ same pod network namespace (K8s)
|
|
128
|
+
▼
|
|
129
|
+
Cilium NetworkPolicy on runner pod (egress rules)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Layer 1 — Process isolation
|
|
133
|
+
|
|
134
|
+
| Mechanism | What it means for your bundle |
|
|
135
|
+
| --------- | ----------------------------- |
|
|
136
|
+
| **Separate process** | Crash or `process.exit` in your bundle does not take down the parent voice stack |
|
|
137
|
+
| **IPC only** | Talk to the platform via `defineAgent` / `speak` / `agentLog` — not direct WebRTC or STT |
|
|
138
|
+
| **Stripped `process.env`** | Child receives only `NODE_ENV`, internal loader path, and allowlisted keys (`SESSION_ID`, `PROJECT_ID`, `BUILD_ID`) — not parent/kube secrets |
|
|
139
|
+
| **Console redirection** | `console.log` / `warn` / `error` → IPC logs; raw stdout is not forwarded to cluster logs |
|
|
140
|
+
|
|
141
|
+
### Layer 2 — Node `--permission` (runtime-enforced)
|
|
142
|
+
|
|
143
|
+
The parent starts the child with Node’s [Permission Model](https://nodejs.org/api/permissions.html) (Node **22+**). Capabilities are **deny-by-default**; only explicitly granted flags apply.
|
|
144
|
+
|
|
145
|
+
**Granted today** (via `execArgv` on `fork()`):
|
|
146
|
+
|
|
147
|
+
| Flag | Effect |
|
|
148
|
+
| ---- | ------ |
|
|
149
|
+
| `--permission` | Enables restriction mode |
|
|
150
|
+
| `--allow-fs-read=<loaderDir>` | Read files under the runner’s child loader directory |
|
|
151
|
+
| `--allow-fs-read=<bundleParentDir>` | Read files under the **directory containing your `agent.js`** (see below) |
|
|
152
|
+
|
|
153
|
+
**Not granted → blocked at runtime:**
|
|
154
|
+
|
|
155
|
+
| Missing flag | What fails |
|
|
156
|
+
| ------------ | ---------- |
|
|
157
|
+
| No `--allow-child-process` | `child_process`, `exec`, `spawn`, `fork` |
|
|
158
|
+
| No `--allow-fs-write` | Any file write (`writeFile`, logs to disk, etc.) |
|
|
159
|
+
| No extra `--allow-fs-read` paths | Reading `/etc/passwd`, parent files, etc. outside bundle dir |
|
|
160
|
+
| No `--allow-addons` | Native `.node` addons (`bcrypt`, `sharp`, …) |
|
|
161
|
+
| No `--allow-worker-threads` | `worker_threads` |
|
|
162
|
+
| No `--allow-wasi` | WASI modules |
|
|
163
|
+
|
|
164
|
+
This is **not** an import allowlist — Node gates **capability classes**, not package names. Using `node:fs` inside the allowed read tree can work; using it on `/etc/passwd` does not.
|
|
165
|
+
|
|
166
|
+
### Bundle directory vs single file
|
|
167
|
+
|
|
168
|
+
`--allow-fs-read` is applied to **`dirname(bundlePath)`**, not only the `.js` file:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
/app/agents/my-build/
|
|
172
|
+
agent.js ← entry (AGENT_BUNDLE_PATH)
|
|
173
|
+
helper.js ← importable if your bundle references it
|
|
174
|
+
data.json ← readable via fs if you import/read it
|
|
175
|
+
node_modules/ ← JS-only deps may resolve; native addons still blocked
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
| Artifact in bundle dir | Works? |
|
|
179
|
+
| ---------------------- | ------ |
|
|
180
|
+
| Single bundled `agent.js` (recommended) | Yes |
|
|
181
|
+
| Extra pure `.js` / `.json` siblings | Usually yes (same allowed tree) |
|
|
182
|
+
| `node_modules/` with **JavaScript-only** packages | Often yes (Node resolves imports by reading under that tree) |
|
|
183
|
+
| **Native** npm packages (`.node` binaries) | **No** — requires `--allow-addons` (not enabled) |
|
|
184
|
+
| Packages that **spawn subprocesses** | **No** — no `--allow-child-process` |
|
|
185
|
+
|
|
186
|
+
Prefer **one esbuild bundle** so production behavior matches `npm run verify:local`.
|
|
187
|
+
|
|
188
|
+
### Layer 3 — Platform policy (documented; not all enforced in-process)
|
|
189
|
+
|
|
190
|
+
These are **unsupported** in customer bundles even if Node might not block them today:
|
|
191
|
+
|
|
192
|
+
| Capability | Enforcement |
|
|
193
|
+
| ---------- | ----------- |
|
|
194
|
+
| **Outbound network** (`fetch`, `http`, `net`, `dns`) | Not gated by `--permission`; child shares the **pod** network. Runner pods use **Cilium NetworkPolicy** (public egress; cluster/RFC1918 denied). **Do not** rely on network from child — use parent/platform APIs. |
|
|
195
|
+
| **`process.exit`** | Not blocked — kills your agent leg; parent may play crash TTS |
|
|
196
|
+
| **Direct WebRTC / mic / STT / TTS** | Parent only — use `speak()` and speech event handlers |
|
|
197
|
+
|
|
198
|
+
### Layer 4 — Kubernetes (runner pod)
|
|
199
|
+
|
|
200
|
+
On cluster deploy, the runner pod also has Helm hardening (non-root, read-only rootfs, dropped caps) and **NetworkPolicy** for egress. That applies to the whole pod (parent + child).
|
|
201
|
+
|
|
202
|
+
### What you should use in agent code
|
|
203
|
+
|
|
204
|
+
**Supported**
|
|
205
|
+
|
|
206
|
+
- `@voicethere/agent` (`defineAgent`, `speak`, `agentLog`, `onSpeechEvent`, …)
|
|
207
|
+
- Pure TypeScript/JavaScript logic and in-memory state
|
|
208
|
+
- Allowlisted env from `onSessionStart` (`SESSION_ID`, `PROJECT_ID`, `BUILD_ID`)
|
|
209
|
+
- `SPEECH_EVENT_TYPE` from `@node-webrtc-rust/sdk/voice` at build time (avoid bundling the full SDK runtime into the child when possible)
|
|
210
|
+
|
|
211
|
+
**Blocked or unsupported**
|
|
212
|
+
|
|
213
|
+
- Subprocesses, shells, `child_process`
|
|
214
|
+
- Arbitrary filesystem access outside your bundle deployment directory
|
|
215
|
+
- File writes
|
|
216
|
+
- Native Node addons (`.node`)
|
|
217
|
+
- `worker_threads` (not allowed)
|
|
218
|
+
- Direct media/network stack access
|
|
219
|
+
|
|
220
|
+
**Pre-publish checklist**
|
|
221
|
+
|
|
222
|
+
1. `npm run build` — produce `dist/agent.js`
|
|
223
|
+
2. `npm run verify:local` — sandbox + IPC smoke (same flags as production child)
|
|
224
|
+
3. Optional: voice E2E with [`voicethere/runner`](https://github.com/voicethere/runner)
|
|
225
|
+
|
|
226
|
+
## Build outputs
|
|
227
|
+
|
|
228
|
+
| Path | Purpose |
|
|
229
|
+
| --------------- | ----------------------------------------------------------- |
|
|
230
|
+
| `dist/index.js` | Published npm library entry |
|
|
231
|
+
| `dist/agent.js` | Example bundle (`examples/agent.ts`) for local runner / verify |
|
|
232
|
+
|
|
233
|
+
## Scripts
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
npm run build # library + example bundle
|
|
237
|
+
npm run verify:local # sandbox smoke (build + fork bundle)
|
|
238
|
+
npm run test:ci # typecheck + vitest
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Release
|
|
242
|
+
|
|
243
|
+
See [`scripts/RELEASE.md`](./scripts/RELEASE.md) — tag `release/X.Y.Z` triggers npm publish (same workflow pattern as [`node-webrtc-rust`](https://github.com/akirilyuk/node-webrtc-rust)).
|
|
244
|
+
|
|
245
|
+
## Related
|
|
246
|
+
|
|
247
|
+
| Repo | Purpose |
|
|
248
|
+
| ----------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
249
|
+
| [`voicethere/runner`](https://github.com/voicethere/runner) | Session worker that hosts your `agent.js` bundle |
|
|
250
|
+
| [`voicethere/cli`](https://github.com/voicethere/cli) | CLI for the VoiceThere platform (projects, deploys, sessions) |
|
|
251
|
+
| [`akirilyuk/node-webrtc-rust`](https://github.com/akirilyuk/node-webrtc-rust) | WebRTC + voice SDK (`SpeechEvent` types) |
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// src/runtime.ts
|
|
2
|
+
function isParentMessage(value) {
|
|
3
|
+
if (!value || typeof value !== "object") return false;
|
|
4
|
+
const msg = value;
|
|
5
|
+
return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end";
|
|
6
|
+
}
|
|
7
|
+
function defineAgent(handlers) {
|
|
8
|
+
process.on("message", (message) => {
|
|
9
|
+
if (!isParentMessage(message)) return;
|
|
10
|
+
void (async () => {
|
|
11
|
+
try {
|
|
12
|
+
switch (message.type) {
|
|
13
|
+
case "session_start":
|
|
14
|
+
await handlers.onSessionStart?.({
|
|
15
|
+
sessionId: message.sessionId,
|
|
16
|
+
env: message.env
|
|
17
|
+
});
|
|
18
|
+
break;
|
|
19
|
+
case "speech_event":
|
|
20
|
+
await handlers.onSpeechEvent?.(
|
|
21
|
+
{ sessionId: message.sessionId },
|
|
22
|
+
message.event
|
|
23
|
+
);
|
|
24
|
+
if (message.event.type === "user_speech_final" && typeof message.event.text === "string" && message.event.text.trim()) {
|
|
25
|
+
await handlers.onUserSpeechFinal?.({
|
|
26
|
+
sessionId: message.sessionId,
|
|
27
|
+
text: message.event.text.trim()
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
case "session_end":
|
|
32
|
+
await handlers.onSessionEnd?.({ sessionId: message.sessionId });
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const errMessage = error instanceof Error ? error.message : String(error);
|
|
37
|
+
process.send?.({
|
|
38
|
+
type: "agent_error",
|
|
39
|
+
sessionId: message.sessionId,
|
|
40
|
+
message: errMessage
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function speak(sessionId, text) {
|
|
47
|
+
process.send?.({ type: "speak", sessionId, text });
|
|
48
|
+
}
|
|
49
|
+
function agentLog(level, message) {
|
|
50
|
+
process.send?.({ type: "log", level, message });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// examples/agent.ts
|
|
54
|
+
var peers = /* @__PURE__ */ new Map();
|
|
55
|
+
function peerState(sessionId) {
|
|
56
|
+
let state = peers.get(sessionId);
|
|
57
|
+
if (!state) {
|
|
58
|
+
state = {
|
|
59
|
+
userSpeaking: false,
|
|
60
|
+
agentSpeaking: false,
|
|
61
|
+
lastPartial: "",
|
|
62
|
+
sttActive: false
|
|
63
|
+
};
|
|
64
|
+
peers.set(sessionId, state);
|
|
65
|
+
}
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
function formatSpeechDetail(speech) {
|
|
69
|
+
if (speech.text) return `"${speech.text}"`;
|
|
70
|
+
if (speech.error) return speech.error;
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
function handleSpeechEvent(sessionId, speech) {
|
|
74
|
+
const state = peerState(sessionId);
|
|
75
|
+
const detail = formatSpeechDetail(speech);
|
|
76
|
+
switch (speech.type) {
|
|
77
|
+
case "user_speaking_start":
|
|
78
|
+
state.userSpeaking = true;
|
|
79
|
+
agentLog("info", `[${sessionId}] user_speaking_start`);
|
|
80
|
+
break;
|
|
81
|
+
case "user_speaking_end":
|
|
82
|
+
state.userSpeaking = false;
|
|
83
|
+
agentLog("info", `[${sessionId}] user_speaking_end`);
|
|
84
|
+
break;
|
|
85
|
+
case "vad_triggered":
|
|
86
|
+
agentLog("info", `[${sessionId}] vad_triggered`);
|
|
87
|
+
break;
|
|
88
|
+
case "stt_stream_start":
|
|
89
|
+
agentLog("info", `[${sessionId}] stt_stream_start`);
|
|
90
|
+
break;
|
|
91
|
+
case "stt_stream_end":
|
|
92
|
+
agentLog("info", `[${sessionId}] stt_stream_end`);
|
|
93
|
+
break;
|
|
94
|
+
case "user_stt_start":
|
|
95
|
+
state.sttActive = true;
|
|
96
|
+
state.lastPartial = "";
|
|
97
|
+
agentLog("info", `[${sessionId}] user_stt_start`);
|
|
98
|
+
break;
|
|
99
|
+
case "user_stt_end":
|
|
100
|
+
state.sttActive = false;
|
|
101
|
+
agentLog("info", `[${sessionId}] user_stt_end`);
|
|
102
|
+
break;
|
|
103
|
+
case "user_stt_not_found":
|
|
104
|
+
agentLog("info", `[${sessionId}] user_stt_not_found`);
|
|
105
|
+
break;
|
|
106
|
+
case "user_speech_partial":
|
|
107
|
+
state.lastPartial = speech.text ?? "";
|
|
108
|
+
agentLog("info", `[${sessionId}] user_speech_partial ${detail}`);
|
|
109
|
+
break;
|
|
110
|
+
case "user_speech_final":
|
|
111
|
+
state.lastPartial = "";
|
|
112
|
+
agentLog("info", `[${sessionId}] user_speech_final ${detail}`);
|
|
113
|
+
break;
|
|
114
|
+
case "agent_speaking_start":
|
|
115
|
+
state.agentSpeaking = true;
|
|
116
|
+
agentLog("info", `[${sessionId}] agent_speaking_start`);
|
|
117
|
+
break;
|
|
118
|
+
case "agent_speaking_end":
|
|
119
|
+
state.agentSpeaking = false;
|
|
120
|
+
agentLog("info", `[${sessionId}] agent_speaking_end`);
|
|
121
|
+
break;
|
|
122
|
+
case "barge_in":
|
|
123
|
+
agentLog("info", `[${sessionId}] barge_in`);
|
|
124
|
+
break;
|
|
125
|
+
case "error":
|
|
126
|
+
agentLog("error", `[${sessionId}] error: ${speech.error ?? "unknown"}`);
|
|
127
|
+
break;
|
|
128
|
+
default: {
|
|
129
|
+
const _exhaustive = speech.type;
|
|
130
|
+
agentLog("error", `[${sessionId}] unhandled: ${String(_exhaustive)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
defineAgent({
|
|
135
|
+
onSessionStart({ sessionId }) {
|
|
136
|
+
speak(sessionId, "Hello! How can I help?");
|
|
137
|
+
},
|
|
138
|
+
onSpeechEvent({ sessionId }, speech) {
|
|
139
|
+
handleSpeechEvent(sessionId, speech);
|
|
140
|
+
},
|
|
141
|
+
onUserSpeechFinal({ sessionId, text }) {
|
|
142
|
+
speak(sessionId, `You said: ${text}`);
|
|
143
|
+
},
|
|
144
|
+
onSessionEnd({ sessionId }) {
|
|
145
|
+
peers.delete(sessionId);
|
|
146
|
+
agentLog("info", `session_end ${sessionId}`);
|
|
147
|
+
}
|
|
148
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
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 SpeakMessage, type SpeechEventMessage, } from "./protocol.js";
|
|
3
|
+
export { agentLog, defineAgent, speak, type AgentHandlers, type SessionContext, type SpeechContext, type SpeechEventContext, } from "./runtime.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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,YAAY,EACjB,KAAK,kBAAkB,GACxB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,QAAQ,EACR,WAAW,EACX,KAAK,EACL,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,sBAAsB,GAUvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,QAAQ,EACR,WAAW,EACX,KAAK,GAKN,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC message shapes between trusted runner parent and isolated customer child.
|
|
3
|
+
*
|
|
4
|
+
* Transport: Node.js `process.send` / `process.on('message')` on a forked child.
|
|
5
|
+
* Speech payloads use {@link SpeechEvent} from `@node-webrtc-rust/sdk/voice` unchanged.
|
|
6
|
+
*
|
|
7
|
+
* IPC shapes are shared with [`voicethere/runner`](https://github.com/voicethere/runner).
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import type { SpeechEvent } from "@node-webrtc-rust/sdk/voice";
|
|
12
|
+
/**
|
|
13
|
+
* Messages the trusted runner parent may send to the sandboxed customer child.
|
|
14
|
+
*
|
|
15
|
+
* Register handlers via {@link defineAgent} in `@voicethere/agent` — do not read
|
|
16
|
+
* `process.on('message')` directly in customer bundles.
|
|
17
|
+
*/
|
|
18
|
+
export type ParentToChildMessage = SessionStartMessage | SpeechEventMessage | SessionEndMessage;
|
|
19
|
+
/**
|
|
20
|
+
* Messages the customer child may send back to the runner parent.
|
|
21
|
+
*
|
|
22
|
+
* Prefer {@link speak} and {@link agentLog} helpers over raw `process.send`.
|
|
23
|
+
*/
|
|
24
|
+
export type ChildToParentMessage = SpeakMessage | AgentLogMessage | AgentErrorMessage;
|
|
25
|
+
/**
|
|
26
|
+
* A WebRTC peer connected to the runner and mapped to this child process.
|
|
27
|
+
*
|
|
28
|
+
* Emitted once per `sessionId` before the first {@link SpeechEventMessage}.
|
|
29
|
+
* `env.SESSION_ID` matches {@link SessionStartMessage.sessionId}.
|
|
30
|
+
*/
|
|
31
|
+
export interface SessionStartMessage {
|
|
32
|
+
type: "session_start";
|
|
33
|
+
/** Browser/signaling peer id for this conversation leg. */
|
|
34
|
+
sessionId: string;
|
|
35
|
+
/**
|
|
36
|
+
* Allowlisted environment variables copied from the runner process.
|
|
37
|
+
* Keys are a subset of {@link ALLOWED_CHILD_ENV_KEYS}.
|
|
38
|
+
*/
|
|
39
|
+
env: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Forwards one speech lifecycle event from the parent Sherpa/VAD/STT/TTS pipeline.
|
|
43
|
+
*
|
|
44
|
+
* The {@link SpeechEvent} shape matches `@node-webrtc-rust/sdk/voice` — see SDK docs
|
|
45
|
+
* for `SpeechEventType` semantics (`user_speech_final`, `barge_in`, etc.).
|
|
46
|
+
*
|
|
47
|
+
* Delivered to customer code as `onSpeechEvent(ctx, message.event)`; `user_speech_final`
|
|
48
|
+
* also triggers the `onUserSpeechFinal` handler when `event.text` is non-empty.
|
|
49
|
+
*/
|
|
50
|
+
export interface SpeechEventMessage {
|
|
51
|
+
type: "speech_event";
|
|
52
|
+
/** Peer/session id the event belongs to. */
|
|
53
|
+
sessionId: string;
|
|
54
|
+
/** Native pipeline event payload (`type`, optional `text` / `error`). */
|
|
55
|
+
event: SpeechEvent;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The peer disconnected or the runner is tearing down this session leg.
|
|
59
|
+
*
|
|
60
|
+
* Emitted when the runner unregisters a peer or tears down the session.
|
|
61
|
+
*/
|
|
62
|
+
export interface SessionEndMessage {
|
|
63
|
+
type: "session_end";
|
|
64
|
+
/** Peer/session id that ended. */
|
|
65
|
+
sessionId: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Ask the parent to synthesize speech on the agent outbound WebRTC track.
|
|
69
|
+
*
|
|
70
|
+
* Handled by the runner parent, which synthesizes audio on the outbound WebRTC track.
|
|
71
|
+
* TTS does **not** run inside the sandboxed child.
|
|
72
|
+
*/
|
|
73
|
+
export interface SpeakMessage {
|
|
74
|
+
type: "speak";
|
|
75
|
+
/** Target peer/session id (must match a prior {@link SessionStartMessage}). */
|
|
76
|
+
sessionId: string;
|
|
77
|
+
/** UTF-8 text passed to the parent TTS vendor. */
|
|
78
|
+
text: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Structured log line forwarded to runner stdout / Winston.
|
|
82
|
+
*
|
|
83
|
+
* Use {@link agentLog} instead of calling `process.send` directly.
|
|
84
|
+
*/
|
|
85
|
+
export interface AgentLogMessage {
|
|
86
|
+
type: "log";
|
|
87
|
+
level: "info" | "error";
|
|
88
|
+
message: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Unhandled exception or rejected promise in customer agent code.
|
|
92
|
+
*
|
|
93
|
+
* The parent may play crash TTS and treat the child as failed. Prefer try/catch in
|
|
94
|
+
* handlers; use this only for fatal reporting.
|
|
95
|
+
*/
|
|
96
|
+
export interface AgentErrorMessage {
|
|
97
|
+
type: "agent_error";
|
|
98
|
+
/** Peer/session id active when the error occurred. */
|
|
99
|
+
sessionId: string;
|
|
100
|
+
/** Human-readable error summary (no stack traces required). */
|
|
101
|
+
message: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Environment variable names the runner may inject into {@link SessionStartMessage.env}.
|
|
105
|
+
*
|
|
106
|
+
* The runner may add more project-specific keys over time; customer bundles must
|
|
107
|
+
* not read `process.env` directly — only the `env` object on session start.
|
|
108
|
+
*/
|
|
109
|
+
export declare const ALLOWED_CHILD_ENV_KEYS: readonly ["SESSION_ID", "PROJECT_ID", "BUILD_ID"];
|
|
110
|
+
/** Union of allowlisted env key names. */
|
|
111
|
+
export type AllowedChildEnvKey = (typeof ALLOWED_CHILD_ENV_KEYS)[number];
|
|
112
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +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,CAAC;AAEtB;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B,YAAY,GACZ,eAAe,GACf,iBAAiB,CAAC;AAEtB;;;;;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;;;;;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;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,mDAIzB,CAAC;AAEX,0CAA0C;AAC1C,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC message shapes between trusted runner parent and isolated customer child.
|
|
3
|
+
*
|
|
4
|
+
* Transport: Node.js `process.send` / `process.on('message')` on a forked child.
|
|
5
|
+
* Speech payloads use {@link SpeechEvent} from `@node-webrtc-rust/sdk/voice` unchanged.
|
|
6
|
+
*
|
|
7
|
+
* IPC shapes are shared with [`voicethere/runner`](https://github.com/voicethere/runner).
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Environment variable names the runner may inject into {@link SessionStartMessage.env}.
|
|
13
|
+
*
|
|
14
|
+
* The runner may add more project-specific keys over time; customer bundles must
|
|
15
|
+
* not read `process.env` directly — only the `env` object on session start.
|
|
16
|
+
*/
|
|
17
|
+
export const ALLOWED_CHILD_ENV_KEYS = [
|
|
18
|
+
"SESSION_ID",
|
|
19
|
+
"PROJECT_ID",
|
|
20
|
+
"BUILD_ID",
|
|
21
|
+
];
|
|
22
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA6GH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,YAAY;IACZ,YAAY;IACZ,UAAU;CACF,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SpeechEvent } from "@node-webrtc-rust/sdk/voice";
|
|
2
|
+
export interface SessionContext {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
env: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
export interface SpeechContext {
|
|
7
|
+
sessionId: string;
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SpeechEventContext {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AgentHandlers {
|
|
14
|
+
onSessionStart?: (ctx: SessionContext) => void | Promise<void>;
|
|
15
|
+
/** Fired for every speech lifecycle event from the parent voice pipeline. */
|
|
16
|
+
onSpeechEvent?: (ctx: SpeechEventContext, event: SpeechEvent) => void | Promise<void>;
|
|
17
|
+
/** Convenience handler — also invoked when `speech.type` is `user_speech_final`. */
|
|
18
|
+
onUserSpeechFinal?: (ctx: SpeechContext) => void | Promise<void>;
|
|
19
|
+
onSessionEnd?: (ctx: {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
}) => void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Register IPC handlers for a customer agent child process.
|
|
25
|
+
* Call once at bundle entry; runner parent sends {@link ParentToChildMessage} events.
|
|
26
|
+
*/
|
|
27
|
+
export declare function defineAgent(handlers: AgentHandlers): void;
|
|
28
|
+
/** Ask the runner parent to synthesize speech for the session. */
|
|
29
|
+
export declare function speak(sessionId: string, text: string): void;
|
|
30
|
+
/** Structured log forwarded to the runner parent. */
|
|
31
|
+
export declare function agentLog(level: "info" | "error", message: string): void;
|
|
32
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAI/D,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,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;CACrE;AAYD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CA4CzD;AAED,kEAAkE;AAClE,wBAAgB,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED,qDAAqD;AACrD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAEvE"}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function isParentMessage(value) {
|
|
2
|
+
if (!value || typeof value !== "object")
|
|
3
|
+
return false;
|
|
4
|
+
const msg = value;
|
|
5
|
+
return (msg.type === "session_start" ||
|
|
6
|
+
msg.type === "speech_event" ||
|
|
7
|
+
msg.type === "session_end");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Register IPC handlers for a customer agent child process.
|
|
11
|
+
* Call once at bundle entry; runner parent sends {@link ParentToChildMessage} events.
|
|
12
|
+
*/
|
|
13
|
+
export function defineAgent(handlers) {
|
|
14
|
+
process.on("message", (message) => {
|
|
15
|
+
if (!isParentMessage(message))
|
|
16
|
+
return;
|
|
17
|
+
void (async () => {
|
|
18
|
+
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?.({ sessionId: message.sessionId }, message.event);
|
|
28
|
+
if (message.event.type === "user_speech_final" &&
|
|
29
|
+
typeof message.event.text === "string" &&
|
|
30
|
+
message.event.text.trim()) {
|
|
31
|
+
await handlers.onUserSpeechFinal?.({
|
|
32
|
+
sessionId: message.sessionId,
|
|
33
|
+
text: message.event.text.trim(),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
case "session_end":
|
|
38
|
+
await handlers.onSessionEnd?.({ sessionId: message.sessionId });
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const errMessage = error instanceof Error ? error.message : String(error);
|
|
44
|
+
process.send?.({
|
|
45
|
+
type: "agent_error",
|
|
46
|
+
sessionId: message.sessionId,
|
|
47
|
+
message: errMessage,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Ask the runner parent to synthesize speech for the session. */
|
|
54
|
+
export function speak(sessionId, text) {
|
|
55
|
+
process.send?.({ type: "speak", sessionId, text });
|
|
56
|
+
}
|
|
57
|
+
/** Structured log forwarded to the runner parent. */
|
|
58
|
+
export function agentLog(level, message) {
|
|
59
|
+
process.send?.({ type: "log", level, message });
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AA8BA,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,GAAG,GAAG,KAA0B,CAAC;IACvC,OAAO,CACL,GAAG,CAAC,IAAI,KAAK,eAAe;QAC5B,GAAG,CAAC,IAAI,KAAK,cAAc;QAC3B,GAAG,CAAC,IAAI,KAAK,aAAa,CAC3B,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,QAAuB;IACjD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAgB,EAAE,EAAE;QACzC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC;YAAE,OAAO;QAEtC,KAAK,CAAC,KAAK,IAAI,EAAE;YACf,IAAI,CAAC;gBACH,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;oBACrB,KAAK,eAAe;wBAClB,MAAM,QAAQ,CAAC,cAAc,EAAE,CAAC;4BAC9B,SAAS,EAAE,OAAO,CAAC,SAAS;4BAC5B,GAAG,EAAE,OAAO,CAAC,GAAG;yBACjB,CAAC,CAAC;wBACH,MAAM;oBACR,KAAK,cAAc;wBACjB,MAAM,QAAQ,CAAC,aAAa,EAAE,CAC5B,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,EAChC,OAAO,CAAC,KAAK,CACd,CAAC;wBACF,IACE,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,mBAAmB;4BAC1C,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ;4BACtC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EACzB,CAAC;4BACD,MAAM,QAAQ,CAAC,iBAAiB,EAAE,CAAC;gCACjC,SAAS,EAAE,OAAO,CAAC,SAAS;gCAC5B,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE;6BAChC,CAAC,CAAC;wBACL,CAAC;wBACD,MAAM;oBACR,KAAK,aAAa;wBAChB,MAAM,QAAQ,CAAC,YAAY,EAAE,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;wBAChE,MAAM;gBACV,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,UAAU,GACd,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACzD,OAAO,CAAC,IAAI,EAAE,CAAC;oBACb,IAAI,EAAE,aAAa;oBACnB,SAAS,EAAE,OAAO,CAAC,SAAS;oBAC5B,OAAO,EAAE,UAAU;iBACpB,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;IACP,CAAC,CAAC,CAAC;AACL,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,KAAK,CAAC,SAAiB,EAAE,IAAY;IACnD,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,QAAQ,CAAC,KAAuB,EAAE,OAAe;IAC/D,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;AAClD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voicethere/agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "VoiceThere customer agent SDK — IPC types and runtime helpers for sandboxed child bundles",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"templates",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json && esbuild examples/agent.ts --bundle --platform=node --format=esm --outfile=dist/agent.js",
|
|
19
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:ci": "npm run typecheck && npm run build && npm run test",
|
|
22
|
+
"verify:local": "npm run build && tsx scripts/verify-local.ts",
|
|
23
|
+
"verify:local:only": "tsx scripts/verify-local.ts",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/voicethere/agent.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/voicethere/agent/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/voicethere/agent#readme",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"voicethere",
|
|
39
|
+
"voice-agent",
|
|
40
|
+
"webrtc",
|
|
41
|
+
"sandbox"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22"
|
|
45
|
+
},
|
|
46
|
+
"license": "UNLICENSED",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@node-webrtc-rust/sdk": "0.5.2"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@node-webrtc-rust/sdk": ">=0.5.2"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^22.10.0",
|
|
55
|
+
"esbuild": "^0.25.12",
|
|
56
|
+
"tsx": "^4.19.2",
|
|
57
|
+
"typescript": "^5.7.0",
|
|
58
|
+
"vitest": "^2.1.8"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Agent templates
|
|
2
|
+
|
|
3
|
+
## `agent.ts`
|
|
4
|
+
|
|
5
|
+
Full starter bundle covering every speech event from `@node-webrtc-rust/sdk/voice`:
|
|
6
|
+
|
|
7
|
+
| Group | Events |
|
|
8
|
+
| ----------- | -------------------------------------------------------------------------------------------- |
|
|
9
|
+
| User VAD | `user_speaking_start`, `user_speaking_end`, `vad_triggered` |
|
|
10
|
+
| STT stream | `stt_stream_start`, `stt_stream_end`, `user_stt_start`, `user_stt_end`, `user_stt_not_found` |
|
|
11
|
+
| Transcripts | `user_speech_partial`, `user_speech_final` |
|
|
12
|
+
| Agent TTS | `agent_speaking_start`, `agent_speaking_end`, `barge_in` |
|
|
13
|
+
| Failures | `error` |
|
|
14
|
+
|
|
15
|
+
**Customize:** replace `onUserSpeechFinal` body with your LLM/tools; extend `PeerState` or swap `handleSpeechEvent` for your architecture.
|
|
16
|
+
|
|
17
|
+
**Build:**
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @voicethere/agent esbuild
|
|
21
|
+
npx esbuild agent.ts --bundle --platform=node --format=esm --outfile=dist/agent.js
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Verify sandbox (no WebRTC):** from the agent repo, `npm run verify:local` after building your bundle.
|
|
25
|
+
|
|
26
|
+
**Voice E2E:** host with [`voicethere/runner`](https://github.com/voicethere/runner) — set `AGENT_BUNDLE_PATH` to your built `dist/agent.js`.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full starter template — copy into your project and build to a single `agent.js` bundle.
|
|
3
|
+
*
|
|
4
|
+
* Handles every `SpeechEvent` from the runner voice pipeline (`@node-webrtc-rust/sdk/voice`).
|
|
5
|
+
* Replace the stub bodies with your product logic; keep `onUserSpeechFinal` as the main turn hook.
|
|
6
|
+
*
|
|
7
|
+
* Build:
|
|
8
|
+
* npm install @voicethere/agent
|
|
9
|
+
* npx esbuild agent.ts --bundle --platform=node --format=esm --outfile=dist/agent.js
|
|
10
|
+
*
|
|
11
|
+
* Local runner (clone https://github.com/voicethere/runner):
|
|
12
|
+
* AGENT_BUNDLE_PATH=./dist/agent.js npm run start
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
agentLog,
|
|
17
|
+
defineAgent,
|
|
18
|
+
speak,
|
|
19
|
+
type SpeechEvent,
|
|
20
|
+
} from "@voicethere/agent";
|
|
21
|
+
|
|
22
|
+
/** Per-peer conversational state — extend or replace with your store. */
|
|
23
|
+
interface PeerState {
|
|
24
|
+
userSpeaking: boolean;
|
|
25
|
+
agentSpeaking: boolean;
|
|
26
|
+
lastPartial: string;
|
|
27
|
+
sttActive: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const peers = new Map<string, PeerState>();
|
|
31
|
+
|
|
32
|
+
function peerState(sessionId: string): PeerState {
|
|
33
|
+
let state = peers.get(sessionId);
|
|
34
|
+
if (!state) {
|
|
35
|
+
state = {
|
|
36
|
+
userSpeaking: false,
|
|
37
|
+
agentSpeaking: false,
|
|
38
|
+
lastPartial: "",
|
|
39
|
+
sttActive: false,
|
|
40
|
+
};
|
|
41
|
+
peers.set(sessionId, state);
|
|
42
|
+
}
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatSpeechDetail(speech: SpeechEvent): string {
|
|
47
|
+
if (speech.text) return `"${speech.text}"`;
|
|
48
|
+
if (speech.error) return speech.error;
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Dispatch every speech lifecycle event from the parent Sherpa/VAD/STT/TTS pipeline.
|
|
54
|
+
* `speech.type` values match `SpeechEventType` in `@node-webrtc-rust/sdk/voice`.
|
|
55
|
+
*/
|
|
56
|
+
function handleSpeechEvent(sessionId: string, speech: SpeechEvent): void {
|
|
57
|
+
const state = peerState(sessionId);
|
|
58
|
+
const detail = formatSpeechDetail(speech);
|
|
59
|
+
|
|
60
|
+
switch (speech.type) {
|
|
61
|
+
// --- User turn (VAD + STT) ---
|
|
62
|
+
case "user_speaking_start":
|
|
63
|
+
state.userSpeaking = true;
|
|
64
|
+
agentLog("info", `[${sessionId}] user_speaking_start`);
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case "user_speaking_end":
|
|
68
|
+
state.userSpeaking = false;
|
|
69
|
+
agentLog("info", `[${sessionId}] user_speaking_end`);
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case "vad_triggered":
|
|
73
|
+
agentLog(
|
|
74
|
+
"info",
|
|
75
|
+
`[${sessionId}] vad_triggered — STT listen window opened`,
|
|
76
|
+
);
|
|
77
|
+
break;
|
|
78
|
+
|
|
79
|
+
case "stt_stream_start":
|
|
80
|
+
agentLog("info", `[${sessionId}] stt_stream_start`);
|
|
81
|
+
break;
|
|
82
|
+
|
|
83
|
+
case "stt_stream_end":
|
|
84
|
+
agentLog("info", `[${sessionId}] stt_stream_end`);
|
|
85
|
+
break;
|
|
86
|
+
|
|
87
|
+
case "user_stt_start":
|
|
88
|
+
state.sttActive = true;
|
|
89
|
+
state.lastPartial = "";
|
|
90
|
+
agentLog("info", `[${sessionId}] user_stt_start`);
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case "user_stt_end":
|
|
94
|
+
state.sttActive = false;
|
|
95
|
+
agentLog("info", `[${sessionId}] user_stt_end`);
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case "user_stt_not_found":
|
|
99
|
+
agentLog(
|
|
100
|
+
"info",
|
|
101
|
+
`[${sessionId}] user_stt_not_found — no speech recognized in listen window`,
|
|
102
|
+
);
|
|
103
|
+
// Optional: speak(sessionId, "I didn't catch that. Could you repeat?")
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case "user_speech_partial":
|
|
107
|
+
state.lastPartial = speech.text ?? "";
|
|
108
|
+
agentLog("info", `[${sessionId}] user_speech_partial ${detail}`);
|
|
109
|
+
// Optional: live captions, early intent detection, custom barge-in rules
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case "user_speech_final":
|
|
113
|
+
state.lastPartial = "";
|
|
114
|
+
agentLog("info", `[${sessionId}] user_speech_final ${detail}`);
|
|
115
|
+
// Primary turn boundary — `onUserSpeechFinal` runs after this for convenience
|
|
116
|
+
break;
|
|
117
|
+
|
|
118
|
+
// --- Agent playback (TTS) ---
|
|
119
|
+
case "agent_speaking_start":
|
|
120
|
+
state.agentSpeaking = true;
|
|
121
|
+
agentLog("info", `[${sessionId}] agent_speaking_start`);
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case "agent_speaking_end":
|
|
125
|
+
state.agentSpeaking = false;
|
|
126
|
+
agentLog("info", `[${sessionId}] agent_speaking_end`);
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case "barge_in":
|
|
130
|
+
agentLog(
|
|
131
|
+
"info",
|
|
132
|
+
`[${sessionId}] barge_in — user interrupted agent playback`,
|
|
133
|
+
);
|
|
134
|
+
// Optional: cancel in-flight LLM/TTS work keyed by sessionId
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case "error":
|
|
138
|
+
agentLog(
|
|
139
|
+
"error",
|
|
140
|
+
`[${sessionId}] pipeline error: ${speech.error ?? "unknown"}`,
|
|
141
|
+
);
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
default: {
|
|
145
|
+
const _exhaustive: never = speech.type;
|
|
146
|
+
agentLog(
|
|
147
|
+
"error",
|
|
148
|
+
`[${sessionId}] unhandled speech event: ${String(_exhaustive)}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
defineAgent({
|
|
155
|
+
onSessionStart({ sessionId, env }) {
|
|
156
|
+
agentLog("info", `session_start ${sessionId} env=${JSON.stringify(env)}`);
|
|
157
|
+
speak(sessionId, "Hello! How can I help?");
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
onSpeechEvent({ sessionId }, speech) {
|
|
161
|
+
handleSpeechEvent(sessionId, speech);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
onUserSpeechFinal({ sessionId, text }) {
|
|
165
|
+
// Replace with LLM / tool calls / business logic
|
|
166
|
+
speak(sessionId, `You said: ${text}`);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
onSessionEnd({ sessionId }) {
|
|
170
|
+
peers.delete(sessionId);
|
|
171
|
+
agentLog("info", `session_end ${sessionId}`);
|
|
172
|
+
},
|
|
173
|
+
});
|