@happyvertical/smrt-chat 0.51.9 → 0.51.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AGENTS.md +14 -3
  2. package/agents/dev-workbench.md +71 -0
  3. package/dist/chunks/{ChatService-B2piYas4.js → ChatService-D48KBDZy.js} +110 -7
  4. package/dist/chunks/ChatService-D48KBDZy.js.map +1 -0
  5. package/dist/collections/AgentSessionCollection.d.ts +2 -0
  6. package/dist/collections/AgentSessionCollection.d.ts.map +1 -1
  7. package/dist/index.js +10 -7
  8. package/dist/index.js.map +1 -1
  9. package/dist/internal/agent-runtime.js +1 -1
  10. package/dist/manifest.json +34 -5
  11. package/dist/models/AgentSession.d.ts +15 -0
  12. package/dist/models/AgentSession.d.ts.map +1 -1
  13. package/dist/persona-conversation.d.ts +6 -0
  14. package/dist/persona-conversation.d.ts.map +1 -1
  15. package/dist/services/ChatService.d.ts +16 -1
  16. package/dist/services/ChatService.d.ts.map +1 -1
  17. package/dist/smrt-knowledge.json +68 -15
  18. package/dist/svelte/components/assistant/AssistantComposer.svelte +2 -0
  19. package/dist/svelte/components/assistant/AssistantDock.svelte +339 -144
  20. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +18 -3
  21. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -1
  22. package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +121 -1
  23. package/dist/svelte/components/assistant/__tests__/assistant-dock-action-outcome.test.js +320 -0
  24. package/dist/svelte/components/assistant/__tests__/assistant-dock-host-actions.test.js +185 -0
  25. package/dist/svelte/components/assistant/__tests__/create-assistant-dock-controller.test.js +10 -11
  26. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts +30 -0
  27. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts.map +1 -1
  28. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.js +0 -0
  29. package/dist/svelte/i18n.d.ts +3 -0
  30. package/dist/svelte/i18n.d.ts.map +1 -1
  31. package/dist/svelte/i18n.js +3 -0
  32. package/dist/svelte/index.d.ts +1 -1
  33. package/dist/svelte/index.d.ts.map +1 -1
  34. package/dist/svelte/index.js +1 -1
  35. package/dist/tool-loop.d.ts +7 -0
  36. package/dist/tool-loop.d.ts.map +1 -1
  37. package/dist/types.d.ts +2 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/voice.d.ts.map +1 -1
  40. package/package.json +27 -21
  41. package/dist/chunks/ChatService-B2piYas4.js.map +0 -1
package/AGENTS.md CHANGED
@@ -13,6 +13,14 @@ through `createChatStreamHandler` in plain mode, so an embedded `SmrtChatBackend
13
13
  client can exercise token streaming locally. `/previews` hosts the shared
14
14
  component playground entries from `src/svelte/playground.ts`.
15
15
 
16
+ The root route also drives the **browser inference path**
17
+ (`@happyvertical/smrt-web/ai`: bitgpu, WebLLM, and route backends composed by
18
+ `createInferencePath`) and an **agent-addressable draft form** (`useViewIntent`
19
+ over `src/routes/chat-dev.intents.ts`, #2588). Their dev-config dependencies
20
+ (static optional-peer imports, `workspace-aliases.js` prefix rules, the theme
21
+ stylesheet import) and the stage→apply tool-call gotchas are in
22
+ [agents/dev-workbench.md](agents/dev-workbench.md).
23
+
16
24
  The root workbench also has a dev-only voice conversation mode. It reads voice
17
25
  gateway connection details through `/api/dev-voice/config`, streams browser mic
18
26
  audio to `WS /ws/voice` as PCM16 mono, appends gateway transcripts/responses to
@@ -37,7 +45,7 @@ by the module-local `sendAgentReply` function.
37
45
  - **ChatParticipant**: `role` (owner/admin/member/viewer), `onlineStatus`, `lastReadMessageId`, `isMuted`. Tenant-scoped (required).
38
46
  - **ChatThread**: `rootMessageId`, `isResolved`, `messageCount`. Created via `ChatService.startThread()` (member-checked). Tenant-scoped (required).
39
47
  - **ChatReaction**: `messageId`, `profileId`, `emoji`. Added/removed via `ChatService.addReaction()`/`removeReaction()` (member-checked, self-keyed). Tenant-scoped (required).
40
- - **AgentSession**: `agentId` (string ref, not FK), `allowedTools` (JSON string array), `sessionContext` (JSON), `systemPrompt`, limits (`maxTokens`/`maxMessages`/`expiresAt`). Optional tenancy.
48
+ - **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), `systemPrompt`, limits (`maxTokens`/`maxMessages`/`expiresAt`). Optional tenancy.
41
49
  - **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()`.
42
50
 
43
51
  ## ChatService
@@ -69,7 +77,8 @@ membership of the server-supplied actor, not a caller-selected subject.
69
77
 
70
78
  Trusted agent runtime imports `sendAgentReply(service, params)` only from
71
79
  `@happyvertical/smrt-chat/internal/agent-runtime`. It is not a `ChatService`
72
- method or package-index export. It authors as `session.agentId`, accepts a
80
+ method or package-index export. It authors as `session.agentProfileId` — the
81
+ agent's resolved `bot` Profile, NEVER the `agentId` slug (#2995) — accepts a
73
82
  same-room/tenant thread, and enforces `allowedTools` fail-closed.
74
83
 
75
84
  Private `#writeMessage` alone can select arbitrary authors/roles or bypass
@@ -94,7 +103,7 @@ The `AgentSession` runtime depends on personas, agents, and users. Keep those
94
103
  edges acyclic: none of those packages may depend back on chat.
95
104
 
96
105
  - **`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.
97
- - **`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.
106
+ - **`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).
98
107
  - **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.
99
108
  - **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.
100
109
 
@@ -137,6 +146,8 @@ reconnaissance, and open gaps.
137
146
 
138
147
  - **sessionContext, not context**: `context` is reserved for slug scoping. Use `getSessionContext()`/`updateSessionContext()` for agent memory.
139
148
  - **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.
149
+ - **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.
150
+ - **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 is NOT part of required PR CI — it runs in `postgres-tests.yml`, gated on `vars.CI_POSTGRES_ENABLED` / manual dispatch — so keep a cheap SQLite assertion alongside it (`chat-service.test.ts` asserts the author is the profile uuid, not the slug).
140
151
  - **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.
141
152
  - **Session expiry**: check `isActive()` before allowing messages (expiresAt or limit-based)
142
153
  - **DM identity**: derived from the deterministic per-tenant `canonicalDmRoomId()` and the authoritative `chat_participants` join, not client metadata; concurrent creates upsert onto one row.
@@ -0,0 +1,71 @@
1
+ # Chat dev workbench: browser inference and draft form
2
+
3
+ Reference for the `pnpm --dir packages/chat dev` workbench root route
4
+ (`src/routes/+page.svelte`). Linked from [../AGENTS.md](../AGENTS.md).
5
+
6
+ ## Browser inference path
7
+
8
+ The root route drives the **browser inference path**
9
+ (`@happyvertical/smrt-web/ai`) end to end, which is what the workbench exists to
10
+ exercise now: THREE backends — `createBitGpuInferenceBackend()` (Bonsai 1.7B,
11
+ ~237 MB, the dev-loop default), `createWebLlmInferenceBackend()`, and
12
+ `createRouteInferenceBackend()` over `/api/dev-chat-stream` — composed by
13
+ `createInferencePath({ backends: [bitgpu, local, route] })`. The local backends
14
+ are listed first, so `auto` prefers one whenever it is READY; the route backend is
15
+ always ready, so `auto` always has somewhere to land. The status panel's Inference
16
+ selector pins `auto` / `bitgpu` / `local` / `route`, it renders a
17
+ `ModelStatusControl` per local backend, and the reply is rendered as it streams.
18
+ `/api/dev-chat` is retained as the non-streaming reference endpoint; the workbench
19
+ no longer calls it.
20
+
21
+ Four dev-config facts this depends on:
22
+
23
+ - The workbench injects `loadModule: () => import('@mlc-ai/web-llm')` and, for
24
+ bitgpu, `loadBitGpu`/`loadChat` as `() => import('bitgpu')` /
25
+ `() => import('bitgpu/chat')`. A browser resolves only STATIC specifiers — the
26
+ adapters' own optional-import helpers use variable ones — so without these the
27
+ optional peers fail to load and (for WebLLM) the error misreports the package as
28
+ "not installed".
29
+ - bitgpu's per-model `manifest.json` + `.aux.bin` come from its repo via jsDelivr;
30
+ the Bonsai GGUF streams from the Hub; the tokenizer comes from
31
+ `onnx-community/Bonsai-1.7B-ONNX` because `prism-ml/Bonsai-1.7B-gguf` ships no
32
+ `tokenizer.json`.
33
+ - `workspace-aliases.js` must list every deep `@happyvertical/smrt-ui/*` subpath
34
+ a consumer reaches. A string alias is a PREFIX match, so the bare
35
+ `@happyvertical/smrt-ui` entry rewrites any unlisted subpath under it into a
36
+ bogus path under `src/index.ts`. That includes STYLESHEETS:
37
+ `@happyvertical/smrt-ui/themes/styles/all.css` needs its own entry or the app
38
+ renders with no theme variables at all.
39
+ - The workbench imports `@happyvertical/smrt-ui/themes/styles/{all,fonts}.css`.
40
+ `ThemeProvider` for a BUILT-IN preset only sets `data-theme` /
41
+ `data-color-scheme` on its wrapper — it deliberately emits no inline variables,
42
+ because built-ins ship their palette as static CSS. Without the import the
43
+ attributes are present, `--smrt-color-*` resolves to nothing, and every themed
44
+ surface paints transparent (which reads as "dark mode is broken" in a dark
45
+ scheme). `smrt-workbench/host` is the reference for this.
46
+
47
+ ## Agent-addressable draft form
48
+
49
+ The workbench also carries an **agent-addressable draft form**: an `Input` inside
50
+ a `Form`, bound by `useViewIntent` to the intent declared in
51
+ `src/routes/chat-dev.intents.ts` (#2588). It is the end-to-end demonstration of the
52
+ control-interaction stack — an agent stages, a human applies. Four things about it
53
+ are non-obvious and cost time to find:
54
+
55
+ - **No `<Provider>` here, so both seams must be explicit.** `useViewIntent` is a
56
+ documented silent no-op without a Provider ancestor, so the binding passes
57
+ `controlRegistry` directly; and `effects: ['read', 'write']` supplies the
58
+ exposure policy, because the default is read-only and would exclude a `stage`.
59
+ - **`inputSchema` on the declaration is REQUIRED for the tool to be callable.**
60
+ `compileViewIntentToolSpec` passes it through verbatim, but
61
+ `buildControlCommand` throws `IntentArgumentError('value')` for a `stage` with
62
+ no `value`. Without it the tool registers, advertises
63
+ `{"type":"object","properties":{}}`, and fails at call time with Chrome's
64
+ `UnknownError: Failed to parse input arguments`.
65
+ - **`document.modelContext.executeTool(tool, args)` takes the tool OBJECT from
66
+ `getTools()` and the arguments as a JSON STRING** — an object as the second
67
+ argument fails the same way.
68
+ - **Apply needs a genuinely trusted gesture.** The registry refuses a
69
+ programmatic `element.click()` (`event.isTrusted === false`) for `apply`, which
70
+ is the stage→apply consent split doing its job. Verification must use a real
71
+ input event, not `evaluate(() => button.click())`.
@@ -1,5 +1,6 @@
1
1
  import { ObjectRegistry, SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
2
2
  import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
3
+ import { ProfileCollection, resolveAgentProfileId } from "@happyvertical/smrt-profiles";
3
4
  import { createHash } from "node:crypto";
4
5
  //#region \0rolldown/runtime.js
5
6
  var __defProp$7 = Object.defineProperty;
@@ -32,6 +33,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
32
33
  var AgentSession = class extends SmrtObject {
33
34
  tenantId = null;
34
35
  agentId = "";
36
+ agentProfileId = null;
35
37
  participantProfileId = "";
36
38
  chatRoomId = null;
37
39
  status = "active";
@@ -49,6 +51,7 @@ var AgentSession = class extends SmrtObject {
49
51
  super(options);
50
52
  if (options.tenantId !== void 0) this.tenantId = options.tenantId;
51
53
  if (options.agentId !== void 0) this.agentId = options.agentId;
54
+ if (options.agentProfileId !== void 0) this.agentProfileId = options.agentProfileId;
52
55
  if (options.participantProfileId !== void 0) this.participantProfileId = options.participantProfileId;
53
56
  if (options.chatRoomId !== void 0) this.chatRoomId = options.chatRoomId;
54
57
  if (options.status !== void 0) this.status = options.status;
@@ -154,6 +157,7 @@ var AgentSession = class extends SmrtObject {
154
157
  __publicField(AgentSession, "SESSION_KEY_CONTEXT_FIELD", "__sessionKey");
155
158
  __decorateClass$6([tenantId({ nullable: true })], AgentSession.prototype, "tenantId", 2);
156
159
  __decorateClass$6([field({ required: true })], AgentSession.prototype, "agentId", 2);
160
+ __decorateClass$6([crossPackageRef("@happyvertical/smrt-profiles:Profile")], AgentSession.prototype, "agentProfileId", 2);
157
161
  __decorateClass$6([crossPackageRef("@happyvertical/smrt-profiles:Profile", { required: true })], AgentSession.prototype, "participantProfileId", 2);
158
162
  __decorateClass$6([foreignKey("ChatRoom")], AgentSession.prototype, "chatRoomId", 2);
159
163
  __decorateClass$6([field({ required: true })], AgentSession.prototype, "status", 2);
@@ -732,6 +736,7 @@ var AgentSessionCollection = class extends SmrtCollection {
732
736
  const sessionContext = params.sessionKey != null ? JSON.stringify({ [AgentSession.SESSION_KEY_CONTEXT_FIELD]: params.sessionKey }) : void 0;
733
737
  return await this.create({
734
738
  agentId: params.agentId,
739
+ agentProfileId: params.agentProfileId ?? null,
735
740
  participantProfileId: params.participantProfileId,
736
741
  tenantId: params.tenantId ?? null,
737
742
  allowedTools: JSON.stringify(params.allowedTools ?? []),
@@ -1230,6 +1235,7 @@ var ChatService_exports = /* @__PURE__ */ __exportAll({
1230
1235
  sendAgentReply: () => sendAgentReply
1231
1236
  });
1232
1237
  var RUN_AGENT_REPLY = /* @__PURE__ */ Symbol("smrt-chat.runAgentReply");
1238
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
1233
1239
  var ChatService = class ChatService {
1234
1240
  #rooms;
1235
1241
  #messages;
@@ -1238,7 +1244,13 @@ var ChatService = class ChatService {
1238
1244
  #agentSessions;
1239
1245
  #reactions;
1240
1246
  #voiceSessions;
1241
- constructor(rooms, messages, participants, threads, agentSessions, reactions, voiceSessions) {
1247
+ /**
1248
+ * Profiles of the OWNING package (`@happyvertical/smrt-profiles`), used only
1249
+ * to resolve the `bot` Profile an agent authors as (#2995). Private for the
1250
+ * same reason as the chat collections: it can mint/read profile rows.
1251
+ */
1252
+ #profiles;
1253
+ constructor(rooms, messages, participants, threads, agentSessions, reactions, voiceSessions, profiles) {
1242
1254
  this.#rooms = rooms;
1243
1255
  this.#messages = messages;
1244
1256
  this.#participants = participants;
@@ -1246,6 +1258,7 @@ var ChatService = class ChatService {
1246
1258
  this.#agentSessions = agentSessions;
1247
1259
  this.#reactions = reactions;
1248
1260
  this.#voiceSessions = voiceSessions;
1261
+ this.#profiles = profiles;
1249
1262
  }
1250
1263
  static async create(options) {
1251
1264
  ObjectRegistry.registerCollection("@happyvertical/smrt-chat:ChatRoom", ChatRoomCollection);
@@ -1262,7 +1275,8 @@ var ChatService = class ChatService {
1262
1275
  const agentSessions = await ObjectRegistry.getCollection("@happyvertical/smrt-chat:AgentSession", options);
1263
1276
  const reactions = await ObjectRegistry.getCollection("@happyvertical/smrt-chat:ChatReaction", options);
1264
1277
  const voiceSessions = await ObjectRegistry.getCollection("@happyvertical/smrt-chat:VoiceSession", options);
1265
- return new ChatService(rooms, messages, participants, threads, agentSessions, reactions, voiceSessions);
1278
+ const profiles = await ProfileCollection.create(options);
1279
+ return new ChatService(rooms, messages, participants, threads, agentSessions, reactions, voiceSessions, profiles);
1266
1280
  }
1267
1281
  /** Initialize all underlying collections (table creation) */
1268
1282
  async initialize() {
@@ -1602,6 +1616,7 @@ var ChatService = class ChatService {
1602
1616
  async createAgentSession(params) {
1603
1617
  const participantProfileId = params.actorProfileId;
1604
1618
  const sessionKey = params.sessionKey ?? null;
1619
+ const suppliedAgentProfileId = params.agentProfileId ? await this.#requireAgentProfile(params.agentProfileId, params.tenantId, participantProfileId) : null;
1605
1620
  const existingSession = await this.#agentSessions.findActiveSession(params.agentId, participantProfileId, params.tenantId, sessionKey);
1606
1621
  if (existingSession && existingSession.tenantId === params.tenantId) {
1607
1622
  if (existingSession.chatRoomId) {
@@ -1616,10 +1631,14 @@ var ChatService = class ChatService {
1616
1631
  profileId: participantProfileId,
1617
1632
  role: "owner"
1618
1633
  });
1634
+ if (!existingSession.agentProfileId) if (suppliedAgentProfileId) {
1635
+ existingSession.agentProfileId = suppliedAgentProfileId;
1636
+ await existingSession.save();
1637
+ } else await this.#resolveAgentProfileId(existingSession);
1619
1638
  await this.#enrollParticipant({
1620
1639
  tenantId: params.tenantId,
1621
1640
  roomId: existingRoom.id,
1622
- profileId: params.agentId,
1641
+ profileId: existingSession.agentProfileId,
1623
1642
  role: "member"
1624
1643
  });
1625
1644
  return {
@@ -1630,6 +1649,10 @@ var ChatService = class ChatService {
1630
1649
  }
1631
1650
  await existingSession.expire();
1632
1651
  }
1652
+ const agentProfileId = suppliedAgentProfileId ?? await resolveAgentProfileId(this.#profiles, {
1653
+ agentId: params.agentId,
1654
+ tenantId: params.tenantId
1655
+ });
1633
1656
  const room = await this.#rooms.create({
1634
1657
  tenantId: params.tenantId,
1635
1658
  name: "",
@@ -1647,11 +1670,12 @@ var ChatService = class ChatService {
1647
1670
  await this.#enrollParticipant({
1648
1671
  tenantId: params.tenantId,
1649
1672
  roomId: room.id,
1650
- profileId: params.agentId,
1673
+ profileId: agentProfileId,
1651
1674
  role: "member"
1652
1675
  });
1653
1676
  const session = await this.#agentSessions.findOrCreate({
1654
1677
  agentId: params.agentId,
1678
+ agentProfileId,
1655
1679
  participantProfileId,
1656
1680
  tenantId: params.tenantId,
1657
1681
  allowedTools: params.allowedTools,
@@ -1695,7 +1719,8 @@ var ChatService = class ChatService {
1695
1719
  *
1696
1720
  * INTERNAL authority: this is the only path that authors a message as the
1697
1721
  * agent, and it is not reachable with a caller-supplied `senderProfileId`/
1698
- * `role`. The author is always `session.agentId`. Tool calls are gated
1722
+ * `role`. The author is always the session's resolved agent Profile
1723
+ * (`session.agentProfileId`), never the `agentId` slug (#2995). Tool calls are gated
1699
1724
  * fail-closed against the session's allow-list. Intended for the trusted
1700
1725
  * agent-runtime, never a per-tenant request handler driven by client-supplied
1701
1726
  * role/sender.
@@ -1713,10 +1738,17 @@ var ChatService = class ChatService {
1713
1738
  if (!toolName) throw new Error("Tool call is missing a tool name");
1714
1739
  if (!session.isToolAllowed(toolName)) throw new Error(`Tool '${toolName}' is not allowed for this agent session (authorization denied)`);
1715
1740
  }
1741
+ const agentProfileId = await this.#resolveAgentProfileId(session);
1742
+ await this.#enrollParticipant({
1743
+ tenantId: params.tenantId,
1744
+ roomId: session.chatRoomId,
1745
+ profileId: agentProfileId,
1746
+ role: "member"
1747
+ });
1716
1748
  return this.#writeMessage({
1717
1749
  tenantId: params.tenantId,
1718
1750
  roomId: session.chatRoomId,
1719
- senderProfileId: session.agentId,
1751
+ senderProfileId: agentProfileId,
1720
1752
  content: params.content,
1721
1753
  role,
1722
1754
  messageType: params.messageType ?? "text",
@@ -1725,6 +1757,77 @@ var ChatService = class ChatService {
1725
1757
  toolCallData: params.toolCallData ?? null
1726
1758
  });
1727
1759
  }
1760
+ /**
1761
+ * Validate a SERVER-supplied agent authoring Profile (S5 #1392, #2995).
1762
+ *
1763
+ * `agentProfileId` selects the author of every later assistant message in the
1764
+ * session and is enrolled as a room member, so it is held to the same
1765
+ * standard as `actorProfileId`: it must resolve to a Profile that is VISIBLE
1766
+ * IN THIS TENANT (the model uses optional tenancy, so an untenanted profile
1767
+ * is legitimate and a profile owned by another tenant is not), and it must
1768
+ * not be the acting participant — an agent room has exactly two identities.
1769
+ *
1770
+ * The `bot` profile type is deliberately NOT required: a persona's
1771
+ * `actsAsProfileId` is an ordinary `crossPackageRef` to `Profile` with no
1772
+ * type constraint anywhere in the framework, and demanding one here would
1773
+ * reject the framework's own acting identities.
1774
+ */
1775
+ async #requireAgentProfile(agentProfileId, tenantId, actorProfileId) {
1776
+ if (agentProfileId === actorProfileId) throw new Error("agentProfileId must not be the acting participant (authorization denied)");
1777
+ const profile = await this.#profiles.get({ id: agentProfileId });
1778
+ if (!profile) throw new Error("agentProfileId does not resolve to a profile (authorization denied)");
1779
+ if (profile.tenantId !== null && profile.tenantId !== tenantId) throw new Error("agentProfileId belongs to another tenant (authorization denied)");
1780
+ return agentProfileId;
1781
+ }
1782
+ /**
1783
+ * Resolve the Profile uuid an agent session's agent authors as (#2995).
1784
+ *
1785
+ * `agentId` is an application slug; every authoring seam is a uuid column
1786
+ * referencing `Profile`, so the slug can never be the author. The agent's
1787
+ * `bot` Profile is resolved — created on first use, tenant-bound — through
1788
+ * the owning package's `resolveAgentProfile()` API.
1789
+ *
1790
+ * Sessions created before #2995 carry no `agentProfileId`. They are backfilled
1791
+ * lazily here, on the first agent turn after the upgrade, so an existing
1792
+ * conversation keeps working without an offline migration.
1793
+ *
1794
+ * One pre-#2995 cohort must be ADOPTED rather than resolved: the voice path
1795
+ * used to collapse a persona's `actsAsProfileId` into `agentId`, so those
1796
+ * sessions carry a real Profile uuid there — and on PostgreSQL those rows
1797
+ * committed, because the uuid cast succeeded. Minting a fresh `bot` profile
1798
+ * for them would change the author mid-conversation, permanently (the
1799
+ * no-re-point rule), and enrol a third identity in a two-seat room. When
1800
+ * `agentId` already names a Profile visible in this tenant that is not the
1801
+ * session participant, that Profile IS the author and is adopted as-is.
1802
+ */
1803
+ async #resolveAgentProfileId(session) {
1804
+ if (session.agentProfileId) return session.agentProfileId;
1805
+ const agentProfileId = await this.#adoptLegacyAgentIdProfile(session) ?? await resolveAgentProfileId(this.#profiles, {
1806
+ agentId: session.agentId,
1807
+ tenantId: session.tenantId
1808
+ });
1809
+ session.agentProfileId = agentProfileId;
1810
+ await session.save();
1811
+ return agentProfileId;
1812
+ }
1813
+ /**
1814
+ * Adopt a pre-#2995 session whose `agentId` is already a Profile uuid, or
1815
+ * return `null` when it is an ordinary agent slug.
1816
+ *
1817
+ * Held to the same standard as a caller-supplied `agentProfileId`: the
1818
+ * Profile must be visible in the session's tenant and must not be the session
1819
+ * participant, so an `agentId` that happens to collide with a human profile
1820
+ * cannot make that human the author.
1821
+ */
1822
+ async #adoptLegacyAgentIdProfile(session) {
1823
+ const candidate = session.agentId;
1824
+ if (!UUID_PATTERN.test(candidate)) return null;
1825
+ if (candidate === session.participantProfileId) return null;
1826
+ const profile = await this.#profiles.get({ id: candidate });
1827
+ if (!profile) return null;
1828
+ if (profile.tenantId !== null && profile.tenantId !== session.tenantId) return null;
1829
+ return candidate;
1830
+ }
1728
1831
  /** Load an active agent session by id, tenant-bound, or throw. */
1729
1832
  async #loadActiveSession(agentSessionId, tenantId) {
1730
1833
  const session = await this.#agentSessions.get({
@@ -1914,4 +2017,4 @@ function sendAgentReply(service, params) {
1914
2017
  //#endregion
1915
2018
  export { VoiceSession as a, ChatReaction as c, AgentSession as d, VoiceSessionCollection as i, ChatParticipant as l, ChatService_exports as n, ChatThread as o, sendAgentReply as r, ChatRoom as s, ChatService as t, ChatMessage as u };
1916
2019
 
1917
- //# sourceMappingURL=ChatService-B2piYas4.js.map
2020
+ //# sourceMappingURL=ChatService-D48KBDZy.js.map