@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.
- package/AGENTS.md +25 -14
- package/README.md +12 -0
- package/agents/dev-workbench.md +69 -0
- package/agents/helper-preferences.md +65 -0
- package/agents/overview-tools.md +172 -0
- package/agents/token-streaming.md +3 -2
- package/dist/chunks/{ChatService-C9UxuJaw.js → ChatService-CFaaDHyU.js} +2 -2
- package/dist/chunks/{ChatService-C9UxuJaw.js.map → ChatService-CFaaDHyU.js.map} +1 -1
- package/dist/chunks/chat-stream-n-7AqsgF.js +2027 -0
- package/dist/chunks/chat-stream-n-7AqsgF.js.map +1 -0
- package/dist/dev-assistant-transport.d.ts +5 -0
- package/dist/dev-assistant-transport.d.ts.map +1 -0
- package/dist/dev-character-persistence-client.d.ts +24 -0
- package/dist/dev-character-persistence-client.d.ts.map +1 -0
- package/dist/dev-helper-client.d.ts +4 -0
- package/dist/dev-helper-client.d.ts.map +1 -0
- package/dist/dev-helper-server.d.ts +18 -0
- package/dist/dev-helper-server.d.ts.map +1 -0
- package/dist/helper-preferences-service.d.ts +22 -0
- package/dist/helper-preferences-service.d.ts.map +1 -0
- package/dist/helper-preferences.d.ts +84 -0
- package/dist/helper-preferences.d.ts.map +1 -0
- package/dist/helper-preferences.js +32 -0
- package/dist/helper-preferences.js.map +1 -0
- package/dist/helper-profile-store.d.ts +27 -0
- package/dist/helper-profile-store.d.ts.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +683 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/agent-runtime.js +1 -1
- package/dist/manifest.json +122 -3
- package/dist/overview-tools.d.ts +221 -0
- package/dist/overview-tools.d.ts.map +1 -0
- package/dist/playground.d.ts +1 -1
- package/dist/playground.d.ts.map +1 -1
- package/dist/playground.js +99 -88
- package/dist/playground.js.map +1 -1
- package/dist/recipes.d.ts +81 -0
- package/dist/recipes.d.ts.map +1 -1
- package/dist/routes/api/dev-character-conversation/protocol.d.ts +33 -0
- package/dist/routes/api/dev-character-conversation/protocol.d.ts.map +1 -0
- package/dist/routes/api/dev-character-persistence/config.d.ts +24 -0
- package/dist/routes/api/dev-character-persistence/config.d.ts.map +1 -0
- package/dist/scripts/provision-dev-character-persistence.d.ts +3 -0
- package/dist/scripts/provision-dev-character-persistence.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +144 -11
- package/dist/svelte/components/assistant/AssistantDock.svelte +58 -2
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +9 -0
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -1
- package/dist/svelte/components/assistant/FloatingAssistant.svelte +309 -0
- package/dist/svelte/components/assistant/FloatingAssistant.svelte.d.ts +27 -0
- package/dist/svelte/components/assistant/FloatingAssistant.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/captions/CaptionOverlay.svelte +105 -0
- package/dist/svelte/components/assistant/captions/CaptionOverlay.svelte.d.ts +11 -0
- package/dist/svelte/components/assistant/captions/CaptionOverlay.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/captions/HeardCaptions.svelte +65 -0
- package/dist/svelte/components/assistant/captions/HeardCaptions.svelte.d.ts +19 -0
- package/dist/svelte/components/assistant/captions/HeardCaptions.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/captions/SpokenCaptions.svelte +65 -0
- package/dist/svelte/components/assistant/captions/SpokenCaptions.svelte.d.ts +21 -0
- package/dist/svelte/components/assistant/captions/SpokenCaptions.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/captions/caption-overlay-context.d.ts +3 -0
- package/dist/svelte/components/assistant/captions/caption-overlay-context.d.ts.map +1 -0
- package/dist/svelte/components/assistant/captions/caption-overlay-context.js +2 -0
- package/dist/svelte/components/assistant/captions/caption-state.svelte.d.ts +71 -0
- package/dist/svelte/components/assistant/captions/caption-state.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/captions/caption-state.svelte.js +225 -0
- package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts.map +1 -1
- package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.js +3 -1
- package/dist/svelte/components/helper/HelperControlPanel.svelte +401 -0
- package/dist/svelte/components/helper/HelperControlPanel.svelte.d.ts +23 -0
- package/dist/svelte/components/helper/HelperControlPanel.svelte.d.ts.map +1 -0
- package/dist/svelte/components/helper/registry.d.ts +53 -0
- package/dist/svelte/components/helper/registry.d.ts.map +1 -0
- package/dist/svelte/components/helper/registry.js +100 -0
- package/dist/svelte/i18n.d.ts +14 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +17 -0
- package/dist/svelte/i18n.messages.d.ts +32 -0
- package/dist/svelte/i18n.messages.d.ts.map +1 -1
- package/dist/svelte/i18n.messages.js +33 -0
- package/dist/svelte/index.d.ts +7 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +15 -5
- package/dist/svelte/playground.d.ts +94 -0
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +16 -5
- package/dist/sveltekit.js +2 -2
- package/package.json +27 -17
- package/dist/chunks/chat-stream-BBk_5_Ph.js +0 -2027
- package/dist/chunks/chat-stream-BBk_5_Ph.js.map +0 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface DevCharacterPersistenceConfig {
|
|
2
|
+
databaseUrl: string;
|
|
3
|
+
assetDirectory: string;
|
|
4
|
+
profileId: string;
|
|
5
|
+
tenantId: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the deliberately opt-in local workbench store. This is not an
|
|
9
|
+
* authentication mechanism: production hosts must supply their own principal
|
|
10
|
+
* and authorization policy to PhotoCutoutProfileStore.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveDevCharacterPersistenceConfig(): DevCharacterPersistenceConfig | null;
|
|
13
|
+
export declare function isLoopbackAddress(address: string): boolean;
|
|
14
|
+
export declare function hasSameOrigin(request: Request): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The URL and Host header identify the server name, not the caller. Use
|
|
17
|
+
* SvelteKit's trusted connection address and fail closed when it is absent.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isLocalDevCharacterRequest({ dev, request, getClientAddress, }: {
|
|
20
|
+
dev: boolean;
|
|
21
|
+
request: Request;
|
|
22
|
+
getClientAddress: () => string;
|
|
23
|
+
}): boolean;
|
|
24
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/routes/api/dev-character-persistence/config.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,6BAA6B;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAqBD;;;;GAIG;AACH,wBAAgB,oCAAoC,IAAI,6BAA6B,GAAG,IAAI,CAiB3F;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAM1D;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAGvD;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,GAAG,EACH,OAAO,EACP,gBAAgB,GACjB,EAAE;IACD,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,MAAM,MAAM,CAAC;CAChC,GAAG,OAAO,CAOV"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provision-dev-character-persistence.d.ts","sourceRoot":"","sources":["../../src/scripts/provision-dev-character-persistence.ts"],"names":[],"mappings":"AAcA,+EAA+E;AAC/E,eAAO,MAAM,gCAAgC,qCACT,CAAC"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-chat",
|
|
6
|
-
"packageVersion": "0.55.
|
|
6
|
+
"packageVersion": "0.55.11",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
13
|
-
"moduleDoc:agents/dev-workbench.md": "
|
|
14
|
-
"moduleDoc:agents/
|
|
10
|
+
"manifest": "571c60cfc1fe30f0c22f0b8920457b084ae8ec6197178a30b3d4786e32b8a1c1",
|
|
11
|
+
"packageJson": "152a4a1156462cc6054c91793f6d2c8f58e992838394b071f3d005b16439abe3",
|
|
12
|
+
"agents": "5dd4f1b94ec589ca363a11f204e50f778d92166cd7d81a62990da02e1405df30",
|
|
13
|
+
"moduleDoc:agents/dev-workbench.md": "f07a428519e8cca4a46371da0a3b33b9c8d3102f1d9056e4c8efba3c0b702048",
|
|
14
|
+
"moduleDoc:agents/overview-tools.md": "752a4afb6b57a2e9224f3c65a98b0deb23cda87371f01ac0566f0887e9bb12da",
|
|
15
|
+
"moduleDoc:agents/token-streaming.md": "e92a28a8702d111a3751a86b8cdff30cb5146cc115fed86c227402bef1c683fd",
|
|
15
16
|
"moduleDoc:agents/recipes.md": "89c2c86e4e26cea9ba10d917f8ad71c23a71d4fd8eaacf638a6cd252e2376c07",
|
|
16
17
|
"agentSurface:src/routes/chat-dev.intents.ts": "ff8f7c19c7eb6e841a08f03c97773c53092ed208004666bfed372bd1ad456b35"
|
|
17
18
|
},
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
"./client",
|
|
22
23
|
"./data-surface",
|
|
23
24
|
"./data-surface-bridge",
|
|
25
|
+
"./helper-preferences",
|
|
24
26
|
"./internal/agent-runtime",
|
|
25
27
|
"./manifest",
|
|
26
28
|
"./manifest.json",
|
|
@@ -30,8 +32,12 @@
|
|
|
30
32
|
"./ui"
|
|
31
33
|
],
|
|
32
34
|
"dependencies": {
|
|
35
|
+
"@happyvertical/animation": "0.5.0",
|
|
33
36
|
"@happyvertical/ai": "catalog:",
|
|
37
|
+
"@happyvertical/speech": "catalog:",
|
|
38
|
+
"@happyvertical/smrt-images": "workspace:*",
|
|
34
39
|
"@happyvertical/smrt-agents": "workspace:*",
|
|
40
|
+
"@happyvertical/smrt-assets": "workspace:*",
|
|
35
41
|
"@happyvertical/smrt-config": "workspace:*",
|
|
36
42
|
"@happyvertical/smrt-core": "workspace:*",
|
|
37
43
|
"@happyvertical/smrt-personas": "workspace:*",
|
|
@@ -42,6 +48,8 @@
|
|
|
42
48
|
"@happyvertical/smrt-ui": "workspace:*",
|
|
43
49
|
"@happyvertical/smrt-users": "workspace:*",
|
|
44
50
|
"@happyvertical/sql": "catalog:",
|
|
51
|
+
"@happyvertical/images": "catalog:",
|
|
52
|
+
"gsap": "^3.13.0",
|
|
45
53
|
"@happyvertical/smrt-playground": "workspace:*",
|
|
46
54
|
"@happyvertical/smrt-scanner": "workspace:*",
|
|
47
55
|
"@happyvertical/smrt-vitest": "workspace:*",
|
|
@@ -61,8 +69,10 @@
|
|
|
61
69
|
},
|
|
62
70
|
"smrtDependencies": [
|
|
63
71
|
"@happyvertical/smrt-agents",
|
|
72
|
+
"@happyvertical/smrt-assets",
|
|
64
73
|
"@happyvertical/smrt-config",
|
|
65
74
|
"@happyvertical/smrt-core",
|
|
75
|
+
"@happyvertical/smrt-images",
|
|
66
76
|
"@happyvertical/smrt-personas",
|
|
67
77
|
"@happyvertical/smrt-playground",
|
|
68
78
|
"@happyvertical/smrt-profiles",
|
|
@@ -77,6 +87,7 @@
|
|
|
77
87
|
],
|
|
78
88
|
"sdkDependencies": [
|
|
79
89
|
"@happyvertical/ai",
|
|
90
|
+
"@happyvertical/images",
|
|
80
91
|
"@happyvertical/sql"
|
|
81
92
|
],
|
|
82
93
|
"tags": [
|
|
@@ -5141,17 +5152,22 @@
|
|
|
5141
5152
|
"polymorphicAssociations": 0,
|
|
5142
5153
|
"uuidColumns": 78
|
|
5143
5154
|
},
|
|
5144
|
-
"agentDoc": "# @happyvertical/smrt-chat\n\nChat rooms, threads, and agent sessions with app-controlled tool whitelisting.\n\n## Dev Server\n\n`pnpm --dir packages/chat dev` runs a package-local SvelteKit workbench. The root\nroute is an interactive chat surface with a dev-only `/api/dev-chat` endpoint:\nit uses `@happyvertical/ai` when local provider credentials are present and\nfalls back to a deterministic local assistant otherwise. `/api/dev-chat-stream`\nis its SSE companion (#1936) — the same provider/local-fallback resolution wired\nthrough `createChatStreamHandler` in plain mode, so an embedded `SmrtChatBackend`\nclient can exercise token streaming locally. `/previews` hosts the shared\ncomponent playground entries from `src/svelte/playground.ts`.\n\nThe root route also drives the **browser inference path**\n(`@happyvertical/smrt-web/ai`: bitgpu, WebLLM, and route backends composed by\n`createInferencePath`) and an **agent-addressable draft form** (`useViewIntent`\nover `src/routes/chat-dev.intents.ts`, #2588). Their dev-config dependencies\n(static optional-peer imports, `workspace-aliases.js` prefix rules, the theme\nstylesheet import) and the stage→apply tool-call gotchas are in\n[agents/dev-workbench.md](agents/dev-workbench.md).\n\nThe root workbench also has a dev-only voice conversation mode. It reads voice\ngateway connection details through `/api/dev-voice/config`, streams browser mic\naudio to `WS /ws/voice` as PCM16 mono, appends gateway transcripts/responses to\nthe chat, and plays returned TTS audio. Exposing\n`SMRT_CHAT_DEV_VOICE_GATEWAY_TOKEN` to the browser requires\n`SMRT_CHAT_DEV_VOICE_GATEWAY_EXPOSE_TOKEN=true`; keep that local-only.\n\n## Models\n\nAll seven models expose only generated REST/MCP `list`/`get`; structural tests\ncheck the registry for accidental mutating operations. Writes go through\nmembership/owner-checked `ChatService` or the binding-checked voice adapter.\n\n`ChatService` is a closed facade. Its collections and security-sensitive\nhelpers use ES `#private` fields/methods (TypeScript `private` is erased).\nCollection classes are not exported from the package index. The agent-reply\nbridge is a Symbol-keyed static accessible through a non-exported symbol held\nby the module-local `sendAgentReply` function.\n\n- **ChatRoom**: `roomType` (public/private/dm/agent), `status`, `topic`, `maxParticipants`, `lastMessageAt`. Tenant-scoped (required).\n- **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).\n- **ChatParticipant**: `role` (owner/admin/member/viewer), `onlineStatus`, `lastReadMessageId`, `isMuted`. Tenant-scoped (required).\n- **ChatThread**: `rootMessageId`, `isResolved`, `messageCount`. Created via `ChatService.startThread()` (member-checked). Tenant-scoped (required).\n- **ChatReaction**: `messageId`, `profileId`, `emoji`. Added/removed via `ChatService.addReaction()`/`removeReaction()` (member-checked, self-keyed). Tenant-scoped (required).\n- **AgentSession**: `agentId` (application slug, string ref, not FK), `agentProfileId` (nullable `crossPackageRef` to `Profile` — the `bot` profile the agent AUTHORS as, #2995), `allowedTools` (JSON string array), `sessionContext` (JSON, sensitive), `systemPrompt`, limits (`maxTokens`/`maxMessages`/`expiresAt`). Optional tenancy.\n- **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()`.\n\n## ChatService\n\nPublic writes take server-supplied `actorProfileId`; never trust a request's\n`senderProfileId`, `role`, owner id, or participant id as authority.\n\n| Operation | Authorization and behavior |\n|---|---|\n| `sendMessage()` | Room member; authors as actor with `role: 'user'`; no public membership bypass |\n| `createRoom()` | Actor becomes owner |\n| `startThread()` | Member; optional root message bound to room and tenant |\n| `addParticipant()` / `removeParticipant()` | Owner/admin; self-leave allowed |\n| `updateRoom()` | Owner/admin |\n| `addReaction()` / `removeReaction()` | Member; self-keyed |\n| `getOrCreateDM()` | Actor must be a DM participant |\n| `createAgentSession()` | Actor becomes session participant; existing-session lookup tenant-bound; optional `sessionKey` isolates conversation subjects |\n| `updateAgentSessionConfig()` | Owner; mandatory tenant-bound lookup |\n| `sendAgentUserMessage()` | Actor must be session participant; authored as participant |\n\nRead facade: `getAgentSession({agentSessionId, tenantId})`,\n`findActiveAgentSessions({tenantId, agentId, participantProfileId})`, and\n`getThread({threadId, tenantId})` are tenant-bound; consumers still check\nownership/context. `listRoomThreads({roomId, actorProfileId, tenantId})`,\n`getThreadMessages({threadId, actorProfileId, tenantId, limit?})` (chronological),\n`getRoomMessages({roomId, actorProfileId, tenantId})`, and\n`getRoomForMember(roomId, actorProfileId, tenantId)` additionally require\nmembership of the server-supplied actor, not a caller-selected subject.\n\nTrusted agent runtime imports `sendAgentReply(service, params)` only from\n`@happyvertical/smrt-chat/internal/agent-runtime`. It is not a `ChatService`\nmethod or package-index export. It authors as `session.agentProfileId` — the\nagent's resolved `bot` Profile, NEVER the `agentId` slug (#2995) — accepts a\nsame-room/tenant thread, and enforces `allowedTools` fail-closed.\n\nPrivate `#writeMessage` alone can select arbitrary authors/roles or bypass\nmembership. It binds every supplied `threadId`, `agentSessionId`, and\n`replyToMessageId` to the same room **and** tenant. Internal enrollment supports\nautomatically created rooms, sessions, and participants.\n\n## Agent Tool Whitelisting\n\n`allowedTools` is a JSON array controlled by the consuming app. Fail-closed: an empty/unparseable whitelist permits NO tools. The internal `sendAgentReply(service, params)` function enforces the whitelist before emitting any `tool`/`tool_call` message; a caller cannot supply a `senderProfileId`/`role` to post as the agent, and the function is not reachable from the package index.\n\nPrincipal-bound data discovery and reads are available as `PrincipalTool[]` via\n`createDataSurfaceTools()` (re-exported by the chat package). Pass the tools as\n`extraTools` to the persona conversation/tool loop. They use the same\nfail-closed `allowedTools` offer/execution gates; RBAC, tenant, redaction,\nbounded query results, and audit authority remain in the authenticated\n`PrincipalRun` supplied by `@happyvertical/smrt-agents`.\n\n## Runtime report tools (#3711)\n\n`createRuntimeReportTools()` (`runtime-report-tools.ts`) returns four\n`PrincipalTool`s for the persona `extraTools` seam: `reports.runtime.sources`,\n`reports.runtime.define` (`preview` / `apply`), `reports.runtime.list`, and\n`reports.runtime.run`. The model supplies only a declarative\n`RuntimeReportSpec` from `@happyvertical/smrt-reports`; sources are a\nserver-owned allow-list gated by `run.assertOperation(collection, 'read')`, and\nfield policy, `readPermission`, and tenancy are enforced by the compiler against\nthe live run on every compile and every stored-report run. `apply` needs the\n`specHash` of the previewed spec plus an app-owned `confirmation` host that\nresolves only after a human approved that exact spec; with no host the tools are\npropose-only and a model-supplied `confirmed` flag is ignored. Saving also needs\n`create` on the `RuntimeReport` collection, and `list`/`run` need `read`; the\ndefault `reportsCollection` is the `RuntimeReport` class itself, which resolves\nto the catalog slug. A string `collection`/`reportsCollection` is used verbatim\nas a catalog slug (never a class name); tests must run the tools under the real\n`assertOperationPermission` guard so a wrong slug fails. A stored report is\nwritten only by this confirmed `apply`: the model layer refuses every other\ninsert/spec change, and the generic manifest tools (`buildManifestToolCatalog`,\n`invokeManifestTool`) never offer or run a `RuntimeReport` write. Stored-report\nreads/saves run under the principal's tenant (`withPrincipalTenant`), refuse a\nmismatching ambient tenant, and filter by the principal tenant explicitly. Contract and authority model:\n[`packages/reports/agents/runtime-reports.md`](../reports/agents/runtime-reports.md).\n\n## Conversational Harness (L3, #1891)\n\nThe `AgentSession` runtime depends on personas, agents, and users. Keep those\nedges acyclic: none of those packages may depend back on chat.\n\n- **`runToolLoop(options)`** (`tool-loop.ts`) — a bounded `tool_call → observe → respond` loop. Tools are **manifest operations** of installed packages: `buildManifestToolCatalog({ allowedTools })` reads the `PermissionCatalogService` catalog and keeps only the `(collection, action)` entries named in the persona's allow-list (the **offer gate**; absent/empty ⇒ NO tools). The loop runs inside one `executeAsPrincipal` context, and `invokeManifestTool` executes each op **in-process (\"side door\")** against `run.context.database` (the RLS tx when Postgres RLS is on), after re-asserting the fail-closed allow-list (`run.assertToolAllowed`) AND the catalog permission (`run.assertOperation`) — the **execution gate**. Bounded by a max-steps ceiling (`DEFAULT_MAX_STEPS = 8`): on the ceiling it disables tools for one final completion so the turn always terminates with text. A thrown tool error is classified (`classifyToolError`): permission denials and 401/403 are `not_permitted`; a `ValidationError` or 400/404/409/422 is `invalid_request` (observation: actionable message, `publicMessage` first, `code`, fix-and-retry hint); else (incl. 429) `execution_error`.\n- **`runPersonaConversationTurn(options)`** (`persona-conversation.ts`) — binds a conversation to an `AgentPersona`/`ResolvedPersona`: runs as its principal (`runAsUserId`), offers only its `allowedTools`, speaks its instructions (`resolvePersonaInstructions`, layering approved learned directives), and injects its **recalled learning memory** (`personaLearningMemory`, isolated per `memoryScope`) into the system prompt. `bindPersonaToSession()` mirrors the persona's `allowedTools`/instructions onto the `AgentSession` so the chat authoring gate agrees with the loop's offer gate. Authors the reply (and each executed tool) via the internal `sendAgentReply` bridge. Tools run with the run-as user's **live** permissions unless the host passes a `permissions` snapshot (#2978), which `runToolLoop` forwards to `executeAsPrincipal` so a narrowed principal (e.g. a shared station) cannot reach grants — such as `readPermission`-gated data-surface fields — beyond that snapshot, including through `agents.invoke` delegation (the snapshot becomes the worker's permission ceiling).\n- **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.\n- **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.\n\n## Voice Gateway Turns (#1910)\n\nVoice 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.\n\nThe 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()`.\n\n## Token Streaming (SSE, #1936)\n\n`chat-stream.ts` is the SSE seam for embeddable conversational UIs:\n`runChatConversationStream()` (engine), `createChatStreamHandler({ authorize })`\n(Fetch handler; `authorize` is the sole trust boundary), and the browser-safe\n`SmrtChatBackend` (`./client`). Persona-bound streams reuse the harness gates\nunchanged, and `binding.extraTools` is resolved server-side. Full contract:\n[agents/token-streaming.md](agents/token-streaming.md).\n\n## Streamed assistant turns with browser tools (#2908)\n\n`runAssistantTurn()` (`assistant-turn.ts`) is the host-route engine for the\nAssistantDock: one turn over `runToolLoop` with server tools (offer-gated by\n`principal.allowedTools`), browser tools (validated by\n`sanitizeClientToolDeclarations` + a server allow-list), `maxSteps`, `signal`,\nand `onUsage`. A browser tool call SUSPENDS the turn (`stoppedReason:\n'client_tools'`); the transcript waits in an `AssistantContinuationStore`\n(single-use, 15-minute TTL; `createSessionContinuationStore` keeps it in the\nAgentSession's `sessionContext`) and the browser resumes it with results,\nwhich reach the model wrapped `untrusted`. `createAssistantTurnResponse`\nserves the events as SSE; `assistant-turn-events.ts` is the browser-safe wire\ncontract (`./assistant-turn` subpath, also a vite library entry because the\nSvelte dock imports it). The dock decides whether a browser call runs or waits\nfor the user from the page registry, never the server's echo; destructive\nalways waits. Details: [`docs/assistant-dock.md`](../../docs/assistant-dock.md).\n\n## AssistantDock (#2904)\n\n`AssistantDock` (`svelte/components/assistant/`, exported from `./svelte`)\nuses a host `DataSurfaceRegistry`'s mounted descriptors (none: plain chat).\nActions: `normalizeDataSurfaceActionRequest` → `AssistantActionClient`\n(preview/apply over a server `DataSurfaceActionAdapter`, not the\n`data-surface-bridge.ts` channel). Generated `ChatThread`/`ChatMessage`\n`list`/`get` are tenant- not member-scoped; the dock never calls them.\n\n**Server routes (#3368).** `./sveltekit` `mountAssistantRoutes()` serves the\ndock from one `[...path]` route; `createAssistantHttp{Transport,ActionClient}`\n(`./svelte`) are its browser half. Principal from `event.locals` or\n`resolvePrincipal`; threads only from the actor's keyed session room in that\ntenant (else 404). `allowedTools` is fail-closed; without `tools` it offers\nthose manifest ops (an unprovided name is an error, #3414). `ai` defaults to\nthe config `ai` block. Pass `runtime`: a turn never keeps the RLS request tx\n(own `runAsPrincipal` tx). `clientRequestId` makes the row id a UUIDv5\n(`clientRequestMessageId`): the PK is the retry reservation. See\n[docs](../../docs/assistant-dock.md).\n\n## Recipes (#3719)\n\n`src/recipes.ts` declares `chat.assistant` (the AssistantDock as a `header.end`\nshell widget, an `llm` provider, runtime `both`; help in\n`src/assistant.recipe.md`). Keep each recipe class self-contained. Details:\n[agents/recipes.md](agents/recipes.md).\n\n## Gotchas\n\n- **sessionContext, not context**: `context` is reserved for slug scoping. Use `getSessionContext()`/`updateSessionContext()` for agent memory.\n- **Agent rooms auto-created**: `roomType: 'agent'`, `maxParticipants` defaults to 2; the agent is enrolled as a member so its replies pass the membership check. `createAgentSession()` re-enrolls the participant AND the agent on the existing-session path, so legacy sessions created before the agent was enrolled self-heal.\n- **The agent's author is a Profile, not `agentId` (#2995)**: `senderProfileId`/`profileId` are `crossPackageRef`s to `Profile` and therefore uuid columns on PostgreSQL, so the slug fails the cast (`22P02`) and no agent reply can persist. `createAgentSession()` resolves the agent's `bot` Profile through `resolveAgentProfile()` in `@happyvertical/smrt-profiles` (created on first use, tenant-bound) and records it as `AgentSession.agentProfileId`; both enrolment and `sendAgentReply` use that uuid. A consumer whose agent already has an acting identity passes `agentProfileId` to skip resolution — the voice path does this with the persona's `actsAsProfileId`, so persona replies stay attributed to the configured acting profile. Like `actorProfileId`, `agentProfileId` is SERVER-supplied, never request input: it selects the author of every later assistant message. It is validated (`#requireAgentProfile`) to exist and be visible in the tenant and to differ from the acting participant, and it can never re-point a session that already resolved one — only a pre-#2995 session with none is backfilled. **Migration**: `agent_profile_id` is an ordinary additive manifest field, so a consumer adopts it the same way as any other — `smrt db:migrate` generates and applies the column from the manifest, and `db:status --parity` verifies it; the framework never creates application schema at runtime. It is nullable and needs no offline DATA backfill — a session created before #2995 resolves and persists its `agentProfileId`, and re-enrols the agent, on its first agent turn after the upgrade. Two pre-#2995 cohorts: a session whose `agentId` is an ordinary slug never committed an author on PostgreSQL (the cast failed), so there is nothing to repair there, while SQLite/DuckDB may hold `chat_messages`/`chat_participants` rows whose id column holds the slug — readable, but not joinable to `profiles`. A session whose `agentId` is already a Profile uuid (the voice path used to collapse a persona's `actsAsProfileId` into it) DID commit on PostgreSQL; that profile is ADOPTED as the author, tenant-checked and never the participant, so the conversation's author does not change and the two-seat room gains no third identity. Known one-time effect: because `voice.ts` no longer derives `agentId` from `actsAsProfileId`, a keyless `createVoiceChatSession()` for such a persona opens a new session and room instead of reusing the legacy one; the old room and its messages stay intact and readable, and resuming with an explicit `agentSessionId` is unaffected.\n- **Only SQLite-backed tests would miss this class of bug**: uuid columns accept any text on SQLite. `src/__tests__/agent-reply-postgres.test.ts` (`pnpm --filter @happyvertical/smrt-chat test:postgres`) pins the agent-reply path against real PostgreSQL. That suite runs in `postgres-tests.yml` on every same-repository PR and merge group (#2659); keep a cheap SQLite assertion alongside it anyway, since the default lane is SQLite-only (`chat-service.test.ts` asserts the author is the profile uuid, not the slug).\n- **Per-subject sessions need `sessionKey`**: `createAgentSession()` reuses ANY active session for the same `(agentId, participantProfileId, tenantId)`. Callers that open separate conversations per subject (e.g. one content-editor session per content id) MUST pass a stable `sessionKey` (stored in `sessionContext.__sessionKey`, read via `AgentSession.getSessionKey()`); otherwise a session opened for one subject is reused and its context overwritten for another, surfacing the wrong room/threads (S5 #1392). A keyed create never reuses a keyless/legacy session.\n- **Session expiry**: check `isActive()` before allowing messages (expiresAt or limit-based)\n- **DM identity**: derived from the deterministic per-tenant `canonicalDmRoomId()` and the authoritative `chat_participants` join, not client metadata; concurrent creates upsert onto one row.\n- **Tenant-bound lookups**: membership/session/DM lookups REQUIRE `tenantId` and always bind it into the WHERE clause (`findActiveMembership`/`isActiveMember`/`findActiveSession` take a required `tenantId`; AgentSession's `null` tenant is an explicit bound scope, not \"any tenant\") so they can never resolve a row from another tenant.\n",
|
|
5155
|
+
"agentDoc": "# @happyvertical/smrt-chat\n\nChat rooms, threads, and agent sessions with app-controlled tool whitelisting.\n\n## Dev Server\n\n`pnpm --dir packages/chat dev` runs a package-local SvelteKit workbench. The root\nroute separates **Character setup** and **Chat dev** into tabs; switching tabs\nretains the current character rig and chat state. The chat tab is an interactive\nsurface with a dev-only `/api/dev-chat` endpoint:\nit uses `@happyvertical/ai` when local provider credentials are present and\nfalls back to a deterministic local assistant otherwise. `/api/dev-chat-stream`\nis its SSE companion (#1936) — the same provider/local-fallback resolution wired\nthrough `createChatStreamHandler` in plain mode, so an embedded `SmrtChatBackend`\nclient can exercise token streaming locally. `/previews` hosts the shared\ncomponent playground entries from `src/svelte/playground.ts`.\n\nThe root route also drives the **browser inference path**\n(`@happyvertical/smrt-web/ai`: bitgpu, WebLLM, and route backends composed by\n`createInferencePath`) and an **agent-addressable draft form** (`useViewIntent`\nover `src/routes/chat-dev.intents.ts`, #2588). Their dev-config dependencies\n(static optional-peer imports, `workspace-aliases.js` prefix rules, the theme\nstylesheet import) and the stage→apply tool-call gotchas are in\n[agents/dev-workbench.md](agents/dev-workbench.md).\n\nDev-only voice and opt-in local character persistence are documented in\n[agents/dev-workbench.md](agents/dev-workbench.md#voice-and-local-character-persistence).\nThese endpoints are loopback-only demos; production hosts supply authenticated\nprincipals, tenancy, and authorization to the owning services.\n\n## Models\n\nAll seven models expose only generated REST/MCP `list`/`get`; structural tests\ncheck the registry for accidental mutating operations. Writes go through\nmembership/owner-checked `ChatService` or the binding-checked voice adapter.\n\n`ChatService` is a closed facade. Its collections and security-sensitive\nhelpers use ES `#private` fields/methods (TypeScript `private` is erased).\nCollection classes are not exported from the package index. The agent-reply\nbridge is a Symbol-keyed static accessible through a non-exported symbol held\nby the module-local `sendAgentReply` function.\n\n- **ChatRoom**: `roomType` (public/private/dm/agent), `status`, `topic`, `maxParticipants`, `lastMessageAt`. Tenant-scoped (required).\n- **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).\n- **ChatParticipant**: `role` (owner/admin/member/viewer), `onlineStatus`, `lastReadMessageId`, `isMuted`. Tenant-scoped (required).\n- **ChatThread**: `rootMessageId`, `isResolved`, `messageCount`. Created via `ChatService.startThread()` (member-checked). Tenant-scoped (required).\n- **ChatReaction**: `messageId`, `profileId`, `emoji`. Added/removed via `ChatService.addReaction()`/`removeReaction()` (member-checked, self-keyed). Tenant-scoped (required).\n- **AgentSession**: `agentId` (application slug, string ref, not FK), `agentProfileId` (nullable `crossPackageRef` to `Profile` — the `bot` profile the agent AUTHORS as, #2995), `allowedTools` (JSON string array), `sessionContext` (JSON, sensitive), `systemPrompt`, limits (`maxTokens`/`maxMessages`/`expiresAt`). Optional tenancy.\n- **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()`.\n\n## ChatService\n\nPublic writes take server-supplied `actorProfileId`; never trust a request's\n`senderProfileId`, `role`, owner id, or participant id as authority.\n\n| Operation | Authorization and behavior |\n|---|---|\n| `sendMessage()` | Room member; authors as actor with `role: 'user'`; no public membership bypass |\n| `createRoom()` | Actor becomes owner |\n| `startThread()` | Member; optional root message bound to room and tenant |\n| `addParticipant()` / `removeParticipant()` | Owner/admin; self-leave allowed |\n| `updateRoom()` | Owner/admin |\n| `addReaction()` / `removeReaction()` | Member; self-keyed |\n| `getOrCreateDM()` | Actor must be a DM participant |\n| `createAgentSession()` | Actor becomes session participant; existing-session lookup tenant-bound; optional `sessionKey` isolates conversation subjects |\n| `updateAgentSessionConfig()` | Owner; mandatory tenant-bound lookup |\n| `sendAgentUserMessage()` | Actor must be session participant; authored as participant |\n\nRead facade: `getAgentSession({agentSessionId, tenantId})`,\n`findActiveAgentSessions({tenantId, agentId, participantProfileId})`, and\n`getThread({threadId, tenantId})` are tenant-bound; consumers still check\nownership/context. `listRoomThreads({roomId, actorProfileId, tenantId})`,\n`getThreadMessages({threadId, actorProfileId, tenantId, limit?})` (chronological),\n`getRoomMessages({roomId, actorProfileId, tenantId})`, and\n`getRoomForMember(roomId, actorProfileId, tenantId)` additionally require\nmembership of the server-supplied actor, not a caller-selected subject.\n\nTrusted agent runtime imports `sendAgentReply(service, params)` only from\n`@happyvertical/smrt-chat/internal/agent-runtime`. It is not a `ChatService`\nmethod or package-index export. It authors as `session.agentProfileId` — the\nagent's resolved `bot` Profile, NEVER the `agentId` slug (#2995) — accepts a\nsame-room/tenant thread, and enforces `allowedTools` fail-closed.\n\nPrivate `#writeMessage` alone can select arbitrary authors/roles or bypass\nmembership. It binds every supplied `threadId`, `agentSessionId`, and\n`replyToMessageId` to the same room **and** tenant. Internal enrollment supports\nautomatically created rooms, sessions, and participants.\n\n## Agent Tool Whitelisting\n\n`allowedTools` is a JSON array controlled by the consuming app. Fail-closed: an empty/unparseable whitelist permits NO tools. The internal `sendAgentReply(service, params)` function enforces the whitelist before emitting any `tool`/`tool_call` message; a caller cannot supply a `senderProfileId`/`role` to post as the agent, and the function is not reachable from the package index.\n\nPrincipal-bound data discovery and reads are available as `PrincipalTool[]` via\n`createDataSurfaceTools()` (re-exported by the chat package). Pass the tools as\n`extraTools` to the persona conversation/tool loop. They use the same\nfail-closed `allowedTools` offer/execution gates; RBAC, tenant, redaction,\nbounded query results, and audit authority remain in the authenticated\n`PrincipalRun` supplied by `@happyvertical/smrt-agents`.\n\n## Runtime report tools (#3711)\n\n`createRuntimeReportTools()` (`runtime-report-tools.ts`) returns four\n`PrincipalTool`s for the persona `extraTools` seam: `reports.runtime.sources`,\n`reports.runtime.define` (`preview` / `apply`), `reports.runtime.list`, and\n`reports.runtime.run`. The model supplies only a declarative\n`RuntimeReportSpec` from `@happyvertical/smrt-reports`; sources are a\nserver-owned allow-list gated by `run.assertOperation(collection, 'read')`, and\nfield policy, `readPermission`, and tenancy are enforced by the compiler against\nthe live run on every compile and every stored-report run. `apply` needs the\n`specHash` of the previewed spec plus an app-owned `confirmation` host that\nresolves only after a human approved that exact spec; with no host the tools are\npropose-only and a model-supplied `confirmed` flag is ignored. Saving also needs\n`create` on the `RuntimeReport` collection, and `list`/`run` need `read`; the\ndefault `reportsCollection` is the `RuntimeReport` class itself, which resolves\nto the catalog slug. A string `collection`/`reportsCollection` is used verbatim\nas a catalog slug (never a class name); tests must run the tools under the real\n`assertOperationPermission` guard so a wrong slug fails. A stored report is\nwritten only by this confirmed `apply`: the model layer refuses every other\ninsert/spec change, and the generic manifest tools (`buildManifestToolCatalog`,\n`invokeManifestTool`) never offer or run a `RuntimeReport` write. Stored-report\nreads/saves run under the principal's tenant (`withPrincipalTenant`), refuse a\nmismatching ambient tenant, and filter by the principal tenant explicitly. Contract and authority model:\n[`packages/reports/agents/runtime-reports.md`](../reports/agents/runtime-reports.md).\n\n## Overview tools (#3727)\n\n`createOverviewTools({ host })` (`overview-tools.ts`) returns\n`overviews.describe`, `overviews.apply` (an atomic batch of structured widget\noperations) and `overviews.undo` (single step, keyed by tenant, user and page).\nThe host opens a page per `PrincipalRun` (smrt-svelte's\n`createOverviewAssistantSurface`, typed structurally here); validation and\n`canCustomize` are the surface's. Contract:\n[agents/overview-tools.md](agents/overview-tools.md).\n\n## Conversational Harness (L3, #1891)\n\nThe `AgentSession` runtime depends on personas, agents, and users. Keep those\nedges acyclic: none of those packages may depend back on chat.\n\n- **`runToolLoop(options)`** (`tool-loop.ts`) — a bounded `tool_call → observe → respond` loop. Tools are **manifest operations** of installed packages: `buildManifestToolCatalog({ allowedTools })` reads the `PermissionCatalogService` catalog and keeps only the `(collection, action)` entries named in the persona's allow-list (the **offer gate**; absent/empty ⇒ NO tools). The loop runs inside one `executeAsPrincipal` context, and `invokeManifestTool` executes each op **in-process (\"side door\")** against `run.context.database` (the RLS tx when Postgres RLS is on), after re-asserting the fail-closed allow-list (`run.assertToolAllowed`) AND the catalog permission (`run.assertOperation`) — the **execution gate**. Bounded by a max-steps ceiling (`DEFAULT_MAX_STEPS = 8`): on the ceiling it disables tools for one final completion so the turn always terminates with text. A thrown tool error is classified (`classifyToolError`): permission denials and 401/403 are `not_permitted`; a `ValidationError` or 400/404/409/422 is `invalid_request` (observation: actionable message, `publicMessage` first, `code`, fix-and-retry hint); else (incl. 429) `execution_error`.\n- **`runPersonaConversationTurn(options)`** (`persona-conversation.ts`) — binds a conversation to an `AgentPersona`/`ResolvedPersona`: runs as its principal (`runAsUserId`), offers only its `allowedTools`, speaks its instructions (`resolvePersonaInstructions`, layering approved learned directives), and injects its **recalled learning memory** (`personaLearningMemory`, isolated per `memoryScope`) into the system prompt. `bindPersonaToSession()` mirrors the persona's `allowedTools`/instructions onto the `AgentSession` so the chat authoring gate agrees with the loop's offer gate. Authors the reply (and each executed tool) via the internal `sendAgentReply` bridge. Tools run with the run-as user's **live** permissions unless the host passes a `permissions` snapshot (#2978), which `runToolLoop` forwards to `executeAsPrincipal` so a narrowed principal (e.g. a shared station) cannot reach grants — such as `readPermission`-gated data-surface fields — beyond that snapshot, including through `agents.invoke` delegation (the snapshot becomes the worker's permission ceiling).\n- **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.\n- **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.\n\n## Voice Gateway Turns (#1910)\n\nVoice 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.\n\nThe 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()`.\n\n## Token streaming\n\n`chat-stream.ts` serves browser-safe SSE through `./client`. The host's\n`authorize(request, body)` is the sole trust boundary: resolve identity, tenant,\ncapabilities, and generation limits server-side; never authorize from request\nsession metadata. Persona turns reuse the principal and fail-closed tool gates.\nThe engine and browser-safe `SmrtChatBackend` keep custom-tool resolution\nserver-side. See [agents/token-streaming.md](agents/token-streaming.md) for the\nevent contract, CORS policy, and streaming behavior.\n\n## Streamed assistant turns with browser tools (#2908)\n\n`runAssistantTurn()` (`assistant-turn.ts`) is the host-route engine for the\nAssistantDock: one turn over `runToolLoop` with server tools (offer-gated by\n`principal.allowedTools`), browser tools (validated by\n`sanitizeClientToolDeclarations` + a server allow-list), `maxSteps`, `signal`,\nand `onUsage`. A browser tool call SUSPENDS the turn (`stoppedReason:\n'client_tools'`); the transcript waits in an `AssistantContinuationStore`\n(single-use, 15-minute TTL; `createSessionContinuationStore` keeps it in the\nAgentSession's `sessionContext`) and the browser resumes it with results,\nwhich reach the model wrapped `untrusted`. `createAssistantTurnResponse`\nserves the events as SSE; `assistant-turn-events.ts` is the browser-safe wire\ncontract (`./assistant-turn` subpath, also a vite library entry because the\nSvelte dock imports it). The dock decides whether a browser call runs or waits\nfor the user from the page registry, never the server's echo; destructive\nalways waits. Details: [`docs/assistant-dock.md`](../../docs/assistant-dock.md).\n\n## AssistantDock (#2904)\n\n`AssistantDock` (`svelte/components/assistant/`, exported from `./svelte`)\nuses a host `DataSurfaceRegistry`'s mounted descriptors (none: plain chat).\nActions: `normalizeDataSurfaceActionRequest` → `AssistantActionClient`\n(preview/apply over a server `DataSurfaceActionAdapter`, not the\n`data-surface-bridge.ts` channel). Generated `ChatThread`/`ChatMessage`\n`list`/`get` are tenant- not member-scoped; the dock never calls them.\n\n**Server routes (#3368).** `./sveltekit` `mountAssistantRoutes()` serves the\ndock from one `[...path]` route; `createAssistantHttp{Transport,ActionClient}`\n(`./svelte`) are its browser half. Principal from `event.locals` or\n`resolvePrincipal`; threads only from the actor's keyed session room in that\ntenant (else 404). `allowedTools` is fail-closed; without `tools` it offers\nthose manifest ops (an unprovided name is an error, #3414). `ai` defaults to\nthe config `ai` block. Pass `runtime`: a turn never keeps the RLS request tx\n(own `runAsPrincipal` tx). `clientRequestId` makes the row id a UUIDv5\n(`clientRequestMessageId`): the PK is the retry reservation. See\n[docs](../../docs/assistant-dock.md).\n\n## Recipes (#3719)\n\n`src/recipes.ts` declares `chat.assistant` (the AssistantDock as a `header.end`\nshell widget, an `llm` provider, runtime `both`; help in\n`src/assistant.recipe.md`). Keep each recipe class self-contained. Details:\n[agents/recipes.md](agents/recipes.md).\n\n## Gotchas\n\n- **sessionContext, not context**: `context` is reserved for slug scoping. Use `getSessionContext()`/`updateSessionContext()` for agent memory.\n- **Agent rooms auto-created**: `roomType: 'agent'`, `maxParticipants` defaults to 2; the agent is enrolled as a member so its replies pass the membership check. `createAgentSession()` re-enrolls the participant AND the agent on the existing-session path, so legacy sessions created before the agent was enrolled self-heal.\n- **The agent's author is a Profile, not `agentId` (#2995)**: `senderProfileId`/`profileId` are `crossPackageRef`s to `Profile` and therefore uuid columns on PostgreSQL, so the slug fails the cast (`22P02`) and no agent reply can persist. `createAgentSession()` resolves the agent's `bot` Profile through `resolveAgentProfile()` in `@happyvertical/smrt-profiles` (created on first use, tenant-bound) and records it as `AgentSession.agentProfileId`; both enrolment and `sendAgentReply` use that uuid. A consumer whose agent already has an acting identity passes `agentProfileId` to skip resolution — the voice path does this with the persona's `actsAsProfileId`, so persona replies stay attributed to the configured acting profile. Like `actorProfileId`, `agentProfileId` is SERVER-supplied, never request input: it selects the author of every later assistant message. It is validated (`#requireAgentProfile`) to exist and be visible in the tenant and to differ from the acting participant, and it can never re-point a session that already resolved one — only a pre-#2995 session with none is backfilled. **Migration**: `agent_profile_id` is an ordinary additive manifest field, so a consumer adopts it the same way as any other — `smrt db:migrate` generates and applies the column from the manifest, and `db:status --parity` verifies it; the framework never creates application schema at runtime. It is nullable and needs no offline DATA backfill — a session created before #2995 resolves and persists its `agentProfileId`, and re-enrols the agent, on its first agent turn after the upgrade. Two pre-#2995 cohorts: a session whose `agentId` is an ordinary slug never committed an author on PostgreSQL (the cast failed), so there is nothing to repair there, while SQLite/DuckDB may hold `chat_messages`/`chat_participants` rows whose id column holds the slug — readable, but not joinable to `profiles`. A session whose `agentId` is already a Profile uuid (the voice path used to collapse a persona's `actsAsProfileId` into it) DID commit on PostgreSQL; that profile is ADOPTED as the author, tenant-checked and never the participant, so the conversation's author does not change and the two-seat room gains no third identity. Known one-time effect: because `voice.ts` no longer derives `agentId` from `actsAsProfileId`, a keyless `createVoiceChatSession()` for such a persona opens a new session and room instead of reusing the legacy one; the old room and its messages stay intact and readable, and resuming with an explicit `agentSessionId` is unaffected.\n- **Only SQLite-backed tests would miss this class of bug**: uuid columns accept any text on SQLite. `src/__tests__/agent-reply-postgres.test.ts` (`pnpm --filter @happyvertical/smrt-chat test:postgres`) pins the agent-reply path against real PostgreSQL. That suite runs in `postgres-tests.yml` on every same-repository PR and merge group (#2659); keep a cheap SQLite assertion alongside it anyway, since the default lane is SQLite-only (`chat-service.test.ts` asserts the author is the profile uuid, not the slug).\n- **Per-subject sessions need `sessionKey`**: `createAgentSession()` reuses ANY active session for the same `(agentId, participantProfileId, tenantId)`. Callers that open separate conversations per subject (e.g. one content-editor session per content id) MUST pass a stable `sessionKey` (stored in `sessionContext.__sessionKey`, read via `AgentSession.getSessionKey()`); otherwise a session opened for one subject is reused and its context overwritten for another, surfacing the wrong room/threads (S5 #1392). A keyed create never reuses a keyless/legacy session.\n- **Session expiry**: check `isActive()` before allowing messages (expiresAt or limit-based)\n- **DM identity**: derived from the deterministic per-tenant `canonicalDmRoomId()` and the authoritative `chat_participants` join, not client metadata; concurrent creates upsert onto one row.\n- **Tenant-bound lookups**: membership/session/DM lookups REQUIRE `tenantId` and always bind it into the WHERE clause (`findActiveMembership`/`isActiveMember`/`findActiveSession` take a required `tenantId`; AgentSession's `null` tenant is an explicit bound scope, not \"any tenant\") so they can never resolve a row from another tenant.\n",
|
|
5145
5156
|
"moduleDocs": [
|
|
5146
5157
|
{
|
|
5147
5158
|
"path": "agents/dev-workbench.md",
|
|
5148
5159
|
"module": "dev-workbench",
|
|
5149
|
-
"content": "# Chat dev workbench: browser inference and draft form\n\nReference for the `pnpm --dir packages/chat dev` workbench root route\n(`src/routes/+page.svelte`). Linked from [../AGENTS.md](../AGENTS.md).\n\n## Browser inference path\n\nThe root route drives the **browser inference path**\n(`@happyvertical/smrt-web/ai`) end to end, which is what the workbench exists to\nexercise now: THREE backends — `createBitGpuInferenceBackend()` (Bonsai 1.7B,\n~237 MB, the dev-loop default), `createWebLlmInferenceBackend()`, and\n`createRouteInferenceBackend()` over `/api/dev-chat-stream` — composed by\n`createInferencePath({ backends: [bitgpu, local, route] })`. The local backends\nare listed first, so `auto` prefers one whenever it is READY; the route backend is\nalways ready, so `auto` always has somewhere to land. The status panel's Inference\nselector pins `auto` / `bitgpu` / `local` / `route`, it renders a\n`ModelStatusControl` per local backend, and the reply is rendered as it streams.\n`/api/dev-chat` is retained as the non-streaming reference endpoint; the workbench\nno longer calls it.\n\nFour dev-config facts this depends on:\n\n- The workbench injects `loadModule: () => import('@mlc-ai/web-llm')` and, for\n bitgpu, `loadBitGpu`/`loadChat` as `() => import('bitgpu')` /\n `() => import('bitgpu/chat')`. A browser resolves only STATIC specifiers — the\n adapters' own optional-import helpers use variable ones — so without these the\n optional peers fail to load and (for WebLLM) the error misreports the package as\n \"not installed\".\n- bitgpu's per-model `manifest.json` + `.aux.bin` come from its repo via jsDelivr;\n the Bonsai GGUF streams from the Hub; the tokenizer comes from\n `onnx-community/Bonsai-1.7B-ONNX` because `prism-ml/Bonsai-1.7B-gguf` ships no\n `tokenizer.json`.\n- `workspace-aliases.js` must list every deep `@happyvertical/smrt-ui/*` subpath\n a consumer reaches. A string alias is a PREFIX match, so the bare\n `@happyvertical/smrt-ui` entry rewrites any unlisted subpath under it into a\n bogus path under `src/index.ts`. That includes STYLESHEETS:\n `@happyvertical/smrt-ui/themes/styles/all.css` needs its own entry or the app\n renders with no theme variables at all.\n- The workbench imports `@happyvertical/smrt-ui/themes/styles/{all,fonts}.css`.\n `ThemeProvider` for a BUILT-IN preset only sets `data-theme` /\n `data-color-scheme` on its wrapper — it deliberately emits no inline variables,\n because built-ins ship their palette as static CSS. Without the import the\n attributes are present, `--smrt-color-*` resolves to nothing, and every themed\n surface paints transparent (which reads as \"dark mode is broken\" in a dark\n scheme). `smrt-workbench/host` is the reference for this.\n\n## Agent-addressable draft form\n\nThe workbench also carries an **agent-addressable draft form**: an `Input` inside\na `Form`, bound by `useViewIntent` to the intent declared in\n`src/routes/chat-dev.intents.ts` (#2588). It is the end-to-end demonstration of the\ncontrol-interaction stack — an agent stages, a human applies. Four things about it\nare non-obvious and cost time to find:\n\n- **No `<Provider>` here, so both seams must be explicit.** `useViewIntent` is a\n documented silent no-op without a Provider ancestor, so the binding passes\n `controlRegistry` directly; and `effects: ['read', 'write']` supplies the\n exposure policy, because the default is read-only and would exclude a `stage`.\n- **`inputSchema` on the declaration is REQUIRED for the tool to be callable.**\n `compileViewIntentToolSpec` passes it through verbatim, but\n `buildControlCommand` throws `IntentArgumentError('value')` for a `stage` with\n no `value`. Without it the tool registers, advertises\n `{\"type\":\"object\",\"properties\":{}}`, and fails at call time with Chrome's\n `UnknownError: Failed to parse input arguments`.\n- **`document.modelContext.executeTool(tool, args)` takes the tool OBJECT from\n `getTools()` and the arguments as a JSON STRING** — an object as the second\n argument fails the same way.\n- **Apply needs a genuinely trusted gesture.** The registry refuses a\n programmatic `element.click()` (`event.isTrusted === false`) for `apply`, which\n is the stage→apply consent split doing its job. Verification must use a real\n input event, not `evaluate(() => button.click())`.\n"
|
|
5160
|
+
"content": "# Chat dev workbench: browser inference and draft form\n\nReference for the `pnpm --dir packages/chat dev` workbench root route\n(`src/routes/+page.svelte`). Linked from [../AGENTS.md](../AGENTS.md).\n\n## Browser inference path\n\nThe root route drives the **browser inference path**\n(`@happyvertical/smrt-web/ai`) end to end, which is what the workbench exists to\nexercise now: THREE backends — `createBitGpuInferenceBackend()` (Bonsai 1.7B,\n~237 MB, the dev-loop default), `createWebLlmInferenceBackend()`, and\n`createRouteInferenceBackend()` over `/api/dev-chat-stream` — composed by\n`createInferencePath({ backends: [bitgpu, local, route] })`. The local backends\nare listed first, so `auto` prefers one whenever it is READY; the route backend is\nalways ready, so `auto` always has somewhere to land. The status panel's Inference\nselector pins `auto` / `bitgpu` / `local` / `route`, it renders a\n`ModelStatusControl` per local backend, and the reply is rendered as it streams.\n`/api/dev-chat` is retained as the non-streaming reference endpoint; the workbench\nno longer calls it.\n\nFour dev-config facts this depends on:\n\n- The workbench injects `loadModule: () => import('@mlc-ai/web-llm')` and, for\n bitgpu, `loadBitGpu`/`loadChat` as `() => import('bitgpu')` /\n `() => import('bitgpu/chat')`. A browser resolves only STATIC specifiers — the\n adapters' own optional-import helpers use variable ones — so without these the\n optional peers fail to load and (for WebLLM) the error misreports the package as\n \"not installed\".\n- bitgpu's per-model `manifest.json` + `.aux.bin` come from its repo via jsDelivr;\n the Bonsai GGUF streams from the Hub; the tokenizer comes from\n `onnx-community/Bonsai-1.7B-ONNX` because `prism-ml/Bonsai-1.7B-gguf` ships no\n `tokenizer.json`.\n- `workspace-aliases.js` must list every deep `@happyvertical/smrt-ui/*` subpath\n a consumer reaches. A string alias is a PREFIX match, so the bare\n `@happyvertical/smrt-ui` entry rewrites any unlisted subpath under it into a\n bogus path under `src/index.ts`. That includes STYLESHEETS:\n `@happyvertical/smrt-ui/themes/styles/all.css` needs its own entry or the app\n renders with no theme variables at all.\n- The workbench imports `@happyvertical/smrt-ui/themes/styles/{all,fonts}.css`.\n `ThemeProvider` for a BUILT-IN preset only sets `data-theme` /\n `data-color-scheme` on its wrapper — it deliberately emits no inline variables,\n because built-ins ship their palette as static CSS. Without the import the\n attributes are present, `--smrt-color-*` resolves to nothing, and every themed\n surface paints transparent (which reads as \"dark mode is broken\" in a dark\n scheme). `smrt-workbench/host` is the reference for this.\n\n## Agent-addressable draft form\n\nThe workbench also carries an **agent-addressable draft form**: an `Input` inside\na `Form`, bound by `useViewIntent` to the intent declared in\n`src/routes/chat-dev.intents.ts` (#2588). It is the end-to-end demonstration of the\ncontrol-interaction stack — an agent stages, a human applies. Four things about it\nare non-obvious and cost time to find:\n\n- **No `<Provider>` here, so both seams must be explicit.** `useViewIntent` is a\n documented silent no-op without a Provider ancestor, so the binding passes\n `controlRegistry` directly; and `effects: ['read', 'write']` supplies the\n exposure policy, because the default is read-only and would exclude a `stage`.\n- **`inputSchema` on the declaration is REQUIRED for the tool to be callable.**\n `compileViewIntentToolSpec` passes it through verbatim, but\n `buildControlCommand` throws `IntentArgumentError('value')` for a `stage` with\n no `value`. Without it the tool registers, advertises\n `{\"type\":\"object\",\"properties\":{}}`, and fails at call time with Chrome's\n `UnknownError: Failed to parse input arguments`.\n- **`document.modelContext.executeTool(tool, args)` takes the tool OBJECT from\n `getTools()` and the arguments as a JSON STRING** — an object as the second\n argument fails the same way.\n- **Apply needs a genuinely trusted gesture.** The registry refuses a\n programmatic `element.click()` (`event.isTrusted === false`) for `apply`, which\n is the stage→apply consent split doing its job. Verification must use a real\n input event, not `evaluate(() => button.click())`.\n\n## Voice and local character persistence\n\nThe root workbench also has a dev-only voice conversation mode. It reads voice\ngateway connection details through `/api/dev-voice/config`, streams browser mic\naudio to `WS /ws/voice` as PCM16 mono, appends gateway transcripts/responses to\nthe chat, and plays returned TTS audio. Exposing\n`SMRT_CHAT_DEV_VOICE_GATEWAY_TOKEN` to the browser requires\n`SMRT_CHAT_DEV_VOICE_GATEWAY_EXPOSE_TOKEN=true`; keep that local-only.\n\n### Local character persistence\n\n`/api/dev-character-persistence` is an opt-in loopback-only demo bridge for\nthe photographic character workbench. It is disabled unless\n`SMRT_CHAT_DEV_CHARACTER_PERSISTENCE=true`; it accepts neither actor, tenant,\nnor profile ids from the browser. Its server configuration names one\npre-provisioned profile and tenant plus a temporary SQLite database and asset\ndirectory, all under the operating system temporary directory. First run\n`DATABASE_URL=<the same SQLite path> pnpm smrt db:migrate`, then run\n`pnpm exec tsx src/scripts/provision-dev-character-persistence.ts` with the\n`SMRT_CHAT_DEV_CHARACTER_*` variables set. The route never creates schema.\nUse fresh UUIDs for `SMRT_CHAT_DEV_CHARACTER_PROFILE_ID` and\n`SMRT_CHAT_DEV_CHARACTER_TENANT_ID`; keep the database and asset directory in\nthe OS temporary directory, for example `/tmp/smrt-character-setup/` on Linux\nor the path reported by `node -p \"require('node:os').tmpdir()\"` on macOS.\n\nThis is not production authentication. A production host must provide its\nauthenticated principal, tenant, and authorization policy to\n`PhotoCutoutProfileStore`; it must not copy this fixed local identity pattern.\n\n### Local helper control panel\n\n`/api/dev-helper` uses the same loopback-only identity and durable Profile\nmetadata store. Provisioning also creates its application-scoped metafield;\nthe route never creates schema or accepts actor, tenant, policy, provider, or\nasset URL fields from the browser. It resolves the current gallery, allowed\nvoice, and helper policy on every load/save/reset. Its whole-value persistence\nsemantic is last-successful-write-wins, so a production host with concurrent\neditors must serialize writes per profile/application or supply a stronger\nadapter. The speech route resolves the saved offered voice server-side; clients\nsubmit only text. Helper preference loading never starts audio or a microphone.\n\n### Character conversation lifecycle\n\nThe Character conversation tab refreshes the persisted rig each time it becomes\nactive, retaining its AssistantDock controller and history. Leaving the tab stops\nmicrophone input and reply playback; listening mode requires an explicit restart.\nAn outstanding turn keeps both the listening input and full dock composer busy\nacross mode changes and restarts until it settles. Both inputs share the retained\ncontroller’s pending-send state; microphone Stop remains reachable. Polling\nretains active sends until their transport completes, even when older history\ncontains the same message text.\nLate persistence loads and cancelled speech responses cannot replace the current\nrig or audio. Spoken captions begin only when SDK playback actually starts.\nListening mode also keeps the newest assistant reply in a dedicated reply\nregion, so a muted or failed spoken reply is still readable while history stays\nhidden. A speech failure is shown beside the conversation controls; disabling\nspoken replies aborts its outstanding request and playback. While the tab is\ninactive, its fixed dock is hidden and polling is paused.\n\nThe development workbench links the SDK speech and animation packages from\nsibling worktrees. Keep those worktrees available until their released package\nversions replace the links; a fresh browser or Vite load cannot resolve a\nlinked package whose sibling checkout is absent.\n\nThe development transport sends at most 24 context messages within a 16 KiB UTF-8\nJSON body, dropping oldest context first. Failed turns are not committed to its\nhistory, so retrying does not duplicate them. Draft proposals use one shared\n200-character limit in the model tool, route, preview, and execution boundary.\n"
|
|
5161
|
+
},
|
|
5162
|
+
{
|
|
5163
|
+
"path": "agents/overview-tools.md",
|
|
5164
|
+
"module": "overview-tools",
|
|
5165
|
+
"content": "# Overview assistant tools (#3727 phase 4)\n\n`createOverviewTools({ host, undoStore?, audit?, maxOperations? })`\n(`src/overview-tools.ts`, package index) returns three `PrincipalTool`s for the\npersona / assistant-route `extraTools` seam. They let the assistant add and\nconfigure widgets on a customizable overview page (\"add a chart of overdue\ninvoices\") with structured operations, and undo its last change. The overview\nmodel, widget registry and option schemas are smrt-svelte's\n([overview-surfaces.md](../../smrt-svelte/agents/overview-surfaces.md)).\n\n| Slug (function name) | Arguments | Result |\n|---|---|---|\n| `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` |\n| `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 |\n| `overviews.undo` (`overviews-undo`) | `{ page, undoToken }` | `{ page, undone: true, overview }` |\n\nOperations (1 to `maxOperations`, default 20): `add { type, span?, options?,\nindex? }`, `configure { id, options }` (merged onto the current options; `null`\nclears a key to its default), `move { id, index }`, `resize { id, span }`,\n`remove { id }`. Option values are JSON primitives over the widget's closed\nfield vocabulary; there is no query, filter expression, URL or SQL anywhere.\n\n## Authority\n\n- **The host decides, per principal.** `host.open(run, pageId)` returns the\n page for that `PrincipalRun` or `null` (unknown and not offered answer the\n same 404). Build it with smrt-svelte's `createOverviewAssistantSurface`\n (`./overview/server`) from the page's `OverviewDefinition`, the\n principal's own stored override, `canCustomize` from the principal's role,\n and `persist`. Never derive any of these from model input except the page id.\n- **Tenant.** Like the runtime-report tools, with tenancy on a run without\n `run.context.tenantId` is refused (403 `tenant_required`), `host.open` and\n every surface call (including `persist`) run under the principal's tenant\n (entered when no tenant context is active), and an ambient tenant that\n differs from the principal's is refused (`DataSurfaceDeniedError`) unless\n the caller is in a system-context or super-admin path. The host scopes its\n reads and writes by `run.context.tenantId`.\n- **Validation is the overview model's.** The surface plans each batch with\n `planOverviewOperations` and checks the result with `checkOverviewOverride`\n (allowed set and `allowedIn`, page-confined `models`, option schemas, span\n ranges, the widget cap), so the assistant can only store an override the\n page's own save endpoint accepts. A batch is atomic.\n- **Errors** are `OverviewToolError` with `status`, `code`, `publicMessage`,\n `issues`. `runToolLoop` classifies 404/409/422 as `invalid_request` (the model\n sees `publicMessage`, which lists every issue as `#<n> <op>: <message>`, and is\n told to fix and retry) and 403 as `not_permitted`. Codes: `invalid_page`,\n `invalid_operations` (422), `unknown_page` (404), `tenant_required` (403),\n `not_allowed` (403,\n `canCustomize` false, on apply and on undo), `conflict` (409, apply lost a\n race with another save), `nothing_to_undo`, `changed_since`,\n `cannot_restore` (409).\n- **Writes are conditional.** `surface.persist(override)` answers\n `OverviewToolPersistResult` (`{ ok: true }` or `{ ok: false, reason:\n 'conflict' | 'not_allowed' | 'invalid', issues? }`) and must write only if\n the stored row is still the revision the surface was opened on. A save that\n lands between `host.open` and the write is a `conflict`: apply answers 409\n `conflict` (describe again, rebuild the batch), undo answers 409\n `changed_since` and keeps its entry. smrt-svelte's surface takes the loaded\n `revision` and passes `{ revision }` to its `persist` option, so the\n revision-guarded `save` of `@happyvertical/smrt-preferences`'\n `createOverviewStore` plugs in directly (see below).\n- **Undo** is single-step. Apply stores `{ token, before, after }` under\n `[tenantId, userId, pageId]` of the calling run; undo looks it up under the\n calling run only, so another principal's (or tenant's) token is simply not\n found. It refuses when the stored override is no longer `after` (someone\n changed the page since), re-checks `before` with `check`, persists it\n conditionally, and deletes the entry only after that write succeeded.\n Removal is compare-and-delete: `OverviewUndoStore.delete(key, token)` is a\n no-op unless the stored entry still carries that token, so an undo that\n finishes after a newer batch stored its entry never removes that entry.\n Storing is a conditional set: an entry's `createdAt` is stamped right after\n its batch's conditional write succeeded (strictly increasing per process),\n and `set` is a no-op when the stored entry is newer, so an older apply whose\n entry write lands late never replaces a newer batch's entry. Ordering by\n write-success time rather than serializing per key works across replicas\n sharing one store (a lock would only cover one process), and the revision\n guard already serializes the writes themselves; revisions are opaque here,\n so they cannot order entries. Across replicas the stamps rely on clocks\n agreeing to within the time between two applies by the same person on the\n same page.\n- **The undo store must outlive a request and a turn.** Without `undoStore`\n the tools use `defaultOverviewUndoStore()`, one in-memory store per process\n (30-minute TTL, 1000 keys), so tools built per turn\n (`extraTools: (context) => createOverviewTools(...)`) still undo an earlier\n turn's batch. Never pass a store created inside the per-turn factory. A\n multi-replica host passes one shared `OverviewUndoStore`, created once,\n whose `set` (newer `createdAt` wins) and `delete(key, token)` are atomic\n conditional operations.\n- **After a successful write the call never fails.** The undo entry is written\n after the write; if the store throws, apply still answers `changed: true`\n with `undoToken: null` and a `note` not to apply the batch again (a lost entry\n only costs the Undo, while an entry written first could later restore over a\n value the batch never wrote). Audit is best effort; failures go to\n `onError` (default `console.warn`).\n\n## Wiring to persistence (phase 3)\n\nThe surface is the only seam; this package has no table and no dependency on\nsmrt-svelte or `@happyvertical/smrt-preferences` (smrt-svelte depends on chat\nfor tests, so the types are structural). Phase 3 stores overview layouts in\n`@happyvertical/smrt-preferences` (table `_smrt_ui_preferences`, preference\nkind `overview`); its overview layer is `createOverviewStore({ db })`, built\nover the generic `createPreferenceStore({ db })` (pass `preferences` to share\none). Every store call acts as the ambient principal, which is the run's\nprincipal inside the tool loop, under the tenant the tools enter.\n\n```ts\nimport { createOverviewTools } from '@happyvertical/smrt-chat';\nimport {\n createOverviewStore,\n withTenantDefaults,\n} from '@happyvertical/smrt-preferences';\nimport { createOverviewAssistantSurface } from '@happyvertical/smrt-svelte/overview/server';\n\n// Module scope: created once, shared by every turn and request.\nconst overviewUndo = createSharedOverviewUndoStore(); // or omit for the process default\n\nconst overviewTools = (db: DatabaseInterface /* per turn */) => {\n const store = createOverviewStore({ db });\n return createOverviewTools({\n undoStore: overviewUndo,\n host: {\n async open(run, pageId) {\n const definition = overviewDefinitions.get(pageId);\n if (!definition) return null;\n const state = await store.load(definition, registry);\n if (!state.user) return null;\n return createOverviewAssistantSurface({\n // The user tier is a delta over the tenant default.\n definition: withTenantDefaults(definition, state.tenant.document),\n registry,\n override: state.user.override,\n revision: state.user.revision,\n canCustomize: state.canCustomize.user,\n // The page definition: the store merges the tenant tier itself.\n persist: (override, { revision }) =>\n store.save(definition, registry, { scope: 'user', override, revision }),\n });\n },\n },\n });\n};\n```\n\n`createSharedOverviewUndoStore` stands for the host's own shared store (any\n`OverviewUndoStore`). How the store's answer maps onto the tools:\n\n| `store.save(definition, registry, { scope, override, revision })` (`OverviewSaveResult`) | Surface / tool |\n|---|---|\n| `{ ok: true, override, revision }` | the surface follows the new `revision`; apply answers `changed: true` |\n| `{ 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 |\n| `{ ok: false, reason: 'not_allowed' }` (no `overviews.personalize` / `overviews.customize`) | 403 `not_allowed` |\n| `{ ok: false, reason: 'invalid', issues }` (`checkOverviewOverride` refused it) | 422 `invalid_operations` |\n\n`revision` is `null` when the principal had no row yet; the first write then\ncreates it, and a concurrent first write answers `conflict`. Add the three\nslugs to the persona's / route's `allowedTools`.\n\n## In the browser\n\nWhen the overview is mounted on the page the dock runs on, use the browser\ntools instead: smrt-svelte's `createOverviewAssistant(controller)` +\n`useOverviewAssistantTools` register `overview_describe`, `overview_apply` and\n`overview_undo` as page tools (add `overview_*` to the route's\n`clientToolAllowList`). A batch lands through `controller.restore`, so the grid\nupdates live and the host's `onchange` persists it, and `OverviewAssistantUndo`\n(or a toast) offers Undo. Same operations, same validation, same error shape\n(returned as JSON, not thrown).\n\nTests: `src/overview-tools.test.ts` (contract against a structural surface) and\n`packages/smrt-svelte/src/components/overview/__tests__/assistant-tools.integration.test.ts`\n(these tools over the real model).\n"
|
|
5150
5166
|
},
|
|
5151
5167
|
{
|
|
5152
5168
|
"path": "agents/token-streaming.md",
|
|
5153
5169
|
"module": "token-streaming",
|
|
5154
|
-
"content": "# Token Streaming (SSE, #1936)\n\
|
|
5170
|
+
"content": "# Token Streaming (SSE, #1936)\n\nReference linked from [../AGENTS.md](../AGENTS.md). This contract was moved from\n`packages/chat/AGENTS.md` to keep the instruction chain under the 32 KB cap\n(`pnpm check:agents-chain`); AGENTS.md retains the cap-oriented summary.\n\n`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`.\n\n- **`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).\n- **`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.\n- **`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.\n- **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.\n- **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.\n"
|
|
5155
5171
|
},
|
|
5156
5172
|
{
|
|
5157
5173
|
"path": "agents/recipes.md",
|
|
@@ -5248,10 +5264,125 @@
|
|
|
5248
5264
|
"demo": {
|
|
5249
5265
|
"mode": "server",
|
|
5250
5266
|
"reasons": [
|
|
5251
|
-
"Needs a server because its package does not build for a browser: node:crypto + node:async_hooks; and its package depends on @happyvertical/smrt-agents, @happyvertical/smrt-jobs, @happyvertical/smrt-profiles, @happyvertical/smrt-reports, @happyvertical/smrt-secrets, @happyvertical/smrt-tenancy, @happyvertical/smrt-users, which do not build for a browser (#3615, #3617, #3624, #3625, #3626, #3627).",
|
|
5267
|
+
"Needs a server because its package does not build for a browser: node:crypto + node:async_hooks; and its package depends on @happyvertical/smrt-agents, @happyvertical/smrt-images, @happyvertical/smrt-jobs, @happyvertical/smrt-profiles, @happyvertical/smrt-reports, @happyvertical/smrt-secrets, @happyvertical/smrt-tenancy, @happyvertical/smrt-users, which do not build for a browser (#3615, #3617, #3624, #3625, #3626, #3627, #3628).",
|
|
5252
5268
|
"Provider llm has an in-browser option (webllm, bitgpu)."
|
|
5253
5269
|
]
|
|
5254
5270
|
}
|
|
5271
|
+
},
|
|
5272
|
+
{
|
|
5273
|
+
"id": "chat.rooms",
|
|
5274
|
+
"className": "ChatRoomsRecipe",
|
|
5275
|
+
"label": "Team Chat",
|
|
5276
|
+
"summary": "Organize team conversations in rooms with threads and reactions.",
|
|
5277
|
+
"synonyms": [
|
|
5278
|
+
"team chat",
|
|
5279
|
+
"channels",
|
|
5280
|
+
"group chat",
|
|
5281
|
+
"conversations"
|
|
5282
|
+
],
|
|
5283
|
+
"models": [
|
|
5284
|
+
"@happyvertical/smrt-chat:ChatRoom",
|
|
5285
|
+
"@happyvertical/smrt-chat:ChatMessage",
|
|
5286
|
+
"@happyvertical/smrt-chat:ChatParticipant",
|
|
5287
|
+
"@happyvertical/smrt-chat:ChatThread",
|
|
5288
|
+
"@happyvertical/smrt-chat:ChatReaction"
|
|
5289
|
+
],
|
|
5290
|
+
"nav": [
|
|
5291
|
+
{
|
|
5292
|
+
"label": "Chat Rooms",
|
|
5293
|
+
"model": "@happyvertical/smrt-chat:ChatRoom",
|
|
5294
|
+
"icon": "users",
|
|
5295
|
+
"description": "Team conversations organized by topic or audience.",
|
|
5296
|
+
"noun": "chat room"
|
|
5297
|
+
}
|
|
5298
|
+
],
|
|
5299
|
+
"requires": [],
|
|
5300
|
+
"group": {
|
|
5301
|
+
"id": "team-collaboration",
|
|
5302
|
+
"label": "Team collaboration",
|
|
5303
|
+
"summary": "Keep conversations and shared work in one place."
|
|
5304
|
+
},
|
|
5305
|
+
"section": {
|
|
5306
|
+
"id": "team",
|
|
5307
|
+
"label": "Team",
|
|
5308
|
+
"icon": "users",
|
|
5309
|
+
"description": "People and the conversations that keep them aligned."
|
|
5310
|
+
},
|
|
5311
|
+
"options": {
|
|
5312
|
+
"@happyvertical/smrt-chat:ChatRoom": {
|
|
5313
|
+
"fields": {
|
|
5314
|
+
"metadata": {
|
|
5315
|
+
"visibility": "hidden"
|
|
5316
|
+
},
|
|
5317
|
+
"createdByProfileId": {
|
|
5318
|
+
"visibility": "hidden"
|
|
5319
|
+
},
|
|
5320
|
+
"lastMessageAt": {
|
|
5321
|
+
"label": "Last message",
|
|
5322
|
+
"order": 1
|
|
5323
|
+
}
|
|
5324
|
+
}
|
|
5325
|
+
},
|
|
5326
|
+
"@happyvertical/smrt-chat:ChatMessage": {
|
|
5327
|
+
"fields": {
|
|
5328
|
+
"metadata": {
|
|
5329
|
+
"visibility": "hidden"
|
|
5330
|
+
},
|
|
5331
|
+
"toolCallData": {
|
|
5332
|
+
"visibility": "hidden"
|
|
5333
|
+
},
|
|
5334
|
+
"attachments": {
|
|
5335
|
+
"visibility": "advanced"
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
5338
|
+
},
|
|
5339
|
+
"@happyvertical/smrt-chat:ChatParticipant": {
|
|
5340
|
+
"fields": {
|
|
5341
|
+
"lastReadMessageId": {
|
|
5342
|
+
"visibility": "hidden"
|
|
5343
|
+
},
|
|
5344
|
+
"lastSeenAt": {
|
|
5345
|
+
"visibility": "advanced"
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
},
|
|
5350
|
+
"help": {
|
|
5351
|
+
"markdown": "## Overview\n\nTeam Chat keeps conversations in rooms so people can find the right discussion,\nfollow threads, and see reactions without mixing unrelated work together. The\nroom and message lists are read-only generated views. Create rooms, manage\nmembers, and send messages through the app's authorized chat workflow.\n\n## Tasks\n\n### Find the right room\n\n1. Open **Chat Rooms** from the navigation.\n2. Search for a room by its **{field:ChatRoom.name}**, or use its\n **{field:ChatRoom.topic}** to tell similar rooms apart.\n3. Open the room and check **{field:ChatRoom.lastMessageAt}** to see where the\n most recent discussion happened.\n\n### Follow a conversation\n\n1. Read the room messages and open a thread when you need the discussion in\n one place.\n2. Use the thread **{field:ChatThread.title}** to describe the subject when\n your app supplies one.\n3. Add a reaction by choosing the **{field:ChatReaction.emoji}** that best\n communicates your response.\n\n### Keep a room useful\n\n1. Give the room a clear **{field:ChatRoom.name}** and a short\n **{field:ChatRoom.description}**.\n2. Set **{field:ChatRoom.roomType}** deliberately: public for broad team\n discussion, private for restricted groups, and direct messages for one-to-one\n conversations.\n3. Use a focused **{field:ChatMessage.content}** so teammates can scan the\n conversation and reply in context.\n",
|
|
5352
|
+
"fieldRefs": [
|
|
5353
|
+
"ChatMessage.content",
|
|
5354
|
+
"ChatReaction.emoji",
|
|
5355
|
+
"ChatRoom.description",
|
|
5356
|
+
"ChatRoom.lastMessageAt",
|
|
5357
|
+
"ChatRoom.name",
|
|
5358
|
+
"ChatRoom.roomType",
|
|
5359
|
+
"ChatRoom.topic",
|
|
5360
|
+
"ChatThread.title"
|
|
5361
|
+
]
|
|
5362
|
+
},
|
|
5363
|
+
"surfaces": [
|
|
5364
|
+
{
|
|
5365
|
+
"kind": "route",
|
|
5366
|
+
"path": "/chat/rooms",
|
|
5367
|
+
"export": "@happyvertical/smrt-chat/svelte#ChatLayout",
|
|
5368
|
+
"label": "Team chat"
|
|
5369
|
+
},
|
|
5370
|
+
{
|
|
5371
|
+
"kind": "playground",
|
|
5372
|
+
"export": "@happyvertical/smrt-chat/playground#default",
|
|
5373
|
+
"label": "Static chat fixtures"
|
|
5374
|
+
}
|
|
5375
|
+
],
|
|
5376
|
+
"runtime": "both",
|
|
5377
|
+
"demoSeed": {
|
|
5378
|
+
"export": "@happyvertical/smrt-chat/playground#chatRoomsFixture"
|
|
5379
|
+
},
|
|
5380
|
+
"demo": {
|
|
5381
|
+
"mode": "server",
|
|
5382
|
+
"reasons": [
|
|
5383
|
+
"Needs a server because its package does not build for a browser: node:crypto + node:async_hooks; and its package depends on @happyvertical/smrt-agents, @happyvertical/smrt-images, @happyvertical/smrt-jobs, @happyvertical/smrt-profiles, @happyvertical/smrt-reports, @happyvertical/smrt-secrets, @happyvertical/smrt-tenancy, @happyvertical/smrt-users, which do not build for a browser (#3615, #3617, #3624, #3625, #3626, #3627, #3628)."
|
|
5384
|
+
]
|
|
5385
|
+
}
|
|
5255
5386
|
}
|
|
5256
5387
|
],
|
|
5257
5388
|
"browser": {
|
|
@@ -5262,11 +5393,13 @@
|
|
|
5262
5393
|
"#3624",
|
|
5263
5394
|
"#3625",
|
|
5264
5395
|
"#3626",
|
|
5265
|
-
"#3627"
|
|
5396
|
+
"#3627",
|
|
5397
|
+
"#3628"
|
|
5266
5398
|
],
|
|
5267
5399
|
"reason": "node:crypto + node:async_hooks",
|
|
5268
5400
|
"via": [
|
|
5269
5401
|
"@happyvertical/smrt-agents",
|
|
5402
|
+
"@happyvertical/smrt-images",
|
|
5270
5403
|
"@happyvertical/smrt-jobs",
|
|
5271
5404
|
"@happyvertical/smrt-profiles",
|
|
5272
5405
|
"@happyvertical/smrt-reports",
|
|
@@ -105,6 +105,11 @@ export interface Props {
|
|
|
105
105
|
surfaces?: DataSurfaceIdentity[];
|
|
106
106
|
/** Whether the dock is currently visible; polling pauses while false. */
|
|
107
107
|
visible?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* `controls` keeps live status and required decisions while omitting the
|
|
110
|
+
* transcript, thread list and composer for a host-owned conversation view.
|
|
111
|
+
*/
|
|
112
|
+
presentation?: 'full' | 'controls';
|
|
108
113
|
/** How the dock presents conversations. `multiple` (default) shows the
|
|
109
114
|
* conversation list and lets the person start new ones. `single` is for
|
|
110
115
|
* hosts where the person never needs to know about separate conversations:
|
|
@@ -160,6 +165,8 @@ export interface Props {
|
|
|
160
165
|
/** Placeholder for the composer's empty textarea (#2991). Defaults to the
|
|
161
166
|
* composer's own placeholder. */
|
|
162
167
|
composerPlaceholder?: string;
|
|
168
|
+
/** Disables text submission while a host-owned input has a pending turn. */
|
|
169
|
+
composerDisabled?: boolean;
|
|
163
170
|
/** The page's browser tools (#2908) — typically
|
|
164
171
|
* `installWebMcpPageToolRegistry()` from
|
|
165
172
|
* `@happyvertical/smrt-web/webmcp-page-tools`. Offered to the model each
|
|
@@ -182,6 +189,8 @@ export interface Props {
|
|
|
182
189
|
/** Called whenever the supervised run changes (#assistant-watch). Also
|
|
183
190
|
* readable as `controller.run`. */
|
|
184
191
|
onrun?: (run: AssistantRun | null) => void;
|
|
192
|
+
/** Reports whether a person must make an explicit decision. */
|
|
193
|
+
onattentionchange?: (required: boolean) => void;
|
|
185
194
|
/** Page features that can offer a few options for the person to pick
|
|
186
195
|
* (see `./assistant-choices.svelte.ts`). The options show as cards here;
|
|
187
196
|
* the person's click applies one. */
|
|
@@ -217,6 +226,7 @@ const {
|
|
|
217
226
|
actionClient,
|
|
218
227
|
surfaces,
|
|
219
228
|
visible = true,
|
|
229
|
+
presentation = 'full',
|
|
220
230
|
conversations = 'multiple',
|
|
221
231
|
threadList = true,
|
|
222
232
|
toolCall,
|
|
@@ -225,6 +235,7 @@ const {
|
|
|
225
235
|
onactionsettled,
|
|
226
236
|
initialDraft,
|
|
227
237
|
composerPlaceholder,
|
|
238
|
+
composerDisabled = false,
|
|
228
239
|
pageTools,
|
|
229
240
|
clientToolPolicy,
|
|
230
241
|
onstatus,
|
|
@@ -232,6 +243,7 @@ const {
|
|
|
232
243
|
clientToolFilter,
|
|
233
244
|
maxPauseMs,
|
|
234
245
|
onrun,
|
|
246
|
+
onattentionchange,
|
|
235
247
|
choiceSources,
|
|
236
248
|
dictation,
|
|
237
249
|
transcribe,
|
|
@@ -319,6 +331,18 @@ $effect(() => {
|
|
|
319
331
|
return () => controller.dispose();
|
|
320
332
|
});
|
|
321
333
|
|
|
334
|
+
const attentionRequired = $derived(
|
|
335
|
+
controller.run?.waitingFor?.kind === 'choice' ||
|
|
336
|
+
controller.toolRequests.some((request) => request.status === 'waiting') ||
|
|
337
|
+
[...controller.actions.values()].some(
|
|
338
|
+
(action) => action.status === 'previewed' || action.outcomeUnknown,
|
|
339
|
+
),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
$effect(() => {
|
|
343
|
+
onattentionchange?.(attentionRequired);
|
|
344
|
+
});
|
|
345
|
+
|
|
322
346
|
// Finding B (#2904 review, third final pass): a SEPARATE effect, scoped to
|
|
323
347
|
// only the `registry` prop, so reassigning it (a host swapping
|
|
324
348
|
// tenant/workspace context) is actually observed — the getter passed to
|
|
@@ -585,7 +609,7 @@ async function handleConfirmAction(requestId: string) {
|
|
|
585
609
|
class="assistant-dock-layout"
|
|
586
610
|
data-threads-open={threadsOpen || undefined}
|
|
587
611
|
>
|
|
588
|
-
{#if conversations !== 'single' && threadList}
|
|
612
|
+
{#if presentation === 'full' && conversations !== 'single' && threadList}
|
|
589
613
|
<Button
|
|
590
614
|
type="button"
|
|
591
615
|
variant="ghost"
|
|
@@ -629,6 +653,23 @@ async function handleConfirmAction(requestId: string) {
|
|
|
629
653
|
</p>
|
|
630
654
|
{/if}
|
|
631
655
|
|
|
656
|
+
{#if controller.run && (presentation === 'controls' || controller.run.state === 'paused')}
|
|
657
|
+
<div class="assistant-dock-run" role="status" aria-live="polite">
|
|
658
|
+
<span>{controller.run.goal}</span>
|
|
659
|
+
{#if controller.run.state === 'running'}
|
|
660
|
+
<Button type="button" size="sm" variant="ghost" onclick={() => controller.pauseRun()}>
|
|
661
|
+
Pause
|
|
662
|
+
</Button>
|
|
663
|
+
{:else if controller.run.state === 'paused'}
|
|
664
|
+
<Button type="button" size="sm" onclick={() => controller.continueRun()}>
|
|
665
|
+
Continue
|
|
666
|
+
</Button>
|
|
667
|
+
{:else if controller.run.state === 'waiting' && controller.run.waitingFor}
|
|
668
|
+
<span>{controller.run.waitingFor.label ?? `Waiting for ${controller.run.waitingFor.kind}`}</span>
|
|
669
|
+
{/if}
|
|
670
|
+
</div>
|
|
671
|
+
{/if}
|
|
672
|
+
|
|
632
673
|
{#if conversations === 'single' && controller.activeThreadId && transport.createThread && controller.messages.length > 0}
|
|
633
674
|
<div class="assistant-dock-single-actions">
|
|
634
675
|
<Button
|
|
@@ -661,6 +702,7 @@ async function handleConfirmAction(requestId: string) {
|
|
|
661
702
|
{/if}
|
|
662
703
|
|
|
663
704
|
<div class="assistant-dock-scroll">
|
|
705
|
+
{#if presentation === 'full'}
|
|
664
706
|
{#if conversations === 'single'}
|
|
665
707
|
{#if !controller.activeThreadId && !controller.error}
|
|
666
708
|
<div
|
|
@@ -784,6 +826,8 @@ async function handleConfirmAction(requestId: string) {
|
|
|
784
826
|
</div>
|
|
785
827
|
{/if}
|
|
786
828
|
|
|
829
|
+
{/if}
|
|
830
|
+
|
|
787
831
|
{#each controller.toolRequests.filter((r) => r.status === 'waiting') as request (request.id)}
|
|
788
832
|
<div class="assistant-dock-tool-request" role="group" aria-label={t(M['chat.assistant_dock.tool_request_title'])}>
|
|
789
833
|
<p class="assistant-dock-tool-request-title">
|
|
@@ -913,6 +957,7 @@ async function handleConfirmAction(requestId: string) {
|
|
|
913
957
|
</div>
|
|
914
958
|
{/if}
|
|
915
959
|
|
|
960
|
+
{#if presentation === 'full'}
|
|
916
961
|
<div class="assistant-dock-composer">
|
|
917
962
|
{#if controller.models.length > 0}
|
|
918
963
|
<div class="assistant-dock-composer-header">
|
|
@@ -931,7 +976,7 @@ async function handleConfirmAction(requestId: string) {
|
|
|
931
976
|
}
|
|
932
977
|
onsend={handleSend}
|
|
933
978
|
onupload={transport.uploadAttachment ? handleUpload : undefined}
|
|
934
|
-
disabled={!controller.activeThreadId}
|
|
979
|
+
disabled={!controller.activeThreadId || composerDisabled}
|
|
935
980
|
placeholder={composerPlaceholder}
|
|
936
981
|
{dictation}
|
|
937
982
|
{transcribe}
|
|
@@ -944,6 +989,7 @@ async function handleConfirmAction(requestId: string) {
|
|
|
944
989
|
/>
|
|
945
990
|
{/key}
|
|
946
991
|
</div>
|
|
992
|
+
{/if}
|
|
947
993
|
</div>
|
|
948
994
|
</div>
|
|
949
995
|
</div>
|
|
@@ -978,6 +1024,16 @@ async function handleConfirmAction(requestId: string) {
|
|
|
978
1024
|
color: var(--smrt-color-on-surface-variant, #44474e);
|
|
979
1025
|
}
|
|
980
1026
|
|
|
1027
|
+
.assistant-dock-run {
|
|
1028
|
+
display: flex;
|
|
1029
|
+
align-items: center;
|
|
1030
|
+
justify-content: space-between;
|
|
1031
|
+
gap: 0.5rem;
|
|
1032
|
+
padding: 0.35rem 0.75rem;
|
|
1033
|
+
border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c7c5);
|
|
1034
|
+
font-size: var(--smrt-typography-body-medium-size, 0.85rem);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
981
1037
|
.assistant-dock-tool-request {
|
|
982
1038
|
margin: 0.5rem 0;
|
|
983
1039
|
padding: 0.75rem;
|
|
@@ -32,6 +32,11 @@ export interface Props {
|
|
|
32
32
|
surfaces?: DataSurfaceIdentity[];
|
|
33
33
|
/** Whether the dock is currently visible; polling pauses while false. */
|
|
34
34
|
visible?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* `controls` keeps live status and required decisions while omitting the
|
|
37
|
+
* transcript, thread list and composer for a host-owned conversation view.
|
|
38
|
+
*/
|
|
39
|
+
presentation?: 'full' | 'controls';
|
|
35
40
|
/** How the dock presents conversations. `multiple` (default) shows the
|
|
36
41
|
* conversation list and lets the person start new ones. `single` is for
|
|
37
42
|
* hosts where the person never needs to know about separate conversations:
|
|
@@ -81,6 +86,8 @@ export interface Props {
|
|
|
81
86
|
/** Placeholder for the composer's empty textarea (#2991). Defaults to the
|
|
82
87
|
* composer's own placeholder. */
|
|
83
88
|
composerPlaceholder?: string;
|
|
89
|
+
/** Disables text submission while a host-owned input has a pending turn. */
|
|
90
|
+
composerDisabled?: boolean;
|
|
84
91
|
/** The page's browser tools (#2908) — typically
|
|
85
92
|
* `installWebMcpPageToolRegistry()` from
|
|
86
93
|
* `@happyvertical/smrt-web/webmcp-page-tools`. Offered to the model each
|
|
@@ -103,6 +110,8 @@ export interface Props {
|
|
|
103
110
|
/** Called whenever the supervised run changes (#assistant-watch). Also
|
|
104
111
|
* readable as `controller.run`. */
|
|
105
112
|
onrun?: (run: AssistantRun | null) => void;
|
|
113
|
+
/** Reports whether a person must make an explicit decision. */
|
|
114
|
+
onattentionchange?: (required: boolean) => void;
|
|
106
115
|
/** Page features that can offer a few options for the person to pick
|
|
107
116
|
* (see `./assistant-choices.svelte.ts`). The options show as cards here;
|
|
108
117
|
* the person's click applies one. */
|