@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.
- package/AGENTS.md +14 -3
- package/agents/dev-workbench.md +71 -0
- package/dist/chunks/{ChatService-B2piYas4.js → ChatService-D48KBDZy.js} +110 -7
- package/dist/chunks/ChatService-D48KBDZy.js.map +1 -0
- package/dist/collections/AgentSessionCollection.d.ts +2 -0
- package/dist/collections/AgentSessionCollection.d.ts.map +1 -1
- package/dist/index.js +10 -7
- package/dist/index.js.map +1 -1
- package/dist/internal/agent-runtime.js +1 -1
- package/dist/manifest.json +34 -5
- package/dist/models/AgentSession.d.ts +15 -0
- package/dist/models/AgentSession.d.ts.map +1 -1
- package/dist/persona-conversation.d.ts +6 -0
- package/dist/persona-conversation.d.ts.map +1 -1
- package/dist/services/ChatService.d.ts +16 -1
- package/dist/services/ChatService.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +68 -15
- package/dist/svelte/components/assistant/AssistantComposer.svelte +2 -0
- package/dist/svelte/components/assistant/AssistantDock.svelte +339 -144
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +18 -3
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -1
- package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +121 -1
- package/dist/svelte/components/assistant/__tests__/assistant-dock-action-outcome.test.js +320 -0
- package/dist/svelte/components/assistant/__tests__/assistant-dock-host-actions.test.js +185 -0
- package/dist/svelte/components/assistant/__tests__/create-assistant-dock-controller.test.js +10 -11
- package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts +30 -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 +0 -0
- package/dist/svelte/i18n.d.ts +3 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +3 -0
- package/dist/svelte/index.d.ts +1 -1
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +1 -1
- package/dist/tool-loop.d.ts +7 -0
- package/dist/tool-loop.d.ts.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/voice.d.ts.map +1 -1
- package/package.json +27 -21
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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-
|
|
2020
|
+
//# sourceMappingURL=ChatService-D48KBDZy.js.map
|