@happyvertical/smrt-chat 0.53.3 → 0.53.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -142,22 +142,22 @@ always waits. Details: [`docs/assistant-dock.md`](../../docs/assistant-dock.md).
142
142
 
143
143
  ## AssistantDock (#2904)
144
144
 
145
- `AssistantDock` (`svelte/components/assistant/`, exported from `./svelte`) is a
146
- shell-mounted, route-aware assistant surface: it discovers currently-mounted
147
- `DataSurfaceDescriptor`s from a host-supplied `DataSurfaceRegistry`
148
- (`@happyvertical/smrt-ui/data-surface`) and fails closed to plain chat when
149
- none are mounted. Action proposals flow through
150
- `normalizeDataSurfaceActionRequest` → a host-supplied `AssistantActionClient`
151
- (preview/apply against a server-hosted `DataSurfaceActionAdapter`, not through
152
- the `data-surface-bridge.ts` live-collaboration command channel) →
153
- `ToolCallDisplay`'s additive `actionResult` rendering. `AssistantTransport`
154
- (`assistant-transport.ts`) is a narrower, separate contract from
155
- `ChatClientBackend` (`client.ts`): `ChatThread`/`ChatMessage` only expose
156
- generated `list`/`get` REST (`api: { include: ['list', 'get'] }`,
157
- `models/ChatThread.ts:16`, `models/ChatMessage.ts:26`), so writes need a host-
158
- supplied `writeEndpoint`. See [`docs/assistant-dock.md`](../../docs/assistant-dock.md)
159
- for the full design, the anytown `PortalChatTool.svelte` polling/stale-send
160
- reconnaissance, and open gaps.
145
+ `AssistantDock` (`svelte/components/assistant/`, exported from `./svelte`)
146
+ uses a host `DataSurfaceRegistry`'s mounted descriptors (none: plain chat).
147
+ Actions: `normalizeDataSurfaceActionRequest` → `AssistantActionClient`
148
+ (preview/apply over a server `DataSurfaceActionAdapter`, not the
149
+ `data-surface-bridge.ts` channel). Generated `ChatThread`/`ChatMessage`
150
+ `list`/`get` are tenant- not member-scoped; the dock never calls them.
151
+
152
+ **Server routes (#3368).** `./sveltekit` `mountAssistantRoutes({ ai, ... })`
153
+ serves the dock from one `[...path]` route; `createAssistantHttpTransport`
154
+ and `createAssistantHttpActionClient` (`./svelte`) are its browser half.
155
+ Principal from `event.locals` (`user.id`, `user.profileId`, `tenantId`) or
156
+ `resolvePrincipal`; threads are served only from the actor's keyed assistant
157
+ session room in that tenant (else 404); `allowedTools` is fail-closed;
158
+ `ai` is injected, never read from env. `sendMessage`'s `clientRequestId`
159
+ makes the row id a UUIDv5 (`clientRequestMessageId`): the PK is the retry
160
+ reservation. Details: [`docs/assistant-dock.md`](../../docs/assistant-dock.md).
161
161
 
162
162
  ## Gotchas
163
163
 
package/README.md CHANGED
@@ -260,6 +260,21 @@ waiting | done | failed | cancelled`), with `pauseRun` / `continueRun`,
260
260
  `matchesToolAllowList` is exported browser-safe. Full guide:
261
261
  [`docs/assistant-dock.md`](../../docs/assistant-dock.md).
262
262
 
263
+ The server side mounts in one route file. The principal comes from
264
+ `event.locals` (`@happyvertical/smrt-users/sveltekit`); threads are scoped to
265
+ the caller's own assistant session in the active tenant; tools are
266
+ fail-closed (`allowedTools`, absent means none):
267
+
268
+ ```ts
269
+ // src/routes/api/assistant/[...path]/+server.ts
270
+ import { mountAssistantRoutes } from '@happyvertical/smrt-chat/sveltekit';
271
+ export const { GET, POST } = mountAssistantRoutes({ ai: assistantAI });
272
+ ```
273
+
274
+ In the page, `createAssistantHttpTransport({ endpoint: '/api/assistant' })`
275
+ is the dock's `transport`, and `createAssistantHttpActionClient` its
276
+ `actionClient` when the route is given `actions: { adapter }`.
277
+
263
278
  ## API
264
279
 
265
280
  ### Models
@@ -59,6 +59,12 @@ export interface AssistantTurnContinuation {
59
59
  tokens?: number;
60
60
  /** `Date.now()` when the turn started (for `maxTurnMs`). Absent: `createdAt`. */
61
61
  startedAt?: number;
62
+ /**
63
+ * The message that started the turn (`AssistantTurnOptions.originMessageId`),
64
+ * kept server-side so a resume restores it from here, never from the
65
+ * request.
66
+ */
67
+ originMessageId?: string;
62
68
  }
63
69
  /**
64
70
  * Where suspended turns wait. `take` is single-use: it returns the
@@ -69,7 +75,24 @@ export interface AssistantTurnContinuation {
69
75
  export interface AssistantContinuationStore {
70
76
  save(key: string, continuation: AssistantTurnContinuation): Promise<void>;
71
77
  take(key: string, id: string): Promise<AssistantTurnContinuation | null>;
78
+ /**
79
+ * Whether `id` is still waiting under `key` (present and unexpired, or
80
+ * claimed by a resume that is still activating), without consuming it.
81
+ * Optional; both built-in stores implement it.
82
+ */
83
+ has?(key: string, id: string): Promise<boolean>;
84
+ /**
85
+ * Forget a continuation `take` returned. The built-in stores' `take`
86
+ * CLAIMS the entry (no second `take` can return it, and `has` stays true)
87
+ * instead of deleting it, so a resume is never invisible between taking
88
+ * its continuation and recording that it runs; the runner calls `release`
89
+ * once that is recorded. An unreleased claim expires after
90
+ * {@link CONTINUATION_CLAIM_TTL_MS}. Optional.
91
+ */
92
+ release?(key: string, id: string): Promise<void>;
72
93
  }
94
+ /** How long a claimed (taken, unreleased) continuation still counts. */
95
+ export declare const CONTINUATION_CLAIM_TTL_MS: number;
73
96
  /** In-memory store, for tests and single-process demos. */
74
97
  export declare function createMemoryContinuationStore(options?: {
75
98
  ttlMs?: number;
@@ -135,6 +158,29 @@ export interface AssistantTurnOptions<M = Record<string, unknown>> {
135
158
  continuationId: string;
136
159
  results: ClientToolResultInput[];
137
160
  };
161
+ /**
162
+ * The message that started this turn (e.g. the user's send). Every reply
163
+ * the turn authors links to it (`replyToMessageId`), and a suspension keeps
164
+ * it with the continuation. Ignored on resume: the consumed continuation's
165
+ * value is used instead.
166
+ */
167
+ originMessageId?: string;
168
+ /**
169
+ * The turn's own lifecycle, reported (and awaited) by the runner at the
170
+ * moment each fact becomes true, whether or not anyone still reads the
171
+ * events: `running` when a fresh turn starts or a resume has consumed its
172
+ * continuation, `suspended` once the continuation is stored (before its
173
+ * `client_tool_calls` event), and `completed` / `cancelled` / `failed`
174
+ * when the leg ends (`completed` after the reply is stored). A throw is
175
+ * logged and never breaks the turn, except for a resume's `running`: when
176
+ * that report throws or returns `false` (not recorded), the leg stops
177
+ * before any model or tool call with `resume_not_recorded`, and its
178
+ * continuation stays claimed (claimable again after
179
+ * {@link CONTINUATION_CLAIM_TTL_MS}). Not called for a resume whose
180
+ * continuation is missing, foreign or expired. Resolve `false` (or throw)
181
+ * for "not recorded"; any other value counts as recorded.
182
+ */
183
+ onState?: (state: AssistantTurnState) => unknown;
138
184
  tools?: ManifestTool[];
139
185
  /** Server tools, narrowed to `principal.allowedTools`. */
140
186
  extraTools?: PrincipalTool[];
@@ -188,11 +234,25 @@ export interface AssistantTurnOptions<M = Record<string, unknown>> {
188
234
  onError?: AssistantTurnErrorLogger;
189
235
  now?: () => number;
190
236
  }
237
+ /**
238
+ * One lifecycle fact of a turn leg, for {@link AssistantTurnOptions.onState}.
239
+ * `resumedFrom` is the continuation this leg consumed (`null` for the first
240
+ * leg), so a recorder can refuse a write from a leg that is no longer
241
+ * current. `continuationId` is set for `suspended`.
242
+ */
243
+ export interface AssistantTurnState {
244
+ state: 'running' | 'suspended' | 'completed' | 'cancelled' | 'failed';
245
+ originMessageId?: string;
246
+ resumedFrom: string | null;
247
+ continuationId?: string;
248
+ }
191
249
  /** What {@link runAssistantTurn} returns when its generator completes. */
192
250
  export interface AssistantTurnResult {
193
251
  stoppedReason: AssistantTurnStopReason | 'client_tools' | 'error';
194
252
  content: string;
195
253
  loop?: ToolLoopResult;
254
+ /** The turn's origin message (from the continuation on a resume). */
255
+ originMessageId?: string;
196
256
  }
197
257
  /** What the browser is told when a turn fails unexpectedly. */
198
258
  export declare const ASSISTANT_TURN_GENERIC_ERROR = "The assistant ran into a problem. Please try again.";
@@ -232,5 +292,10 @@ export declare function createAssistantTurnResponse(events: AsyncGenerator<Assis
232
292
  headers?: Record<string, string>;
233
293
  /** Server-side log for a failure while pulling (default `console.error`). */
234
294
  onError?: AssistantTurnErrorLogger;
295
+ /**
296
+ * Called synchronously when the reader cancels the body (a client that
297
+ * went away), before the generator is closed — e.g. to abort the turn.
298
+ */
299
+ onCancel?: () => void;
235
300
  }): Response;
236
301
  //# sourceMappingURL=assistant-turn.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-turn.d.ts","sourceRoot":"","sources":["../src/assistant-turn.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"assistant-turn.d.ts","sourceRoot":"","sources":["../src/assistant-turn.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import { ObjectRegistry, SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
1
+ import { ObjectRegistry, SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, isPostgresDatabase, smrt } from "@happyvertical/smrt-core";
2
2
  import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
3
- import { ProfileCollection, resolveAgentProfileId } from "@happyvertical/smrt-profiles";
4
3
  import { createHash } from "node:crypto";
4
+ import { ProfileCollection, resolveAgentProfileId } from "@happyvertical/smrt-profiles";
5
5
  //#region \0rolldown/runtime.js
6
6
  var __defProp$7 = Object.defineProperty;
7
7
  var __exportAll = (all, no_symbols) => {
@@ -1231,9 +1231,37 @@ var VoiceSessionCollection = class extends SmrtCollection {
1231
1231
  //#endregion
1232
1232
  //#region src/services/ChatService.ts
1233
1233
  var ChatService_exports = /* @__PURE__ */ __exportAll({
1234
+ ChatClientRequestConflictError: () => ChatClientRequestConflictError,
1234
1235
  ChatService: () => ChatService,
1236
+ clientRequestMessageId: () => clientRequestMessageId,
1235
1237
  sendAgentReply: () => sendAgentReply
1236
1238
  });
1239
+ var CLIENT_REQUEST_NAMESPACE = "b7e2f1c4-5a3d-4e8f-9b6a-2c1d0e9f8a7b";
1240
+ function clientRequestMessageId(params) {
1241
+ const namespace = Buffer.from(CLIENT_REQUEST_NAMESPACE.replace(/-/g, ""), "hex");
1242
+ const name = Buffer.from(JSON.stringify([
1243
+ "smrt-chat:client-request",
1244
+ params.tenantId,
1245
+ params.roomId,
1246
+ params.threadId ?? null,
1247
+ params.actorProfileId,
1248
+ params.clientRequestId
1249
+ ]), "utf8");
1250
+ const bytes = createHash("sha1").update(namespace).update(name).digest().subarray(0, 16);
1251
+ bytes[6] = bytes[6] & 15 | 80;
1252
+ bytes[8] = bytes[8] & 63 | 128;
1253
+ const hex = bytes.toString("hex");
1254
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1255
+ }
1256
+ var ChatClientRequestConflictError = class extends Error {
1257
+ constructor(messageId) {
1258
+ super("A message for this client request is already stored.");
1259
+ this.messageId = messageId;
1260
+ this.name = "ChatClientRequestConflictError";
1261
+ }
1262
+ messageId;
1263
+ code = "CHAT_CLIENT_REQUEST_CONFLICT";
1264
+ };
1237
1265
  var RUN_AGENT_REPLY = /* @__PURE__ */ Symbol("smrt-chat.runAgentReply");
1238
1266
  var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
1239
1267
  var ChatService = class ChatService {
@@ -1330,17 +1358,58 @@ var ChatService = class ChatService {
1330
1358
  * {@link ChatService.writeMessage} path.
1331
1359
  */
1332
1360
  async sendMessage(params) {
1333
- return this.#writeMessage({
1361
+ const id = params.clientRequestId ? clientRequestMessageId({
1334
1362
  tenantId: params.tenantId,
1335
1363
  roomId: params.roomId,
1336
- senderProfileId: params.actorProfileId,
1337
- content: params.content,
1338
- role: "user",
1339
- messageType: params.messageType ?? "text",
1340
1364
  threadId: params.threadId ?? null,
1341
- agentSessionId: params.agentSessionId ?? null,
1342
- replyToMessageId: params.replyToMessageId ?? null
1343
- });
1365
+ actorProfileId: params.actorProfileId,
1366
+ clientRequestId: params.clientRequestId
1367
+ }) : void 0;
1368
+ const savepoint = id ? this.#reservationSavepoint() : null;
1369
+ if (savepoint) await savepoint.open();
1370
+ try {
1371
+ const message = await this.#writeMessage({
1372
+ tenantId: params.tenantId,
1373
+ roomId: params.roomId,
1374
+ senderProfileId: params.actorProfileId,
1375
+ content: params.content,
1376
+ role: "user",
1377
+ messageType: params.messageType ?? "text",
1378
+ threadId: params.threadId ?? null,
1379
+ agentSessionId: params.agentSessionId ?? null,
1380
+ replyToMessageId: params.replyToMessageId ?? null,
1381
+ ...id ? { id } : {},
1382
+ metadata: params.clientRequestId ? { clientRequestId: params.clientRequestId } : null,
1383
+ attachments: params.attachments?.length ? params.attachments : null
1384
+ });
1385
+ if (savepoint) await savepoint.release();
1386
+ return message;
1387
+ } catch (error) {
1388
+ if (savepoint) await savepoint.rollback();
1389
+ if (id && error?.code === "VALIDATION_UNIQUE_CONSTRAINT") throw new ChatClientRequestConflictError(id);
1390
+ throw error;
1391
+ }
1392
+ }
1393
+ /**
1394
+ * A savepoint around a reserved insert when the messages handle is a
1395
+ * PostgreSQL transaction handle (`@happyvertical/sql` transaction handles
1396
+ * have no `beginTransaction`), else `null`.
1397
+ */
1398
+ #reservationSavepoint() {
1399
+ const db = this.#messages.db;
1400
+ if (!isPostgresDatabase(db) || typeof db.beginTransaction === "function" || typeof db.query !== "function") return null;
1401
+ const name = `smrt_chat_reserve_${createHash("sha1").update(`${Date.now()}:${Math.random()}`).digest("hex").slice(0, 12)}`;
1402
+ return {
1403
+ open: async () => {
1404
+ await db.query(`SAVEPOINT ${name}`);
1405
+ },
1406
+ release: async () => {
1407
+ await db.query(`RELEASE SAVEPOINT ${name}`);
1408
+ },
1409
+ rollback: async () => {
1410
+ await db.query(`ROLLBACK TO SAVEPOINT ${name}`).catch(() => void 0);
1411
+ }
1412
+ };
1344
1413
  }
1345
1414
  /**
1346
1415
  * INTERNAL persistence path for all message writes. Not exposed publicly: it
@@ -1375,6 +1444,10 @@ var ChatService = class ChatService {
1375
1444
  })) throw new Error("replyToMessageId does not belong to this room/tenant (authorization denied)");
1376
1445
  }
1377
1446
  const message = await this.#messages.create({
1447
+ ...write.id ? {
1448
+ id: write.id,
1449
+ _insertOnly: true
1450
+ } : {},
1378
1451
  tenantId: write.tenantId,
1379
1452
  roomId: write.roomId,
1380
1453
  senderProfileId: write.senderProfileId,
@@ -1384,7 +1457,9 @@ var ChatService = class ChatService {
1384
1457
  threadId: write.threadId ?? null,
1385
1458
  agentSessionId: write.agentSessionId ?? null,
1386
1459
  replyToMessageId: write.replyToMessageId ?? null,
1387
- toolCallData: write.toolCallData ? JSON.stringify(write.toolCallData) : null
1460
+ toolCallData: write.toolCallData ? JSON.stringify(write.toolCallData) : null,
1461
+ ...write.metadata ? { metadata: JSON.stringify(write.metadata) } : {},
1462
+ ...write.attachments ? { attachments: JSON.stringify(write.attachments) } : {}
1388
1463
  });
1389
1464
  const room = await this.#rooms.get({
1390
1465
  id: write.roomId,
@@ -1754,6 +1829,7 @@ var ChatService = class ChatService {
1754
1829
  messageType: params.messageType ?? "text",
1755
1830
  threadId: params.threadId ?? null,
1756
1831
  agentSessionId: params.agentSessionId,
1832
+ replyToMessageId: params.replyToMessageId ?? null,
1757
1833
  toolCallData: params.toolCallData ?? null
1758
1834
  });
1759
1835
  }
@@ -1963,6 +2039,89 @@ var ChatService = class ChatService {
1963
2039
  })).reverse();
1964
2040
  }
1965
2041
  /**
2042
+ * A stored message and the replies linked to it (`replyToMessageId`),
2043
+ * oldest first (#3368), or `null` when the message is not in the thread.
2044
+ * Tenant- and membership-bound like {@link ChatService.getThreadMessages}.
2045
+ * Replies are found by their link, not by position, so overlapping turns
2046
+ * in one thread never claim each other's replies.
2047
+ */
2048
+ async getThreadMessageReplies(params) {
2049
+ const thread = await this.#threads.get({
2050
+ id: params.threadId,
2051
+ tenantId: params.tenantId
2052
+ });
2053
+ if (!thread) throw new Error("Thread not found");
2054
+ await this.#requireActiveMembership(thread.roomId, params.actorProfileId, params.tenantId);
2055
+ const message = await this.#messages.get({
2056
+ id: params.messageId,
2057
+ threadId: params.threadId,
2058
+ tenantId: params.tenantId
2059
+ });
2060
+ if (!message || message.isDeleted) return null;
2061
+ return {
2062
+ message,
2063
+ replies: await this.#messages.list({
2064
+ where: {
2065
+ tenantId: params.tenantId,
2066
+ threadId: params.threadId,
2067
+ replyToMessageId: params.messageId,
2068
+ isDeleted: false
2069
+ },
2070
+ orderBy: "created_at ASC",
2071
+ limit: params.limit ?? 100
2072
+ })
2073
+ };
2074
+ }
2075
+ /**
2076
+ * Record where the turn answering a client request stands, on the actor's
2077
+ * own user message (`metadata.turnOutcome`, #3368). Only the message's
2078
+ * author, still a member of its room, can record it.
2079
+ *
2080
+ * Transitions only move forward, per leg, and are compare-and-set on the
2081
+ * row's revision (a concurrent writer re-reads and is re-checked):
2082
+ * `unset → running → suspended(c) → running(resumedFrom c) → … →
2083
+ * completed | cancelled | failed`, plus `unset → suspended | terminal` for
2084
+ * a first leg whose earlier write was lost. A write from a leg that is not
2085
+ * the current one (its `resumedFrom` differs), a `running` that does not
2086
+ * consume the waiting continuation, or anything after a terminal outcome
2087
+ * is refused. Returns whether the write was applied.
2088
+ */
2089
+ async recordClientRequestOutcome(params) {
2090
+ for (let attempt = 0; attempt < 5; attempt += 1) {
2091
+ const message = await this.#messages.get({
2092
+ id: params.messageId,
2093
+ threadId: params.threadId,
2094
+ tenantId: params.tenantId
2095
+ });
2096
+ if (message?.role !== "user" || message.senderProfileId !== params.actorProfileId) throw new Error("Message not found");
2097
+ await this.#requireActiveMembership(message.roomId, params.actorProfileId, params.tenantId);
2098
+ const metadata = message.getMetadata();
2099
+ if (!ChatService.#outcomeTransitionAllowed(metadata, params.outcome, params.resumedFrom ?? null, params.continuationId ?? null)) return false;
2100
+ message.setMetadata({
2101
+ ...metadata,
2102
+ turnOutcome: params.outcome,
2103
+ turnSettledAt: (/* @__PURE__ */ new Date()).toISOString(),
2104
+ turnContinuationId: params.outcome === "suspended" ? params.continuationId ?? null : params.resumedFrom ?? null
2105
+ });
2106
+ try {
2107
+ await message.save();
2108
+ return true;
2109
+ } catch (error) {
2110
+ if (error?.code !== "RUNTIME_REVISION_CONFLICT") throw error;
2111
+ }
2112
+ }
2113
+ return false;
2114
+ }
2115
+ static #outcomeTransitionAllowed(metadata, next, resumedFrom, continuationId) {
2116
+ const current = typeof metadata.turnOutcome === "string" ? metadata.turnOutcome : null;
2117
+ const recordedId = typeof metadata.turnContinuationId === "string" ? metadata.turnContinuationId : null;
2118
+ if (current === "completed" || current === "cancelled" || current === "failed") return false;
2119
+ if (next === "running") return resumedFrom === null ? current === null : current === "suspended" && recordedId === resumedFrom;
2120
+ if (next === "suspended" && !continuationId) return false;
2121
+ if (current === "running") return recordedId === resumedFrom;
2122
+ return current === null && resumedFrom === null;
2123
+ }
2124
+ /**
1966
2125
  * Update the per-session agent configuration (allowedTools / systemPrompt),
1967
2126
  * restricted to the session owner — the room owner participant (S5 #1392).
1968
2127
  *
@@ -2015,6 +2174,6 @@ function sendAgentReply(service, params) {
2015
2174
  return ChatService[RUN_AGENT_REPLY](service, params);
2016
2175
  }
2017
2176
  //#endregion
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 };
2177
+ export { sendAgentReply as a, ChatThread as c, ChatParticipant as d, ChatMessage as f, clientRequestMessageId as i, ChatRoom as l, ChatService as n, VoiceSessionCollection as o, AgentSession as p, ChatService_exports as r, VoiceSession as s, ChatClientRequestConflictError as t, ChatReaction as u };
2019
2178
 
2020
- //# sourceMappingURL=ChatService-jJi2JJJi.js.map
2179
+ //# sourceMappingURL=ChatService-Dl5tsK_F.js.map