@happyvertical/smrt-chat 0.55.6 → 0.55.8

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 CHANGED
@@ -97,6 +97,31 @@ fail-closed `allowedTools` offer/execution gates; RBAC, tenant, redaction,
97
97
  bounded query results, and audit authority remain in the authenticated
98
98
  `PrincipalRun` supplied by `@happyvertical/smrt-agents`.
99
99
 
100
+ ## Runtime report tools (#3711)
101
+
102
+ `createRuntimeReportTools()` (`runtime-report-tools.ts`) returns four
103
+ `PrincipalTool`s for the persona `extraTools` seam: `reports.runtime.sources`,
104
+ `reports.runtime.define` (`preview` / `apply`), `reports.runtime.list`, and
105
+ `reports.runtime.run`. The model supplies only a declarative
106
+ `RuntimeReportSpec` from `@happyvertical/smrt-reports`; sources are a
107
+ server-owned allow-list gated by `run.assertOperation(collection, 'read')`, and
108
+ field policy, `readPermission`, and tenancy are enforced by the compiler against
109
+ the live run on every compile and every stored-report run. `apply` needs the
110
+ `specHash` of the previewed spec plus an app-owned `confirmation` host that
111
+ resolves only after a human approved that exact spec; with no host the tools are
112
+ propose-only and a model-supplied `confirmed` flag is ignored. Saving also needs
113
+ `create` on the `RuntimeReport` collection, and `list`/`run` need `read`; the
114
+ default `reportsCollection` is the `RuntimeReport` class itself, which resolves
115
+ to the catalog slug. A string `collection`/`reportsCollection` is used verbatim
116
+ as a catalog slug (never a class name); tests must run the tools under the real
117
+ `assertOperationPermission` guard so a wrong slug fails. A stored report is
118
+ written only by this confirmed `apply`: the model layer refuses every other
119
+ insert/spec change, and the generic manifest tools (`buildManifestToolCatalog`,
120
+ `invokeManifestTool`) never offer or run a `RuntimeReport` write. Stored-report
121
+ reads/saves run under the principal's tenant (`withPrincipalTenant`), refuse a
122
+ mismatching ambient tenant, and filter by the principal tenant explicitly. Contract and authority model:
123
+ [`packages/reports/agents/runtime-reports.md`](../reports/agents/runtime-reports.md).
124
+
100
125
  ## Conversational Harness (L3, #1891)
101
126
 
102
127
  The `AgentSession` runtime depends on personas, agents, and users. Keep those
@@ -115,13 +140,12 @@ The gateway bearer token proves only "this request came from the gateway"; it ne
115
140
 
116
141
  ## Token Streaming (SSE, #1936)
117
142
 
118
- `chat-stream.ts` is the SSE seam for embeddable conversational UIs (first consumer: the Happy chat widget, `animation#5`): a client POSTs the conversation so far and receives a `text/event-stream` of `data: <json>` frames — `token` deltas as the model generates, then a final `done` frame with the message. The wire `ChatStreamEvent` union also declares `emotion` and `control` (#1921 host-page control commands) lanes for forward compatibility; the v1 engine emits `token`/`done`/`error`.
119
-
120
- - **`runChatConversationStream({ context, messages })`** — the transport-agnostic engine (an `AsyncGenerator<ChatStreamEvent>`). Dispatches on `context.binding`: **persona-bound** runs the full `runPersonaConversationTurn` with a token sink wired through the tool loop (`onToken` → `ai.chat({ stream: true, onProgress })`), then persists via `ChatService` and emits the persisted message as `done`; **plain/unbound** streams `ai.stream()` directly and emits a synthesized (unpersisted) `done`. Streamed tokens are a live PREVIEW (a tool-call round may narrate before acting); the `done` message is authoritative. Failures surface as an in-band `error` event, never a throw (the 200 has already committed once streaming starts).
121
- - **`createChatStreamHandler({ authorize, allowedOrigins?, allowCredentials? })`** — a Fetch-compatible handler returning `text/event-stream` (mirrors `createVoiceGatewayTurnHandler`). `authorize(request, body)` is the SOLE trust boundary and works exactly like the voice gateway: this module NEVER authorizes from the request's `session` metadata — the app validates the caller (bearer session id / cookie / same-origin) and the claimed ids against the authenticated principal, and returns an already-authorized `ChatStreamContext`. Generation caps (`model`/`maxTokens`/`maxSteps`) live on the context (server-resolved), never on the request. Cross-origin embedding uses the same fail-closed CORS posture as core `_events` (#1861): the `Origin` is echoed only when allow-listed (never `*`), credentials only when opted in.
122
- - **`SmrtChatBackend` (`@happyvertical/smrt-chat/client`)** — the consume side of the same contract: a browser SSE client (`src/client.ts`) that POSTs the conversation and dispatches `token`/`emotion`/`done`/`error` frames to streaming handlers, tolerating heartbeat comments and frames split across chunks. The subpath is BROWSER-SAFE and dependency-free (no server runtime, no workspace imports — keep it that way), and its widget-facing types are structurally identical to `@happyvertical/animation`'s `ChatBackend` contract so an instance plugs straight into the floating chat widget. `src/client.contract.ts` carries the compile-time locks pinning it to `chat-stream.ts`'s `ChatStreamEvent`/`ChatStreamSession` — a NON-test module precisely so `pnpm typecheck` actually enforces them (`tsconfig.typecheck.json` excludes `.test.ts` files, and Vitest transpiles without typechecking). A clean close without a `done` frame surfaces as an error (never an empty reply), and a settled turn cancels the reader so the connection is released promptly.
123
- - **Persona path reuses the harness's own gates unchanged** — persona principal, fail-closed `allowedTools` offer+execution gates, tenant binding. `onToken` is best-effort telemetry threaded through `runToolLoop`; it never changes what the loop persists or authorizes.
124
- - **Custom tools stream via `binding.extraTools`** — the persona binding threads an optional `extraTools?: PrincipalTool[]` down to `runPersonaConversationTurn`, so a *streamed* persona chat can offer non-manifest, service-backed tools (the persona messaging tool `messages.send`, or an assistance-request/lead-ticket tool wrapping a `@smrt({ api:false, mcp:false })` service) and thus *act*, not only answer — matching the non-streaming persona path. It is resolved server-side by `authorize` (trusted), never from request input, and stays fully gated: each tool is filtered by the persona's `allowedTools` (offer gate) and re-asserts the bound principal's authority in `execute` (execution gate). Offering a tool is not authorizing it.
143
+ `chat-stream.ts` is the SSE seam for embeddable conversational UIs:
144
+ `runChatConversationStream()` (engine), `createChatStreamHandler({ authorize })`
145
+ (Fetch handler; `authorize` is the sole trust boundary), and the browser-safe
146
+ `SmrtChatBackend` (`./client`). Persona-bound streams reuse the harness gates
147
+ unchanged, and `binding.extraTools` is resolved server-side. Full contract:
148
+ [agents/token-streaming.md](agents/token-streaming.md).
125
149
 
126
150
  ## Streamed assistant turns with browser tools (#2908)
127
151
 
@@ -160,6 +184,13 @@ the config `ai` block. Pass `runtime`: a turn never keeps the RLS request tx
160
184
  (`clientRequestMessageId`): the PK is the retry reservation. See
161
185
  [docs](../../docs/assistant-dock.md).
162
186
 
187
+ ## Recipes (#3719)
188
+
189
+ `src/recipes.ts` declares `chat.assistant` (the AssistantDock as a `header.end`
190
+ shell widget, an `llm` provider, runtime `both`; help in
191
+ `src/assistant.recipe.md`). Keep each recipe class self-contained. Details:
192
+ [agents/recipes.md](agents/recipes.md).
193
+
163
194
  ## Gotchas
164
195
 
165
196
  - **sessionContext, not context**: `context` is reserved for slug scoping. Use `getSessionContext()`/`updateSessionContext()` for agent memory.
package/README.md CHANGED
@@ -261,6 +261,8 @@ waiting | done | failed | cancelled`), with `pauseRun` / `continueRun`,
261
261
  (the host then opens a thread via `oncontroller`). `matchesToolAllowList` is exported browser-safe. Full guide:
262
262
  [`docs/assistant-dock.md`](../../docs/assistant-dock.md).
263
263
 
264
+ Pass `conversations="single"` to `AssistantDock` for an app where people should not see separate conversations: it reuses the newest thread (or creates one), opens the focused composer, and offers an icon-only clear action.
265
+
264
266
  The server side mounts in one route file. The principal comes from
265
267
  `event.locals` (`@happyvertical/smrt-users/sveltekit`); threads are scoped to
266
268
  the caller's own assistant session in the active tenant; tools are
@@ -285,6 +287,14 @@ In the page, `createAssistantHttpTransport({ endpoint: '/api/assistant' })`
285
287
  is the dock's `transport`, and `createAssistantHttpActionClient` its
286
288
  `actionClient` when the route is given `actions: { adapter }`.
287
289
 
290
+ ### Recipes
291
+
292
+ `AssistantRecipe` (`chat.assistant`) declares the assistant dock for app and
293
+ agent catalogs: a shell widget in `header.end` that mounts `AssistantDock`
294
+ from `@happyvertical/smrt-chat/svelte`, an `llm` provider (hosted keys
295
+ `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` or `GEMINI_API_KEY`, or a browser model
296
+ with no key), and user-facing help. See `agents/recipes.md`.
297
+
288
298
  ## API
289
299
 
290
300
  ### Models
@@ -0,0 +1,38 @@
1
+ # Chat recipes (#3719)
2
+
3
+ Recipes are declared in `src/recipes.ts` (see
4
+ [`packages/core/agents/recipes.md`](../../core/agents/recipes.md)) and are
5
+ read statically by the scanner. Each class stays self-contained: rooms and the
6
+ assistant are separate recipes that add their own class to the same file.
7
+
8
+ ## `chat.assistant` (`AssistantRecipe`)
9
+
10
+ The AI assistant dock as a shell widget. Help: `src/assistant.recipe.md`.
11
+
12
+ - **Surface.** One `shell-widget` in `header.end` exporting
13
+ `@happyvertical/smrt-chat/svelte#AssistantDock`. `header.end` is where
14
+ `AppShell` puts `dockToggles` by default; the toggle and the
15
+ `ShellDockTool` come from the host shell, not from this package. The host
16
+ supplies the dock's `transport` and the shell's `DataSurfaceRegistry`
17
+ ([`docs/assistant-dock.md`](../../../docs/assistant-dock.md)).
18
+ - **Models.** `AgentSession`, `ChatRoom`, `ChatParticipant`, `ChatThread`,
19
+ `ChatMessage`: the records the assistant routes persist into. No `nav`: the
20
+ generated list routes are tenant-scoped, not member-scoped, so the dock never
21
+ reads them and no admin screen should.
22
+ - **Provider.** One `llm` provider. Hosted options (`openai`, `anthropic`,
23
+ `gemini`) each need their own key (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`,
24
+ `GEMINI_API_KEY`, the names `@happyvertical/smrt-config` resolves); the
25
+ browser options (`webllm`, `bitgpu`) need none. Names only, never values.
26
+ - **Runtime.** `both`: the dock, the in-memory transport and the browser
27
+ inference path run in the browser; saved conversations and tool turns go
28
+ through `mountAssistantRoutes()` on the server.
29
+ - **No `demoSeed`.** The package ships no assistant fixture; the in-memory
30
+ transport starts empty. Add one with `demoSeed` once a fixture exists.
31
+ - **Report tools (#3711).** The help describes `createRuntimeReportTools()` as
32
+ an opt-in capability a host passes through `extraTools`. The recipe does not
33
+ wire them.
34
+
35
+ Tests: `src/__tests__/recipes.test.ts` scans `src` with the real scanner
36
+ (declaration validity, qualified model names, emitted surfaces, providers and
37
+ runtime), runs `ManifestGenerator.assertRecipeOptions`/`assertRecipeHelp`, and
38
+ renders the help.
@@ -0,0 +1,12 @@
1
+ # Token Streaming (SSE, #1936)
2
+
3
+ Moved verbatim from `packages/chat/AGENTS.md` to keep the instruction chain under
4
+ the 32 KB cap (`pnpm check:agents-chain`). The summary stays in AGENTS.md.
5
+
6
+ `chat-stream.ts` is the SSE seam for embeddable conversational UIs (first consumer: the Happy chat widget, `animation#5`): a client POSTs the conversation so far and receives a `text/event-stream` of `data: <json>` frames — `token` deltas as the model generates, then a final `done` frame with the message. The wire `ChatStreamEvent` union also declares `emotion` and `control` (#1921 host-page control commands) lanes for forward compatibility; the v1 engine emits `token`/`done`/`error`.
7
+
8
+ - **`runChatConversationStream({ context, messages })`** — the transport-agnostic engine (an `AsyncGenerator<ChatStreamEvent>`). Dispatches on `context.binding`: **persona-bound** runs the full `runPersonaConversationTurn` with a token sink wired through the tool loop (`onToken` → `ai.chat({ stream: true, onProgress })`), then persists via `ChatService` and emits the persisted message as `done`; **plain/unbound** streams `ai.stream()` directly and emits a synthesized (unpersisted) `done`. Streamed tokens are a live PREVIEW (a tool-call round may narrate before acting); the `done` message is authoritative. Failures surface as an in-band `error` event, never a throw (the 200 has already committed once streaming starts).
9
+ - **`createChatStreamHandler({ authorize, allowedOrigins?, allowCredentials? })`** — a Fetch-compatible handler returning `text/event-stream` (mirrors `createVoiceGatewayTurnHandler`). `authorize(request, body)` is the SOLE trust boundary and works exactly like the voice gateway: this module NEVER authorizes from the request's `session` metadata — the app validates the caller (bearer session id / cookie / same-origin) and the claimed ids against the authenticated principal, and returns an already-authorized `ChatStreamContext`. Generation caps (`model`/`maxTokens`/`maxSteps`) live on the context (server-resolved), never on the request. Cross-origin embedding uses the same fail-closed CORS posture as core `_events` (#1861): the `Origin` is echoed only when allow-listed (never `*`), credentials only when opted in.
10
+ - **`SmrtChatBackend` (`@happyvertical/smrt-chat/client`)** — the consume side of the same contract: a browser SSE client (`src/client.ts`) that POSTs the conversation and dispatches `token`/`emotion`/`done`/`error` frames to streaming handlers, tolerating heartbeat comments and frames split across chunks. The subpath is BROWSER-SAFE and dependency-free (no server runtime, no workspace imports — keep it that way), and its widget-facing types are structurally identical to `@happyvertical/animation`'s `ChatBackend` contract so an instance plugs straight into the floating chat widget. `src/client.contract.ts` carries the compile-time locks pinning it to `chat-stream.ts`'s `ChatStreamEvent`/`ChatStreamSession` — a NON-test module precisely so `pnpm typecheck` actually enforces them (`tsconfig.typecheck.json` excludes `.test.ts` files, and Vitest transpiles without typechecking). A clean close without a `done` frame surfaces as an error (never an empty reply), and a settled turn cancels the reader so the connection is released promptly.
11
+ - **Persona path reuses the harness's own gates unchanged** — persona principal, fail-closed `allowedTools` offer+execution gates, tenant binding. `onToken` is best-effort telemetry threaded through `runToolLoop`; it never changes what the loop persists or authorizes.
12
+ - **Custom tools stream via `binding.extraTools`** — the persona binding threads an optional `extraTools?: PrincipalTool[]` down to `runPersonaConversationTurn`, so a *streamed* persona chat can offer non-manifest, service-backed tools (the persona messaging tool `messages.send`, or an assistance-request/lead-ticket tool wrapping a `@smrt({ api:false, mcp:false })` service) and thus *act*, not only answer — matching the non-streaming persona path. It is resolved server-side by `authorize` (trusted), never from request input, and stays fully gated: each tool is filtered by the persona's `allowedTools` (offer gate) and re-asserts the bound principal's authority in `execute` (execution gate). Offering a tool is not authorizing it.