@happyvertical/smrt-chat 0.38.23 → 0.38.25

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.
Files changed (36) hide show
  1. package/AGENTS.md +24 -2
  2. package/README.md +123 -3
  3. package/dist/chunks/{ChatService-BRsE5HmY.js → ChatService-B9rJuwtb.js} +269 -110
  4. package/dist/chunks/ChatService-B9rJuwtb.js.map +1 -0
  5. package/dist/collections/VoiceGatewayTurnCollection.d.ts +13 -0
  6. package/dist/collections/VoiceGatewayTurnCollection.d.ts.map +1 -0
  7. package/dist/collections/VoiceSessionCollection.d.ts +11 -0
  8. package/dist/collections/VoiceSessionCollection.d.ts.map +1 -0
  9. package/dist/index.d.ts +4 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +495 -13
  12. package/dist/index.js.map +1 -1
  13. package/dist/internal/agent-runtime.js +1 -1
  14. package/dist/manifest.json +2726 -1823
  15. package/dist/models/VoiceGatewayTurn.d.ts +20 -0
  16. package/dist/models/VoiceGatewayTurn.d.ts.map +1 -0
  17. package/dist/models/VoiceSession.d.ts +40 -0
  18. package/dist/models/VoiceSession.d.ts.map +1 -0
  19. package/dist/models/index.d.ts +2 -0
  20. package/dist/models/index.d.ts.map +1 -1
  21. package/dist/persona-conversation.d.ts +8 -0
  22. package/dist/persona-conversation.d.ts.map +1 -1
  23. package/dist/services/ChatService.d.ts.map +1 -1
  24. package/dist/smrt-knowledge.json +656 -184
  25. package/dist/svelte/components/layout/ChatLayout.svelte +7 -3
  26. package/dist/svelte/components/messages/MessageInput.svelte +22 -15
  27. package/dist/svelte/components/messages/MessageInput.svelte.d.ts.map +1 -1
  28. package/dist/types.d.ts +30 -0
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/ui.d.ts.map +1 -1
  31. package/dist/ui.js +4 -2
  32. package/dist/ui.js.map +1 -1
  33. package/dist/voice.d.ts +127 -0
  34. package/dist/voice.d.ts.map +1 -0
  35. package/package.json +18 -16
  36. package/dist/chunks/ChatService-BRsE5HmY.js.map +0 -1
package/AGENTS.md CHANGED
@@ -2,11 +2,26 @@
2
2
 
3
3
  Chat rooms, threads, and agent sessions with app-controlled tool whitelisting.
4
4
 
5
+ ## Dev Server
6
+
7
+ `pnpm --dir packages/chat dev` runs a package-local SvelteKit workbench. The root
8
+ route is an interactive chat surface with a dev-only `/api/dev-chat` endpoint:
9
+ it uses `@happyvertical/ai` when local provider credentials are present and
10
+ falls back to a deterministic local assistant otherwise. `/previews` hosts the
11
+ shared component playground entries from `src/svelte/playground.ts`.
12
+
13
+ The root workbench also has a dev-only voice conversation mode. It reads voice
14
+ gateway connection details through `/api/dev-voice/config`, streams browser mic
15
+ audio to `WS /ws/voice` as PCM16 mono, appends gateway transcripts/responses to
16
+ the chat, and plays returned TTS audio. Exposing
17
+ `SMRT_CHAT_DEV_VOICE_GATEWAY_TOKEN` to the browser requires
18
+ `SMRT_CHAT_DEV_VOICE_GATEWAY_EXPOSE_TOKEN=true`; keep that local-only.
19
+
5
20
  ## Models
6
21
 
7
- Internal models — all mutations go through the membership/owner-checked `ChatService` (S5 #1392). EVERY `@smrt()` model in this package (ChatRoom, ChatMessage, ChatParticipant, ChatThread, ChatReaction, AgentSession) has a READ-ONLY generated REST/MCP surface (`list`/`get` only); `create`/`update`/`delete` are intentionally NOT generated so the raw collection routes cannot skip the service-layer authorization. A structural regression test enumerates the registry to assert no chat model exposes a mutating op.
22
+ Internal models — all mutations go through the membership/owner-checked `ChatService` (S5 #1392) or the voice adapter's binding-checked flow. EVERY `@smrt()` model in this package (ChatRoom, ChatMessage, ChatParticipant, ChatThread, ChatReaction, AgentSession, VoiceSession) has a READ-ONLY generated REST/MCP surface (`list`/`get` only); `create`/`update`/`delete` are intentionally NOT generated so the raw collection routes cannot skip the service-layer authorization. A structural regression test enumerates the registry to assert no chat model exposes a mutating op.
8
23
 
9
- `ChatService` is a CLOSED FACADE (S5 #1392). The raw collections (`rooms`, `messages`, `participants`, `threads`, `agentSessions`, `reactions`) are ES `#private` fields — they are NOT on the public `ChatService` type and the package index does NOT export the collection classes, so a consumer cannot do `chat.messages.create({senderProfileId, role})` / `new ChatParticipantCollection(...)` to mutate around the authorization. The security-sensitive internals (`#writeMessage`, `#emitAgentReply`, `#enrollParticipant`, `#loadActiveSession`, `#requireActiveMembership`, `#requireRoomAdmin`, `#extractToolName`) are ES `#private` too, so they are unreachable at runtime — TypeScript `private` alone is erased and would leave them callable on the prototype. The agent-reply bridge is a `Symbol`-keyed static (not the old enumerable `_runAgentReply`), reachable only by the module-local `sendAgentReply` that holds the non-exported symbol.
24
+ `ChatService` is a CLOSED FACADE (S5 #1392). The raw collections (`rooms`, `messages`, `participants`, `threads`, `agentSessions`, `reactions`, `voiceSessions`) are ES `#private` fields — they are NOT on the public `ChatService` type and the package index does NOT export the collection classes, so a consumer cannot do `chat.messages.create({senderProfileId, role})` / `new ChatParticipantCollection(...)` to mutate around the authorization. The security-sensitive internals (`#writeMessage`, `#emitAgentReply`, `#enrollParticipant`, `#loadActiveSession`, `#requireActiveMembership`, `#requireRoomAdmin`, `#extractToolName`) are ES `#private` too, so they are unreachable at runtime — TypeScript `private` alone is erased and would leave them callable on the prototype. The agent-reply bridge is a `Symbol`-keyed static (not the old enumerable `_runAgentReply`), reachable only by the module-local `sendAgentReply` that holds the non-exported symbol.
10
25
 
11
26
  - **ChatRoom**: `roomType` (public/private/dm/agent), `status`, `topic`, `maxParticipants`, `lastMessageAt`. Tenant-scoped (required).
12
27
  - **ChatMessage**: shared by users + agents. `role` (user/assistant/system/tool), `messageType` (text/system/action/file/tool_call/tool_result), `toolCallData` JSON. Unified model — no separate agent message type. Tenant-scoped (required).
@@ -14,6 +29,7 @@ Internal models — all mutations go through the membership/owner-checked `ChatS
14
29
  - **ChatThread**: `rootMessageId`, `isResolved`, `messageCount`. Created via `ChatService.startThread()` (member-checked). Tenant-scoped (required).
15
30
  - **ChatReaction**: `messageId`, `profileId`, `emoji`. Added/removed via `ChatService.addReaction()`/`removeReaction()` (member-checked, self-keyed). Tenant-scoped (required).
16
31
  - **AgentSession**: `agentId` (string ref, not FK), `allowedTools` (JSON string array), `sessionContext` (JSON), `systemPrompt`, limits (`maxTokens`/`maxMessages`/`expiresAt`). Optional tenancy.
32
+ - **VoiceSession**: short-lived voice-gateway binding over `(tenant, actorProfileId, personaId, room/thread/agentSession)` plus a persona snapshot, gateway `session_id`, expiry, replay tracking, and metadata. Tenant-scoped (required). Generated surface is read-only; creation and turns go through `createVoiceChatSession()` / `handleVoiceGatewayTurn()`.
17
33
 
18
34
  ## ChatService
19
35
 
@@ -34,6 +50,12 @@ The "chat with your learning agent" surface — the real agentic runtime for `Ag
34
50
  - **Agent orchestration** (L3, #1892) — the loop accepts non-manifest **`extraTools`** (`PrincipalTool[]`, from `@happyvertical/smrt-agents`), gated by the *same* fail-closed allow-list. The standard **`invoke-agent`** tool (`createInvokeAgentTool`, slug `agents.invoke`) lets a conversational agent delegate to a **worker agent under its own principal** — the worker runs via `executeAsPrincipal` as the originating user (never its own authority), the principal is immutable along the chain, and its completion is surfaced back into the conversation. `runPersonaConversationTurn` filters `extraTools` by the persona's `allowedTools` (offer gate); the tool's `execute` re-asserts `assertToolAllowed` (execution gate). See `@happyvertical/smrt-agents` for the delegation envelope + transports.
35
51
  - **Chat feedback capture** (`chat-feedback.ts`) — `captureChatFeedback()` + `acceptAppliedChange`/`rejectAppliedChange`/`correctResponse`/`rateResponse`/`thumbsUp`/`thumbsDown` write a `Feedback` row (personas) carrying the conversation's **correlation-id**, and (by default) reinforce the persona's learning memory (`reinforceFromFeedback`). So an in-chat reject decays a strategy below the reuse floor and it stops being recalled; a correction supersedes its stored value.
36
52
 
53
+ ## Voice Gateway Turns (#1910)
54
+
55
+ Voice is an input mode for the existing persona chat harness, not a separate chat runtime. `createVoiceChatSession()` creates a short-lived `VoiceSession` for an authenticated actor/profile, binding tenant, persona, agent session, room, and optional thread. `handleVoiceGatewayTurn()` resolves that binding from `metadata.voiceSessionId`, checks the gateway's `session_id` and any supplied tenant/profile/persona/session/thread metadata against the server-side binding, persists the transcript through `ChatService.sendAgentUserMessage()`, runs `runPersonaConversationTurn()`, stamps voice/correlation metadata onto the persisted user/assistant/tool messages, records the gateway `turn_id`, and returns the gateway response contract. The Fetch-compatible `createVoiceGatewayTurnHandler()` adds the coarse gateway bearer-token check.
56
+
57
+ The gateway bearer token proves only "this request came from the gateway"; it never authorizes the end user. The short-lived `VoiceSession` binding is the user/session proof, and untrusted gateway metadata must be validated against that binding before any chat write or tool loop. Tool execution remains fail-closed through the persona allow-list mirrored onto `AgentSession` by `bindPersonaToSession()`.
58
+
37
59
  ## Gotchas
38
60
 
39
61
  - **sessionContext, not context**: `context` is reserved for slug scoping. Use `getSessionContext()`/`updateSessionContext()` for agent memory.
package/README.md CHANGED
@@ -10,6 +10,30 @@ pnpm add @happyvertical/smrt-chat
10
10
 
11
11
  ## Usage
12
12
 
13
+ ### Local dev server
14
+
15
+ Run the chat package workbench directly:
16
+
17
+ ```bash
18
+ pnpm --dir packages/chat dev
19
+ ```
20
+
21
+ The root route opens an interactive chat surface backed by
22
+ `src/routes/api/dev-chat/+server.ts`. If `SMRT_CHAT_DEV_PROVIDER` /
23
+ `SMRT_CHAT_DEV_API_KEY` or standard provider credentials such as
24
+ `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or `GEMINI_API_KEY` are available, the
25
+ assistant turn goes through `@happyvertical/ai`; otherwise it uses a
26
+ deterministic local fallback. Component previews are available at `/previews`.
27
+
28
+ The workbench also has a local-only voice conversation mode. It reads
29
+ `SMRT_CHAT_DEV_VOICE_GATEWAY_HTTP_URL` and
30
+ `SMRT_CHAT_DEV_VOICE_GATEWAY_WS_URL`, fetches gateway targets from
31
+ `/api/dev-voice/config`, streams browser microphone audio to `WS /ws/voice` as
32
+ PCM16 mono at 16 kHz, appends gateway transcripts/responses to the chat, and
33
+ plays returned TTS audio. If the gateway requires a bearer token, set
34
+ `SMRT_CHAT_DEV_VOICE_GATEWAY_TOKEN`; browser WebSocket testing also requires
35
+ `SMRT_CHAT_DEV_VOICE_GATEWAY_EXPOSE_TOKEN=true`, so use this only for local dev.
36
+
13
37
  ### Rooms and messages
14
38
 
15
39
  ```typescript
@@ -104,6 +128,93 @@ if (session.isActive()) {
104
128
  }
105
129
  ```
106
130
 
131
+ ### Voice gateway turns
132
+
133
+ `smrt-chat` can expose a gateway-facing turn target for browser voice input.
134
+ The browser receives only a short-lived SMRT voice session binding; the
135
+ gateway-to-SMRT service bearer token stays server-side.
136
+
137
+ ```typescript
138
+ import {
139
+ createVoiceChatSession,
140
+ createVoiceGatewayTurnHandler,
141
+ } from '@happyvertical/smrt-chat';
142
+
143
+ // Authenticated app route: bind the speaking profile to a persona and an
144
+ // existing or newly-created agent session.
145
+ const voice = await createVoiceChatSession({
146
+ chatService: chat,
147
+ db,
148
+ tenantId: locals.tenantId,
149
+ actorProfileId: locals.profileId,
150
+ actorUserId: locals.userId,
151
+ persona: resolvedPersona,
152
+ agentSessionId: session.id, // omit to create/reuse an agent session
153
+ ttlSeconds: 600,
154
+ });
155
+
156
+ // Return voice.gatewaySessionId + voice.metadata to the browser/gateway. Do not
157
+ // return the gateway service token.
158
+
159
+ // Gateway target route: configure the deployed voice gateway HTTP target to
160
+ // POST here with Authorization: Bearer <SMRT_VOICE_GATEWAY_TOKEN>.
161
+ export const POST = ({ request }) =>
162
+ createVoiceGatewayTurnHandler({
163
+ chatService: chat,
164
+ db,
165
+ ai,
166
+ gatewayToken: process.env.SMRT_VOICE_GATEWAY_TOKEN ?? '',
167
+ })(request);
168
+ ```
169
+
170
+ The gateway sends the transcribed text turn:
171
+
172
+ ```json
173
+ {
174
+ "session_id": "gateway-or-smrt-stable-session-id",
175
+ "turn_id": "gateway-generated-turn-id",
176
+ "target": "smrt:chat",
177
+ "actor": "optional-display-name",
178
+ "text": "transcribed user utterance",
179
+ "metadata": {
180
+ "tenantId": "tenant uuid",
181
+ "actorProfileId": "profile id",
182
+ "chatRoomId": "room id",
183
+ "threadId": "optional thread id",
184
+ "agentSessionId": "agent session id",
185
+ "personaId": "persona id",
186
+ "voiceSessionId": "SMRT-issued voice session id",
187
+ "source": "voice-gateway"
188
+ }
189
+ }
190
+ ```
191
+
192
+ SMRT validates `metadata.voiceSessionId` against its server-side binding, then
193
+ checks any supplied tenant/profile/persona/session/thread ids against that
194
+ binding instead of trusting the gateway metadata. A valid turn is persisted as a
195
+ normal user chat message, routed through `runPersonaConversationTurn()`, and the
196
+ assistant reply is persisted as a normal assistant message. The response is:
197
+
198
+ ```json
199
+ {
200
+ "session_id": "same session_id",
201
+ "turn_id": "same turn_id",
202
+ "text": "assistant response text",
203
+ "metadata": {
204
+ "tenantId": "tenant id",
205
+ "chatRoomId": "room id",
206
+ "threadId": null,
207
+ "agentSessionId": "agent session id",
208
+ "userMessageId": "persisted transcript message id",
209
+ "assistantMessageId": "persisted assistant message id",
210
+ "personaId": "persona id",
211
+ "correlationId": "feedback correlation id",
212
+ "voiceSessionId": "SMRT-issued voice session id",
213
+ "source": "voice-gateway"
214
+ }
215
+ }
216
+ ```
217
+
107
218
  ### Direct messages
108
219
 
109
220
  ```typescript
@@ -129,10 +240,15 @@ const dmRoom = await chat.getOrCreateDM({
129
240
  | `ChatThread` | Threaded conversation linked to a root message, with resolve/reopen lifecycle |
130
241
  | `ChatReaction` | Emoji reaction on a message |
131
242
  | `AgentSession` | AI agent session with `allowedTools` (JSON string array), `sessionContext` for multi-turn memory, `systemPrompt`, and usage limits (`maxTokens`/`maxMessages`/`expiresAt`) |
243
+ | `VoiceSession` | Short-lived voice gateway binding over a tenant, actor profile, persona, and agent session |
132
244
 
133
- ### Collections
245
+ ### Internal collections
134
246
 
135
- | Export | Description |
247
+ Raw collection classes are not exported from the package index. They are listed
248
+ here as implementation details; application code should use `ChatService` and
249
+ the voice helpers.
250
+
251
+ | Internal class | Description |
136
252
  |--------|------------|
137
253
  | `ChatRoomCollection` | Room queries, `findOrCreateDM()` |
138
254
  | `ChatMessageCollection` | Message queries and search filters |
@@ -140,16 +256,20 @@ const dmRoom = await chat.getOrCreateDM({
140
256
  | `ChatThreadCollection` | Thread queries |
141
257
  | `ChatReactionCollection` | Reaction queries |
142
258
  | `AgentSessionCollection` | Session queries, `findActiveSession()`, `findOrCreate()` |
259
+ | `VoiceSessionCollection` | Voice session binding queries and stale-expiry helper |
143
260
 
144
261
  ### Services
145
262
 
146
263
  | Export | Description |
147
264
  |--------|------------|
148
265
  | `ChatService` | Facade: `createRoom()`, `sendMessage()`, `startThread()`, `addParticipant()`, `removeParticipant()`, `updateRoom()`, `addReaction()`, `removeReaction()`, `getOrCreateDM()`, `createAgentSession()`, `sendAgentUserMessage()`, `getRoomMessages()`, `getRoomForMember()`, `updateAgentSessionConfig()`. Every write takes a server-supplied `actorProfileId`. The agent-authored reply path (`sendAgentReply`) is intentionally NOT on this facade or the package index — it is an internal function in `services/ChatService.ts` for the trusted in-process agent runtime only (S5 #1392). |
266
+ | `createVoiceChatSession()` | Creates a short-lived voice binding for an authenticated actor and persona, optionally against an existing agent session |
267
+ | `handleVoiceGatewayTurn()` | Lower-level gateway turn adapter for non-Fetch hosts |
268
+ | `createVoiceGatewayTurnHandler()` | Fetch-compatible HTTP handler for the gateway target endpoint |
149
269
 
150
270
  ### Types
151
271
 
152
- `ChatRoomType`, `ChatRoomStatus`, `ChatRoomOptions`, `ChatMessageType`, `ChatMessageRole`, `ChatMessageOptions`, `ChatMessageSearchFilters`, `ChatParticipantRole`, `ChatParticipantStatus`, `ChatParticipantOptions`, `OnlineStatus`, `ChatThreadOptions`, `ChatReactionOptions`, `AgentSessionStatus`, `AgentSessionOptions`
272
+ `ChatRoomType`, `ChatRoomStatus`, `ChatRoomOptions`, `ChatMessageType`, `ChatMessageRole`, `ChatMessageOptions`, `ChatMessageSearchFilters`, `ChatParticipantRole`, `ChatParticipantStatus`, `ChatParticipantOptions`, `OnlineStatus`, `ChatThreadOptions`, `ChatReactionOptions`, `AgentSessionStatus`, `AgentSessionOptions`, `VoiceSessionStatus`, `VoiceSessionOptions`, `VoiceGatewayTurnPayload`, `VoiceGatewayTurnResponse`
153
273
 
154
274
  ### Constants
155
275