@happyvertical/smrt-chat 0.55.9 → 0.55.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/AGENTS.md +25 -14
  2. package/README.md +12 -0
  3. package/agents/dev-workbench.md +69 -0
  4. package/agents/helper-preferences.md +65 -0
  5. package/agents/overview-tools.md +172 -0
  6. package/agents/token-streaming.md +3 -2
  7. package/dist/chunks/{ChatService-C9UxuJaw.js → ChatService-CFaaDHyU.js} +2 -2
  8. package/dist/chunks/{ChatService-C9UxuJaw.js.map → ChatService-CFaaDHyU.js.map} +1 -1
  9. package/dist/chunks/chat-stream-n-7AqsgF.js +2027 -0
  10. package/dist/chunks/chat-stream-n-7AqsgF.js.map +1 -0
  11. package/dist/dev-assistant-transport.d.ts +5 -0
  12. package/dist/dev-assistant-transport.d.ts.map +1 -0
  13. package/dist/dev-character-persistence-client.d.ts +24 -0
  14. package/dist/dev-character-persistence-client.d.ts.map +1 -0
  15. package/dist/dev-helper-client.d.ts +4 -0
  16. package/dist/dev-helper-client.d.ts.map +1 -0
  17. package/dist/dev-helper-server.d.ts +18 -0
  18. package/dist/dev-helper-server.d.ts.map +1 -0
  19. package/dist/helper-preferences-service.d.ts +22 -0
  20. package/dist/helper-preferences-service.d.ts.map +1 -0
  21. package/dist/helper-preferences.d.ts +84 -0
  22. package/dist/helper-preferences.d.ts.map +1 -0
  23. package/dist/helper-preferences.js +32 -0
  24. package/dist/helper-preferences.js.map +1 -0
  25. package/dist/helper-profile-store.d.ts +27 -0
  26. package/dist/helper-profile-store.d.ts.map +1 -0
  27. package/dist/index.d.ts +5 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +683 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/internal/agent-runtime.js +1 -1
  32. package/dist/manifest.json +122 -3
  33. package/dist/overview-tools.d.ts +221 -0
  34. package/dist/overview-tools.d.ts.map +1 -0
  35. package/dist/playground.d.ts +1 -1
  36. package/dist/playground.d.ts.map +1 -1
  37. package/dist/playground.js +99 -88
  38. package/dist/playground.js.map +1 -1
  39. package/dist/recipes.d.ts +81 -0
  40. package/dist/recipes.d.ts.map +1 -1
  41. package/dist/routes/api/dev-character-conversation/protocol.d.ts +33 -0
  42. package/dist/routes/api/dev-character-conversation/protocol.d.ts.map +1 -0
  43. package/dist/routes/api/dev-character-persistence/config.d.ts +24 -0
  44. package/dist/routes/api/dev-character-persistence/config.d.ts.map +1 -0
  45. package/dist/scripts/provision-dev-character-persistence.d.ts +3 -0
  46. package/dist/scripts/provision-dev-character-persistence.d.ts.map +1 -0
  47. package/dist/smrt-knowledge.json +144 -11
  48. package/dist/svelte/components/assistant/AssistantDock.svelte +58 -2
  49. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +9 -0
  50. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -1
  51. package/dist/svelte/components/assistant/FloatingAssistant.svelte +309 -0
  52. package/dist/svelte/components/assistant/FloatingAssistant.svelte.d.ts +27 -0
  53. package/dist/svelte/components/assistant/FloatingAssistant.svelte.d.ts.map +1 -0
  54. package/dist/svelte/components/assistant/captions/CaptionOverlay.svelte +105 -0
  55. package/dist/svelte/components/assistant/captions/CaptionOverlay.svelte.d.ts +11 -0
  56. package/dist/svelte/components/assistant/captions/CaptionOverlay.svelte.d.ts.map +1 -0
  57. package/dist/svelte/components/assistant/captions/HeardCaptions.svelte +65 -0
  58. package/dist/svelte/components/assistant/captions/HeardCaptions.svelte.d.ts +19 -0
  59. package/dist/svelte/components/assistant/captions/HeardCaptions.svelte.d.ts.map +1 -0
  60. package/dist/svelte/components/assistant/captions/SpokenCaptions.svelte +65 -0
  61. package/dist/svelte/components/assistant/captions/SpokenCaptions.svelte.d.ts +21 -0
  62. package/dist/svelte/components/assistant/captions/SpokenCaptions.svelte.d.ts.map +1 -0
  63. package/dist/svelte/components/assistant/captions/caption-overlay-context.d.ts +3 -0
  64. package/dist/svelte/components/assistant/captions/caption-overlay-context.d.ts.map +1 -0
  65. package/dist/svelte/components/assistant/captions/caption-overlay-context.js +2 -0
  66. package/dist/svelte/components/assistant/captions/caption-state.svelte.d.ts +71 -0
  67. package/dist/svelte/components/assistant/captions/caption-state.svelte.d.ts.map +1 -0
  68. package/dist/svelte/components/assistant/captions/caption-state.svelte.js +225 -0
  69. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts.map +1 -1
  70. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.js +3 -1
  71. package/dist/svelte/components/helper/HelperControlPanel.svelte +401 -0
  72. package/dist/svelte/components/helper/HelperControlPanel.svelte.d.ts +23 -0
  73. package/dist/svelte/components/helper/HelperControlPanel.svelte.d.ts.map +1 -0
  74. package/dist/svelte/components/helper/registry.d.ts +53 -0
  75. package/dist/svelte/components/helper/registry.d.ts.map +1 -0
  76. package/dist/svelte/components/helper/registry.js +100 -0
  77. package/dist/svelte/i18n.d.ts +14 -0
  78. package/dist/svelte/i18n.d.ts.map +1 -1
  79. package/dist/svelte/i18n.js +17 -0
  80. package/dist/svelte/i18n.messages.d.ts +32 -0
  81. package/dist/svelte/i18n.messages.d.ts.map +1 -1
  82. package/dist/svelte/i18n.messages.js +33 -0
  83. package/dist/svelte/index.d.ts +7 -0
  84. package/dist/svelte/index.d.ts.map +1 -1
  85. package/dist/svelte/index.js +15 -5
  86. package/dist/svelte/playground.d.ts +94 -0
  87. package/dist/svelte/playground.d.ts.map +1 -1
  88. package/dist/svelte/playground.js +16 -5
  89. package/dist/sveltekit.js +2 -2
  90. package/package.json +27 -17
  91. package/dist/chunks/chat-stream-BBk_5_Ph.js +0 -2027
  92. package/dist/chunks/chat-stream-BBk_5_Ph.js.map +0 -1
package/AGENTS.md CHANGED
@@ -5,7 +5,9 @@ Chat rooms, threads, and agent sessions with app-controlled tool whitelisting.
5
5
  ## Dev Server
6
6
 
7
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:
8
+ route separates **Character setup** and **Chat dev** into tabs; switching tabs
9
+ retains the current character rig and chat state. The chat tab is an interactive
10
+ surface with a dev-only `/api/dev-chat` endpoint:
9
11
  it uses `@happyvertical/ai` when local provider credentials are present and
10
12
  falls back to a deterministic local assistant otherwise. `/api/dev-chat-stream`
11
13
  is its SSE companion (#1936) — the same provider/local-fallback resolution wired
@@ -21,12 +23,10 @@ over `src/routes/chat-dev.intents.ts`, #2588). Their dev-config dependencies
21
23
  stylesheet import) and the stage→apply tool-call gotchas are in
22
24
  [agents/dev-workbench.md](agents/dev-workbench.md).
23
25
 
24
- The root workbench also has a dev-only voice conversation mode. It reads voice
25
- gateway connection details through `/api/dev-voice/config`, streams browser mic
26
- audio to `WS /ws/voice` as PCM16 mono, appends gateway transcripts/responses to
27
- the chat, and plays returned TTS audio. Exposing
28
- `SMRT_CHAT_DEV_VOICE_GATEWAY_TOKEN` to the browser requires
29
- `SMRT_CHAT_DEV_VOICE_GATEWAY_EXPOSE_TOKEN=true`; keep that local-only.
26
+ Dev-only voice and opt-in local character persistence are documented in
27
+ [agents/dev-workbench.md](agents/dev-workbench.md#voice-and-local-character-persistence).
28
+ These endpoints are loopback-only demos; production hosts supply authenticated
29
+ principals, tenancy, and authorization to the owning services.
30
30
 
31
31
  ## Models
32
32
 
@@ -122,6 +122,16 @@ reads/saves run under the principal's tenant (`withPrincipalTenant`), refuse a
122
122
  mismatching ambient tenant, and filter by the principal tenant explicitly. Contract and authority model:
123
123
  [`packages/reports/agents/runtime-reports.md`](../reports/agents/runtime-reports.md).
124
124
 
125
+ ## Overview tools (#3727)
126
+
127
+ `createOverviewTools({ host })` (`overview-tools.ts`) returns
128
+ `overviews.describe`, `overviews.apply` (an atomic batch of structured widget
129
+ operations) and `overviews.undo` (single step, keyed by tenant, user and page).
130
+ The host opens a page per `PrincipalRun` (smrt-svelte's
131
+ `createOverviewAssistantSurface`, typed structurally here); validation and
132
+ `canCustomize` are the surface's. Contract:
133
+ [agents/overview-tools.md](agents/overview-tools.md).
134
+
125
135
  ## Conversational Harness (L3, #1891)
126
136
 
127
137
  The `AgentSession` runtime depends on personas, agents, and users. Keep those
@@ -138,14 +148,15 @@ Voice is an input mode for the existing persona chat harness, not a separate cha
138
148
 
139
149
  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()`.
140
150
 
141
- ## Token Streaming (SSE, #1936)
151
+ ## Token streaming
142
152
 
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).
153
+ `chat-stream.ts` serves browser-safe SSE through `./client`. The host's
154
+ `authorize(request, body)` is the sole trust boundary: resolve identity, tenant,
155
+ capabilities, and generation limits server-side; never authorize from request
156
+ session metadata. Persona turns reuse the principal and fail-closed tool gates.
157
+ The engine and browser-safe `SmrtChatBackend` keep custom-tool resolution
158
+ server-side. See [agents/token-streaming.md](agents/token-streaming.md) for the
159
+ event contract, CORS policy, and streaming behavior.
149
160
 
150
161
  ## Streamed assistant turns with browser tools (#2908)
151
162
 
package/README.md CHANGED
@@ -29,6 +29,18 @@ trusting browser data or throwing raw transport errors.
29
29
 
30
30
  ## Usage
31
31
 
32
+ ### Team Chat recipe
33
+
34
+ `ChatRoomsRecipe` declares the `chat.rooms` unit for app navigation and
35
+ generated read-only room, message, membership, thread, and reaction views.
36
+ It places the Chat Rooms entry in the Team section under the Team collaboration
37
+ card, using the shell's `users` icon.
38
+ Hosts can mount its exported `ChatLayout` at `/chat/rooms`; the recipe also
39
+ points demo hosts at the browser-safe static chat fixture from `./playground`.
40
+ Its [user help](src/chat-rooms.recipe.md) describes how to find rooms and
41
+ follow conversations. The package playground previews use static mock fixtures;
42
+ they do not connect to a live chat service.
43
+
32
44
  ### Local dev server
33
45
 
34
46
  Run the chat package workbench directly:
@@ -69,3 +69,72 @@ are non-obvious and cost time to find:
69
69
  programmatic `element.click()` (`event.isTrusted === false`) for `apply`, which
70
70
  is the stage→apply consent split doing its job. Verification must use a real
71
71
  input event, not `evaluate(() => button.click())`.
72
+
73
+ ## Voice and local character persistence
74
+
75
+ The root workbench also has a dev-only voice conversation mode. It reads voice
76
+ gateway connection details through `/api/dev-voice/config`, streams browser mic
77
+ audio to `WS /ws/voice` as PCM16 mono, appends gateway transcripts/responses to
78
+ the chat, and plays returned TTS audio. Exposing
79
+ `SMRT_CHAT_DEV_VOICE_GATEWAY_TOKEN` to the browser requires
80
+ `SMRT_CHAT_DEV_VOICE_GATEWAY_EXPOSE_TOKEN=true`; keep that local-only.
81
+
82
+ ### Local character persistence
83
+
84
+ `/api/dev-character-persistence` is an opt-in loopback-only demo bridge for
85
+ the photographic character workbench. It is disabled unless
86
+ `SMRT_CHAT_DEV_CHARACTER_PERSISTENCE=true`; it accepts neither actor, tenant,
87
+ nor profile ids from the browser. Its server configuration names one
88
+ pre-provisioned profile and tenant plus a temporary SQLite database and asset
89
+ directory, all under the operating system temporary directory. First run
90
+ `DATABASE_URL=<the same SQLite path> pnpm smrt db:migrate`, then run
91
+ `pnpm exec tsx src/scripts/provision-dev-character-persistence.ts` with the
92
+ `SMRT_CHAT_DEV_CHARACTER_*` variables set. The route never creates schema.
93
+ Use fresh UUIDs for `SMRT_CHAT_DEV_CHARACTER_PROFILE_ID` and
94
+ `SMRT_CHAT_DEV_CHARACTER_TENANT_ID`; keep the database and asset directory in
95
+ the OS temporary directory, for example `/tmp/smrt-character-setup/` on Linux
96
+ or the path reported by `node -p "require('node:os').tmpdir()"` on macOS.
97
+
98
+ This is not production authentication. A production host must provide its
99
+ authenticated principal, tenant, and authorization policy to
100
+ `PhotoCutoutProfileStore`; it must not copy this fixed local identity pattern.
101
+
102
+ ### Local helper control panel
103
+
104
+ `/api/dev-helper` uses the same loopback-only identity and durable Profile
105
+ metadata store. Provisioning also creates its application-scoped metafield;
106
+ the route never creates schema or accepts actor, tenant, policy, provider, or
107
+ asset URL fields from the browser. It resolves the current gallery, allowed
108
+ voice, and helper policy on every load/save/reset. Its whole-value persistence
109
+ semantic is last-successful-write-wins, so a production host with concurrent
110
+ editors must serialize writes per profile/application or supply a stronger
111
+ adapter. The speech route resolves the saved offered voice server-side; clients
112
+ submit only text. Helper preference loading never starts audio or a microphone.
113
+
114
+ ### Character conversation lifecycle
115
+
116
+ The Character conversation tab refreshes the persisted rig each time it becomes
117
+ active, retaining its AssistantDock controller and history. Leaving the tab stops
118
+ microphone input and reply playback; listening mode requires an explicit restart.
119
+ An outstanding turn keeps both the listening input and full dock composer busy
120
+ across mode changes and restarts until it settles. Both inputs share the retained
121
+ controller’s pending-send state; microphone Stop remains reachable. Polling
122
+ retains active sends until their transport completes, even when older history
123
+ contains the same message text.
124
+ Late persistence loads and cancelled speech responses cannot replace the current
125
+ rig or audio. Spoken captions begin only when SDK playback actually starts.
126
+ Listening mode also keeps the newest assistant reply in a dedicated reply
127
+ region, so a muted or failed spoken reply is still readable while history stays
128
+ hidden. A speech failure is shown beside the conversation controls; disabling
129
+ spoken replies aborts its outstanding request and playback. While the tab is
130
+ inactive, its fixed dock is hidden and polling is paused.
131
+
132
+ The development workbench links the SDK speech and animation packages from
133
+ sibling worktrees. Keep those worktrees available until their released package
134
+ versions replace the links; a fresh browser or Vite load cannot resolve a
135
+ linked package whose sibling checkout is absent.
136
+
137
+ The development transport sends at most 24 context messages within a 16 KiB UTF-8
138
+ JSON body, dropping oldest context first. Failed turns are not committed to its
139
+ history, so retrying does not duplicate them. Draft proposals use one shared
140
+ 200-character limit in the model tool, route, preview, and execution boundary.
@@ -0,0 +1,65 @@
1
+ # Helper preferences server contract
2
+
3
+ `HelperPreferencesService` is presentation policy, never an identity, persona,
4
+ tool, or authorization authority. A host creates `HelperContext` from its
5
+ authenticated request and supplies `authorize`, `resolvePolicy`, registered
6
+ `styleIds`, and `validateOffering`. Browser payloads never supply those values.
7
+
8
+ Use `createHelperProfilePreferenceStore({ db, applicationId, metafieldSlug })` only with an
9
+ already provisioned, application-scoped ProfileMetafield. The adapter checks
10
+ the context profile and tenant on every operation, fails duplicate rows, and
11
+ persists one complete versioned preference value. `clear()` writes the explicit
12
+ `{ version: 1, cleared: true }` marker. Its durable guarantee is complete
13
+ last-successful-write-wins values **only when the host serializes writes for a
14
+ profile/application across instances**; it does not offer distributed locking
15
+ or compare-and-swap. Hosts requiring either must supply their own store adapter.
16
+
17
+ The service re-resolves policy and validates saved values on each load, save,
18
+ and reset. Invalid old values produce a recoverable snapshot without silently
19
+ overwriting metadata. Owner-assigned policies ignore personal state and deny
20
+ save/reset. An offering validator must re-check saved asset ownership, tenant,
21
+ relationship, and payload before it can become effective.
22
+
23
+ When an editable policy has no valid effective helper but can preserve its
24
+ locked baseline fields while selecting an offered replacement, the snapshot
25
+ contains `recoveryDraft`. It is an edit candidate only: UI must submit it to
26
+ `save()` and wait for the returned snapshot before applying it.
27
+
28
+ ## Browser DTO import
29
+
30
+ Browser clients import the DTO, parser, and field list from
31
+ `@happyvertical/smrt-chat/helper-preferences`. This public subpath deliberately
32
+ contains no Profile/SQL adapter or `HelperPreferencesService`; authenticated
33
+ servers import those server APIs from the package root.
34
+
35
+ ```ts
36
+ import {
37
+ helperPreferenceFields,
38
+ parseHelperPreferences,
39
+ type HelperClient,
40
+ type HelperPreferences,
41
+ } from '@happyvertical/smrt-chat/helper-preferences';
42
+ ```
43
+
44
+ ## Svelte helper controls
45
+
46
+ `HelperControlPanel` distinguishes a local draft from the applied server
47
+ snapshot. Selecting a gallery item, changing a control, or completing photo
48
+ setup creates an unsaved draft; only the snapshot returned by `save()` is
49
+ applied through `onchanged`. Photo setup returns to the settings panel, selects
50
+ the newly returned offering in the draft, and restores focus to its gallery
51
+ choice so the next action is explicit: **Save changes**.
52
+
53
+ Gallery choices use roving radio keyboard behavior (Arrow keys, Home, End).
54
+ Hosts should provide distinct safe offering labels, such as a user-provided
55
+ name with a saved timestamp, rather than raw duplicate filenames.
56
+
57
+ `FloatingAssistant` accepts an optional bottom-corner placement. Conversation
58
+ hosts pass the effective saved placement to keep the dock with its selected
59
+ helper. Opening listening mode may reveal the existing assistant surface, but
60
+ must never start microphone capture or audio playback; those remain explicit
61
+ user actions.
62
+
63
+ `FloatingAssistant.hideIdleControls` is opt-in and defaults to false for existing hosts.
64
+ The character listening workbench enables it to hide idle controls chrome while
65
+ keeping active runs, errors and confirmation surfaces reachable.
@@ -0,0 +1,172 @@
1
+ # Overview assistant tools (#3727 phase 4)
2
+
3
+ `createOverviewTools({ host, undoStore?, audit?, maxOperations? })`
4
+ (`src/overview-tools.ts`, package index) returns three `PrincipalTool`s for the
5
+ persona / assistant-route `extraTools` seam. They let the assistant add and
6
+ configure widgets on a customizable overview page ("add a chart of overdue
7
+ invoices") with structured operations, and undo its last change. The overview
8
+ model, widget registry and option schemas are smrt-svelte's
9
+ ([overview-surfaces.md](../../smrt-svelte/agents/overview-surfaces.md)).
10
+
11
+ | Slug (function name) | Arguments | Result |
12
+ |---|---|---|
13
+ | `overviews.describe` (`overviews-describe`) | `{ page? }` | `{ page, overview }`: allowed widget types with option fields (type, required, default, range, `choices`, page `models`), span range, the current widgets in order, `canCustomize`. Without `page`: `{ pages }` from `host.pages` |
14
+ | `overviews.apply` (`overviews-apply`) | `{ page, operations }` | `{ page, changed, applied: [{ index, op, id }], undoToken, overview }`; `changed: false` and `undoToken: null` for a batch that changes nothing |
15
+ | `overviews.undo` (`overviews-undo`) | `{ page, undoToken }` | `{ page, undone: true, overview }` |
16
+
17
+ Operations (1 to `maxOperations`, default 20): `add { type, span?, options?,
18
+ index? }`, `configure { id, options }` (merged onto the current options; `null`
19
+ clears a key to its default), `move { id, index }`, `resize { id, span }`,
20
+ `remove { id }`. Option values are JSON primitives over the widget's closed
21
+ field vocabulary; there is no query, filter expression, URL or SQL anywhere.
22
+
23
+ ## Authority
24
+
25
+ - **The host decides, per principal.** `host.open(run, pageId)` returns the
26
+ page for that `PrincipalRun` or `null` (unknown and not offered answer the
27
+ same 404). Build it with smrt-svelte's `createOverviewAssistantSurface`
28
+ (`./overview/server`) from the page's `OverviewDefinition`, the
29
+ principal's own stored override, `canCustomize` from the principal's role,
30
+ and `persist`. Never derive any of these from model input except the page id.
31
+ - **Tenant.** Like the runtime-report tools, with tenancy on a run without
32
+ `run.context.tenantId` is refused (403 `tenant_required`), `host.open` and
33
+ every surface call (including `persist`) run under the principal's tenant
34
+ (entered when no tenant context is active), and an ambient tenant that
35
+ differs from the principal's is refused (`DataSurfaceDeniedError`) unless
36
+ the caller is in a system-context or super-admin path. The host scopes its
37
+ reads and writes by `run.context.tenantId`.
38
+ - **Validation is the overview model's.** The surface plans each batch with
39
+ `planOverviewOperations` and checks the result with `checkOverviewOverride`
40
+ (allowed set and `allowedIn`, page-confined `models`, option schemas, span
41
+ ranges, the widget cap), so the assistant can only store an override the
42
+ page's own save endpoint accepts. A batch is atomic.
43
+ - **Errors** are `OverviewToolError` with `status`, `code`, `publicMessage`,
44
+ `issues`. `runToolLoop` classifies 404/409/422 as `invalid_request` (the model
45
+ sees `publicMessage`, which lists every issue as `#<n> <op>: <message>`, and is
46
+ told to fix and retry) and 403 as `not_permitted`. Codes: `invalid_page`,
47
+ `invalid_operations` (422), `unknown_page` (404), `tenant_required` (403),
48
+ `not_allowed` (403,
49
+ `canCustomize` false, on apply and on undo), `conflict` (409, apply lost a
50
+ race with another save), `nothing_to_undo`, `changed_since`,
51
+ `cannot_restore` (409).
52
+ - **Writes are conditional.** `surface.persist(override)` answers
53
+ `OverviewToolPersistResult` (`{ ok: true }` or `{ ok: false, reason:
54
+ 'conflict' | 'not_allowed' | 'invalid', issues? }`) and must write only if
55
+ the stored row is still the revision the surface was opened on. A save that
56
+ lands between `host.open` and the write is a `conflict`: apply answers 409
57
+ `conflict` (describe again, rebuild the batch), undo answers 409
58
+ `changed_since` and keeps its entry. smrt-svelte's surface takes the loaded
59
+ `revision` and passes `{ revision }` to its `persist` option, so the
60
+ revision-guarded `save` of `@happyvertical/smrt-preferences`'
61
+ `createOverviewStore` plugs in directly (see below).
62
+ - **Undo** is single-step. Apply stores `{ token, before, after }` under
63
+ `[tenantId, userId, pageId]` of the calling run; undo looks it up under the
64
+ calling run only, so another principal's (or tenant's) token is simply not
65
+ found. It refuses when the stored override is no longer `after` (someone
66
+ changed the page since), re-checks `before` with `check`, persists it
67
+ conditionally, and deletes the entry only after that write succeeded.
68
+ Removal is compare-and-delete: `OverviewUndoStore.delete(key, token)` is a
69
+ no-op unless the stored entry still carries that token, so an undo that
70
+ finishes after a newer batch stored its entry never removes that entry.
71
+ Storing is a conditional set: an entry's `createdAt` is stamped right after
72
+ its batch's conditional write succeeded (strictly increasing per process),
73
+ and `set` is a no-op when the stored entry is newer, so an older apply whose
74
+ entry write lands late never replaces a newer batch's entry. Ordering by
75
+ write-success time rather than serializing per key works across replicas
76
+ sharing one store (a lock would only cover one process), and the revision
77
+ guard already serializes the writes themselves; revisions are opaque here,
78
+ so they cannot order entries. Across replicas the stamps rely on clocks
79
+ agreeing to within the time between two applies by the same person on the
80
+ same page.
81
+ - **The undo store must outlive a request and a turn.** Without `undoStore`
82
+ the tools use `defaultOverviewUndoStore()`, one in-memory store per process
83
+ (30-minute TTL, 1000 keys), so tools built per turn
84
+ (`extraTools: (context) => createOverviewTools(...)`) still undo an earlier
85
+ turn's batch. Never pass a store created inside the per-turn factory. A
86
+ multi-replica host passes one shared `OverviewUndoStore`, created once,
87
+ whose `set` (newer `createdAt` wins) and `delete(key, token)` are atomic
88
+ conditional operations.
89
+ - **After a successful write the call never fails.** The undo entry is written
90
+ after the write; if the store throws, apply still answers `changed: true`
91
+ with `undoToken: null` and a `note` not to apply the batch again (a lost entry
92
+ only costs the Undo, while an entry written first could later restore over a
93
+ value the batch never wrote). Audit is best effort; failures go to
94
+ `onError` (default `console.warn`).
95
+
96
+ ## Wiring to persistence (phase 3)
97
+
98
+ The surface is the only seam; this package has no table and no dependency on
99
+ smrt-svelte or `@happyvertical/smrt-preferences` (smrt-svelte depends on chat
100
+ for tests, so the types are structural). Phase 3 stores overview layouts in
101
+ `@happyvertical/smrt-preferences` (table `_smrt_ui_preferences`, preference
102
+ kind `overview`); its overview layer is `createOverviewStore({ db })`, built
103
+ over the generic `createPreferenceStore({ db })` (pass `preferences` to share
104
+ one). Every store call acts as the ambient principal, which is the run's
105
+ principal inside the tool loop, under the tenant the tools enter.
106
+
107
+ ```ts
108
+ import { createOverviewTools } from '@happyvertical/smrt-chat';
109
+ import {
110
+ createOverviewStore,
111
+ withTenantDefaults,
112
+ } from '@happyvertical/smrt-preferences';
113
+ import { createOverviewAssistantSurface } from '@happyvertical/smrt-svelte/overview/server';
114
+
115
+ // Module scope: created once, shared by every turn and request.
116
+ const overviewUndo = createSharedOverviewUndoStore(); // or omit for the process default
117
+
118
+ const overviewTools = (db: DatabaseInterface /* per turn */) => {
119
+ const store = createOverviewStore({ db });
120
+ return createOverviewTools({
121
+ undoStore: overviewUndo,
122
+ host: {
123
+ async open(run, pageId) {
124
+ const definition = overviewDefinitions.get(pageId);
125
+ if (!definition) return null;
126
+ const state = await store.load(definition, registry);
127
+ if (!state.user) return null;
128
+ return createOverviewAssistantSurface({
129
+ // The user tier is a delta over the tenant default.
130
+ definition: withTenantDefaults(definition, state.tenant.document),
131
+ registry,
132
+ override: state.user.override,
133
+ revision: state.user.revision,
134
+ canCustomize: state.canCustomize.user,
135
+ // The page definition: the store merges the tenant tier itself.
136
+ persist: (override, { revision }) =>
137
+ store.save(definition, registry, { scope: 'user', override, revision }),
138
+ });
139
+ },
140
+ },
141
+ });
142
+ };
143
+ ```
144
+
145
+ `createSharedOverviewUndoStore` stands for the host's own shared store (any
146
+ `OverviewUndoStore`). How the store's answer maps onto the tools:
147
+
148
+ | `store.save(definition, registry, { scope, override, revision })` (`OverviewSaveResult`) | Surface / tool |
149
+ |---|---|
150
+ | `{ ok: true, override, revision }` | the surface follows the new `revision`; apply answers `changed: true` |
151
+ | `{ ok: false, reason: 'conflict' }` (the loaded `revision` is stale, or a first write raced another; nothing is overwritten) | apply: 409 `conflict`; undo: 409 `changed_since`, entry kept |
152
+ | `{ ok: false, reason: 'not_allowed' }` (no `overviews.personalize` / `overviews.customize`) | 403 `not_allowed` |
153
+ | `{ ok: false, reason: 'invalid', issues }` (`checkOverviewOverride` refused it) | 422 `invalid_operations` |
154
+
155
+ `revision` is `null` when the principal had no row yet; the first write then
156
+ creates it, and a concurrent first write answers `conflict`. Add the three
157
+ slugs to the persona's / route's `allowedTools`.
158
+
159
+ ## In the browser
160
+
161
+ When the overview is mounted on the page the dock runs on, use the browser
162
+ tools instead: smrt-svelte's `createOverviewAssistant(controller)` +
163
+ `useOverviewAssistantTools` register `overview_describe`, `overview_apply` and
164
+ `overview_undo` as page tools (add `overview_*` to the route's
165
+ `clientToolAllowList`). A batch lands through `controller.restore`, so the grid
166
+ updates live and the host's `onchange` persists it, and `OverviewAssistantUndo`
167
+ (or a toast) offers Undo. Same operations, same validation, same error shape
168
+ (returned as JSON, not thrown).
169
+
170
+ Tests: `src/overview-tools.test.ts` (contract against a structural surface) and
171
+ `packages/smrt-svelte/src/components/overview/__tests__/assistant-tools.integration.test.ts`
172
+ (these tools over the real model).
@@ -1,7 +1,8 @@
1
1
  # Token Streaming (SSE, #1936)
2
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.
3
+ Reference linked from [../AGENTS.md](../AGENTS.md). This contract was moved from
4
+ `packages/chat/AGENTS.md` to keep the instruction chain under the 32 KB cap
5
+ (`pnpm check:agents-chain`); AGENTS.md retains the cap-oriented summary.
5
6
 
6
7
  `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
 
@@ -1,7 +1,7 @@
1
1
  import { ObjectRegistry, SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, isPostgresDatabase, smrt } from "@happyvertical/smrt-core";
2
2
  import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
3
- import { createHash } from "node:crypto";
4
3
  import { ProfileCollection, resolveAgentProfileId } from "@happyvertical/smrt-profiles";
4
+ import { createHash } from "node:crypto";
5
5
  //#region \0rolldown/runtime.js
6
6
  var __defProp$7 = Object.defineProperty;
7
7
  var __exportAll = (all, no_symbols) => {
@@ -2225,4 +2225,4 @@ function sendAgentReply(service, params) {
2225
2225
  //#endregion
2226
2226
  export { sendAgentReply as a, ChatThread as c, ChatParticipant as d, ChatMessage as f, clientRequestMessageId as i, ChatRoom as l, ChatService as n, VoiceSessionCollection as o, AgentSession as p, ChatService_exports as r, VoiceSession as s, ChatClientRequestConflictError as t, ChatReaction as u };
2227
2227
 
2228
- //# sourceMappingURL=ChatService-C9UxuJaw.js.map
2228
+ //# sourceMappingURL=ChatService-CFaaDHyU.js.map