@happyvertical/smrt-chat 0.38.24 → 0.38.26
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/AGENTS.md +24 -2
- package/README.md +123 -3
- package/dist/chunks/{ChatService-BRsE5HmY.js → ChatService-B9rJuwtb.js} +269 -110
- package/dist/chunks/ChatService-B9rJuwtb.js.map +1 -0
- package/dist/collections/VoiceGatewayTurnCollection.d.ts +13 -0
- package/dist/collections/VoiceGatewayTurnCollection.d.ts.map +1 -0
- package/dist/collections/VoiceSessionCollection.d.ts +11 -0
- package/dist/collections/VoiceSessionCollection.d.ts.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +495 -13
- package/dist/index.js.map +1 -1
- package/dist/internal/agent-runtime.js +1 -1
- package/dist/manifest.json +2795 -1892
- package/dist/models/VoiceGatewayTurn.d.ts +20 -0
- package/dist/models/VoiceGatewayTurn.d.ts.map +1 -0
- package/dist/models/VoiceSession.d.ts +40 -0
- package/dist/models/VoiceSession.d.ts.map +1 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/persona-conversation.d.ts +8 -0
- package/dist/persona-conversation.d.ts.map +1 -1
- package/dist/services/ChatService.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +597 -125
- package/dist/svelte/components/layout/ChatLayout.svelte +7 -3
- package/dist/svelte/components/messages/MessageInput.svelte +22 -15
- package/dist/svelte/components/messages/MessageInput.svelte.d.ts.map +1 -1
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +4 -2
- package/dist/ui.js.map +1 -1
- package/dist/voice.d.ts +127 -0
- package/dist/voice.d.ts.map +1 -0
- package/package.json +16 -14
- 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
|
-
###
|
|
245
|
+
### Internal collections
|
|
134
246
|
|
|
135
|
-
|
|
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
|
|