@tangle-network/agent-app 0.43.71 → 0.43.73

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.
@@ -186,6 +186,15 @@ declare function createAssistantDraftWriter(options: AssistantDraftWriterOptions
186
186
  * `updateMessage` a draft row could never be patched, so the caller keeps
187
187
  * today's exact single-write behavior. */
188
188
  declare function storeSupportsDraftPersistence(store: AssistantDraftStore): boolean;
189
+ /** The row id an `appendMessage` actually returned, or `null` when the store
190
+ * returned nothing usable. `ChatTurnMessageStore.appendMessage` is typed
191
+ * `Promise<unknown>` so a product adapter is free to resolve `void`; every
192
+ * caller that wants to NAME the row it just wrote has to read defensively.
193
+ *
194
+ * Deliberately no fallback to a caller-assigned id — see `writeOnce`, which
195
+ * adds its own. A caller that let the store mint the id has nothing to fall
196
+ * back TO, and guessing one would report a row that may not exist. */
197
+ declare function rowIdOf(inserted: unknown): string | null;
189
198
  /** The default deterministic assistant-row id for a turn. Readable on purpose
190
199
  * (an operator grepping a transcript row id finds the run), and stable across
191
200
  * re-entries because every input already is. */
@@ -213,24 +222,31 @@ declare function assistantRowIdForTurn(turnKey: string): string;
213
222
  * no router import. Auth/access is one injected `authorize` seam, composable
214
223
  * with `/app-auth` guards but not coupled to them.
215
224
  *
216
- * Six optional product seams let a complex turn-orchestrator compose the
217
- * vertical instead of hand-rolling a generator — each omittable to the exact
218
- * behavior above: `turnLock` (single-flight acquire/release around the turn),
225
+ * Optional product seams let a complex turn-orchestrator compose the vertical
226
+ * instead of hand-rolling a generator — each omittable to the exact behavior
227
+ * above: `turnLock` (single-flight acquire/release around the turn),
219
228
  * `contextGate` (pre-producer domain-readiness short-circuit), `beforeTurn`
220
229
  * (observe + augment the producer input), `lifecycle` (deterministic
221
230
  * start/complete/error telemetry), `heartbeat` (keepalive during silent
222
- * producer waits), plus `onRawEvent` (the raw producer events, for telemetry).
223
- * `handleChatTurn` stays the engine the seams only wrap its input, its
224
- * producer stream, and its settle.
231
+ * producer waits), and `onRawEvent` (the raw producer events, for telemetry)
232
+ * plus the `authorize` result's `insertUserMessage` flag (suppress the user row
233
+ * for a product-dispatched turn). `handleChatTurn` stays the engine — the seams
234
+ * only wrap its input, its producer stream, and its settle.
235
+ *
236
+ * Seam stability: all of the above are STABLE and safe to depend on. They
237
+ * graduated in #227 against the bar this package holds itself to — a seam is
238
+ * provisional until two INDEPENDENT consumers exercise it, because one
239
+ * consumer's shape is indistinguishable from that consumer's assumptions.
240
+ * `turnLock` cleared it with `/turn-stream`'s shared DO adapter (#221);
241
+ * `contextGate`, `beforeTurn`, `onRawEvent` and `insertUserMessage` each
242
+ * cleared it with two product verticals reading them differently — and the
243
+ * review found no leaked assumptions to fix, only `ChatRouteEvent` to export.
225
244
  *
226
- * Seam stability: `lifecycle`, `heartbeat`, and `turnLock` are generic and
227
- * stable (`turnLock` graduated with `/turn-stream`'s shared DO adapter, #221).
228
- * `contextGate`, `beforeTurn`, and `onRawEvent` are `@experimental` proven
229
- * by a single consumer (gtm's chat vertical, #200) and may change once a
230
- * second consumer exercises them. They stay FLAT top-level options (not
231
- * grouped under a `hooks` object): that grouping would break the shipped
232
- * consumer's call for no mechanism gain, and this package's exports are
233
- * additive-only.
245
+ * They stay FLAT top-level options (not grouped under a `hooks` object): that
246
+ * grouping would break every shipped consumer's call for no mechanism gain, and
247
+ * this package's exports are additive-only. For the same reason `onRawEvent`
248
+ * keeps its two-argument `(event, context)` signature rather than being
249
+ * normalized to the single-args shape the other seams take.
234
250
  */
235
251
 
236
252
  /** Usage receipt persisted onto the assistant message (the flattened
@@ -329,7 +345,14 @@ type ChatTurnAuthorization<TContext> = {
329
345
  * never overrides — the engine's retry-dedup: `authorize` runs before
330
346
  * turn identity is resolved, so it cannot tell a retry from a fresh turn;
331
347
  * a turn already deduped stays deduped. Omit / `true` → today's behavior.
332
- * @experimental Single-consumer; shape may change. */
348
+ *
349
+ * Settled shape (#227). The validated case in both consumers is the same:
350
+ * a durable plan-approval follow-up re-entering an execution the decision
351
+ * route already enqueued — a decision, not a typed message, so it must
352
+ * not surface a user bubble. Suppressing the insert also propagates:
353
+ * `ChatTurnProduceArgs.userMessageId` is `null` for the rest of the turn
354
+ * when there is no row to reuse, so a seam anchoring to the user row must
355
+ * handle that arm rather than assume a string. */
333
356
  insertUserMessage?: boolean;
334
357
  } | {
335
358
  ok: false;
@@ -360,9 +383,28 @@ interface ChatTurnProduceArgs<TContext> {
360
383
  /** The turn-buffer id announced to the client for replay. */
361
384
  turnStreamId: string;
362
385
  priorMessages: PersistedChatMessageForTurn[];
386
+ /** The durable `role:'user'` row this turn is anchored to — the row the
387
+ * factory just inserted, or the one retry-dedup REUSED. Products anchor
388
+ * optimistic-bubble swaps, retry targeting, and stop-polling to it, and it
389
+ * is the only way to name a REUSED row (which `priorMessages` excludes).
390
+ *
391
+ * Three states, all meaningful:
392
+ * - `undefined` — not resolved yet. `turnLock.acquire` is the one seam that
393
+ * sees this: it runs before any side effect by contract, so the row does
394
+ * not exist when it reads the args.
395
+ * - `null` — resolved, no row: `authorize` returned `insertUserMessage:
396
+ * false` on a turn with nothing to reuse, or the store's `appendMessage`
397
+ * resolved without a usable id.
398
+ * - a string — the row id, for `contextGate`, `beforeTurn`, and `produce`. */
399
+ userMessageId?: string | null;
363
400
  }
364
401
  /** One event as it crosses the route: the producer's own vocabulary, or an
365
- * injected keepalive. Same shape the engine forwards verbatim. */
402
+ * injected keepalive. Same shape the engine forwards verbatim.
403
+ *
404
+ * Public because it IS the vocabulary of two seams — `onRawEvent`'s parameter
405
+ * and `heartbeat.event`'s return. Exported so a product can declare a
406
+ * standalone handler (`function onRawEvent(e: ChatRouteEvent, ctx: T)`) rather
407
+ * than depending on contextual typing from an inline object literal. */
366
408
  type ChatRouteEvent = {
367
409
  type: string;
368
410
  data?: Record<string, unknown>;
@@ -434,6 +476,11 @@ interface ChatTurnLifecycleComplete<TContext> extends ChatTurnLifecycleBase<TCon
434
476
  /** Attribution for a downgrade: which models were tried and why each failed.
435
477
  * `undefined` when the producer reports no failover support. */
436
478
  modelFailover?: ChatTurnModelFailover;
479
+ /** The durable `role:'assistant'` row this turn wrote, or `null` when it
480
+ * wrote none (an empty turn leaves no row — a draft started mid-stream is
481
+ * retracted). The detached lane surfaces the same id as
482
+ * `DetachedTurnResult.messageId`; one contract, two lanes. */
483
+ assistantMessageId: string | null;
437
484
  }
438
485
  /** Represent an error occurring during a chat turn lifecycle with context and duration information */
439
486
  interface ChatTurnLifecycleError<TContext> extends ChatTurnLifecycleBase<TContext> {
@@ -451,6 +498,31 @@ interface ChatTurnLifecycle<TContext> {
451
498
  onTurnComplete?(info: ChatTurnLifecycleComplete<TContext>): void | Promise<void>;
452
499
  onTurnError?(info: ChatTurnLifecycleError<TContext>): void | Promise<void>;
453
500
  }
501
+ /** What a settled turn reports to `onTurnComplete` — the product's
502
+ * post-processing seam (billing, titles, audit). */
503
+ interface ChatTurnCompleteInput<TContext> {
504
+ identity: ChatTurnIdentity;
505
+ finalText: string;
506
+ context: TContext;
507
+ failed: boolean;
508
+ failureReason?: string;
509
+ /** The model that SERVED this turn. With failover wired it may differ from
510
+ * the requested one, so a product that bills or scores per model MUST read
511
+ * it here rather than assuming the model it asked for. */
512
+ model?: string;
513
+ /** Present when the producer supports failover: the full attempt trail, and
514
+ * `usedFallback` — the flag that makes a silent downgrade impossible. */
515
+ modelFailover?: ChatTurnModelFailover;
516
+ /** The durable `role:'assistant'` row this turn wrote, or `null` when it
517
+ * wrote none (an empty turn leaves no row).
518
+ *
519
+ * Populated even when `failed` is true — a terminal error event still
520
+ * persists whatever partial answer arrived, and that pairing is exactly
521
+ * what lets a product render an error row against a REAL message instead of
522
+ * hunting for the newest row in the thread. The detached lane surfaces the
523
+ * same id as `DetachedTurnResult.messageId`. */
524
+ assistantMessageId: string | null;
525
+ }
454
526
  /** Define options to configure chat turn routes including authorization, storage, and event buffering */
455
527
  interface CreateChatTurnRoutesOptions<TContext = void> {
456
528
  /** Names the product in `deriveExecutionId` so retries land on the same
@@ -479,11 +551,34 @@ interface CreateChatTurnRoutesOptions<TContext = void> {
479
551
  /** Pre-turn readiness gate that can short-circuit with a product `Response`
480
552
  * before the producer runs (the user row is already persisted). Runs after
481
553
  * `turnLock.acquire`, before `beforeTurn`. Omit → always proceed.
482
- * @experimental Single-consumer (gtm, #200); shape may change. */
554
+ *
555
+ * Settled shape (#227). What a consumer may depend on:
556
+ * - `args.userMessageId` is RESOLVED here — a string, or `null` when the
557
+ * insert was suppressed with nothing to reuse. (`turnLock.acquire` is the
558
+ * only seam that sees it `undefined`.)
559
+ * - `{proceed:false}` releases the lock and returns the product's `Response`
560
+ * verbatim: `beforeTurn` and `produce` never run, no assistant row is
561
+ * written, and the user row already inserted is KEPT — a real user turn
562
+ * whose assistant side is the gate's own response.
563
+ * - Always returning `{proceed:true}` is supported, not a misuse: the seam
564
+ * doubles as the one place that runs after the user row exists and before
565
+ * the producer, which is where per-turn analytics and readiness
566
+ * precomputation belong. A gate that never gates is a valid consumer. */
483
567
  contextGate?(args: ChatTurnProduceArgs<TContext>): ChatTurnGateResult | Promise<ChatTurnGateResult>;
484
568
  /** Observe the assembled producer input and optionally augment it (rewrite
485
569
  * the prompt / prior messages) before the producer runs. Omit → no change.
486
- * @experimental Single-consumer (gtm, #200); shape may change. */
570
+ *
571
+ * Settled shape (#227). BOTH return arms are contract:
572
+ * - a `ChatTurnInputPatch` shallow-merges over the route-assembled args, so
573
+ * an omitted field keeps the route's value;
574
+ * - `void` means "no patch" — and mutating `args.context` in place is the
575
+ * supported way to thread request-scoped state forward to `produce` /
576
+ * `lifecycle` / `onTurnComplete`, which all receive the same object.
577
+ *
578
+ * A throw propagates (the turn fails with the lock released). It runs BEFORE
579
+ * `lifecycle.onTurnStart`, so a throw here fires no terminal lifecycle hook —
580
+ * the span never opened. Telemetry for a failure in this seam belongs in the
581
+ * seam, not in `lifecycle`. */
487
582
  beforeTurn?(args: ChatTurnProduceArgs<TContext>): ChatTurnInputPatch | void | Promise<ChatTurnInputPatch | void>;
488
583
  /** Deterministic run telemetry (start / complete / error) with identity and
489
584
  * timing. Omit → no telemetry. */
@@ -494,7 +589,18 @@ interface CreateChatTurnRoutesOptions<TContext = void> {
494
589
  * before any heartbeat injection (the raw sidecar-producer events, for
495
590
  * telemetry). Never alters the stream; errors are swallowed. Distinct from
496
591
  * `onEvent`, which sees the engine-framed stream incl. lifecycle envelopes.
497
- * @experimental Single-consumer (gtm, #200); shape may change. */
592
+ *
593
+ * Settled shape (#227). What a consumer may depend on:
594
+ * - it sees EXACTLY the producer's own events — no engine lifecycle
595
+ * envelopes, and no injected keepalives (`heartbeat` wraps the stream
596
+ * downstream of this tap, so a synthetic event never reaches a trace);
597
+ * - a throw is caught and logged, never surfaced: a broken telemetry sink
598
+ * cannot fail a turn or truncate the client's stream;
599
+ * - it is an OBSERVER — the return value is ignored, and the event object
600
+ * continues downstream. Mutating it mutates the stream; don't.
601
+ *
602
+ * Takes `(event, context)` rather than one args object, matching both
603
+ * shipped consumers; see the module header on why it stays that way. */
498
604
  onRawEvent?(event: ChatRouteEvent, context: TContext): void | Promise<void>;
499
605
  /** Pre-persist transform of the final text (e.g. `/redact`'s `redactPII`).
500
606
  * Live stream is never altered. */
@@ -534,20 +640,7 @@ interface CreateChatTurnRoutesOptions<TContext = void> {
534
640
  * than billing an empty turn and marking it done. A turn that THROWS never
535
641
  * reaches this hook (the engine skips it on a producer throw). Errors are
536
642
  * swallowed by the engine — they never fail a streamed turn. */
537
- onTurnComplete?(input: {
538
- identity: ChatTurnIdentity;
539
- finalText: string;
540
- context: TContext;
541
- failed: boolean;
542
- failureReason?: string;
543
- /** The model that SERVED this turn. With failover wired it may differ from
544
- * the requested one, so a product that bills or scores per model MUST read
545
- * it here rather than assuming the model it asked for. */
546
- model?: string;
547
- /** Present when the producer supports failover: the full attempt trail, and
548
- * `usedFallback` — the flag that makes a silent downgrade impossible. */
549
- modelFailover?: ChatTurnModelFailover;
550
- }): Promise<void>;
643
+ onTurnComplete?(input: ChatTurnCompleteInput<TContext>): Promise<void>;
551
644
  /** Per-event side channel (product broadcast). The turn-buffer tap is
552
645
  * already wired; this runs in addition. */
553
646
  onEvent?(event: {
@@ -1517,4 +1610,4 @@ interface PromoteAgentFilePartOptions {
1517
1610
  /** Promote a part of an agent file with optional byte limits and MIME type detection */
1518
1611
  declare function promoteAgentFilePart(options: PromoteAgentFilePartOptions): Promise<PromoteFilePartResult>;
1519
1612
 
1520
- export { type AssistantDraftSnapshot, type AssistantDraftStore, type AssistantDraftWriter, type AssistantDraftWriterOptions, type AssistantRowValues, type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentUploadAuthorization, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, type ChatTurnModelFailover, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateAttachmentUploadRouteOptions, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DetachedTurnFinal, type DetachedTurnOptions, type DetachedTurnParts, type DetachedTurnResult, type DispatchPartsOutcome, type DraftPersistenceTuning, type DraftStoredMessage, type FilePartPromotionOutcome, type ModelFailoverStreamHandle, type ModelFailoverStreamOptions, type ModelFallbackInfo, type OpenModelStream, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, assistantRowIdForTurn, buildDispatchParts, bytesToBase64, classifyTerminalFailure, createAssistantDraftWriter, createAttachmentUploadRoute, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, isCommittingSandboxEvent, isDraftContentEvent, promoteAgentFilePart, resolveChatAttachments, runDetachedTurn, sanitizeUploadFilename, sniffMimeFromName, storeSupportsDraftPersistence, streamWithModelFailover, withDurableChatProjection };
1613
+ export { type AssistantDraftSnapshot, type AssistantDraftStore, type AssistantDraftWriter, type AssistantDraftWriterOptions, type AssistantRowValues, type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentUploadAuthorization, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatRouteEvent, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, type ChatTurnCompleteInput, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, type ChatTurnModelFailover, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateAttachmentUploadRouteOptions, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DetachedTurnFinal, type DetachedTurnOptions, type DetachedTurnParts, type DetachedTurnResult, type DispatchPartsOutcome, type DraftPersistenceTuning, type DraftStoredMessage, type FilePartPromotionOutcome, type ModelFailoverStreamHandle, type ModelFailoverStreamOptions, type ModelFallbackInfo, type OpenModelStream, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, assistantRowIdForTurn, buildDispatchParts, bytesToBase64, classifyTerminalFailure, createAssistantDraftWriter, createAttachmentUploadRoute, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, isCommittingSandboxEvent, isDraftContentEvent, promoteAgentFilePart, resolveChatAttachments, rowIdOf, runDetachedTurn, sanitizeUploadFilename, sniffMimeFromName, storeSupportsDraftPersistence, streamWithModelFailover, withDurableChatProjection };
@@ -31,7 +31,7 @@ import {
31
31
  normalizeClientTurnId,
32
32
  replayTurnEvents,
33
33
  resolveChatTurn
34
- } from "../chunk-YN7QR7MJ.js";
34
+ } from "../chunk-RK2MKLBT.js";
35
35
  import {
36
36
  createInteractionAnswerRoute
37
37
  } from "../chunk-3JUTOVUH.js";
@@ -216,8 +216,7 @@ function createAssistantDraftWriter(options) {
216
216
  role: "assistant",
217
217
  ...values
218
218
  });
219
- const insertedId = inserted?.id;
220
- rowId = typeof insertedId === "string" && insertedId ? insertedId : options.messageId;
219
+ rowId = rowIdOf(inserted) ?? options.messageId;
221
220
  writes += 1;
222
221
  }
223
222
  function trigger() {
@@ -284,6 +283,10 @@ function createAssistantDraftWriter(options) {
284
283
  function storeSupportsDraftPersistence(store) {
285
284
  return typeof store.updateMessage === "function";
286
285
  }
286
+ function rowIdOf(inserted) {
287
+ const id = inserted?.id;
288
+ return typeof id === "string" && id ? id : null;
289
+ }
287
290
  function assistantRowIdForTurn(turnKey) {
288
291
  return `assistant:${turnKey}`;
289
292
  }
@@ -464,6 +467,8 @@ function createChatTurnRoutes(options) {
464
467
  }
465
468
  let producer;
466
469
  let draft;
470
+ let assistantMessageId;
471
+ const assistantRowId = () => assistantMessageId ?? draft?.rowId() ?? null;
467
472
  let runFailed = false;
468
473
  let lastFailureData;
469
474
  let turnStartedAtMs = 0;
@@ -495,6 +500,7 @@ function createChatTurnRoutes(options) {
495
500
  durationMs,
496
501
  finalText: producer?.finalText() ?? "",
497
502
  usage: producer?.usage?.() ?? {},
503
+ assistantMessageId: assistantRowId(),
498
504
  ...producer?.model ? { model: producer.model } : {},
499
505
  ...failoverInfo ? { modelFailover: failoverInfo } : {}
500
506
  });
@@ -508,14 +514,18 @@ function createChatTurnRoutes(options) {
508
514
  };
509
515
  try {
510
516
  const insertUserMessage = chatTurn.shouldInsertUserMessage && (auth.insertUserMessage ?? true);
517
+ let userMessageId = chatTurn.reusedUserMessageId ?? null;
511
518
  if (insertUserMessage) {
512
- await options.store.appendMessage({
513
- threadId: payload.threadId,
514
- role: "user",
515
- content,
516
- parts: userPartsWithFiles(chatTurn.userParts, fileParts, mentions)
517
- });
519
+ userMessageId = rowIdOf(
520
+ await options.store.appendMessage({
521
+ threadId: payload.threadId,
522
+ role: "user",
523
+ content,
524
+ parts: userPartsWithFiles(chatTurn.userParts, fileParts, mentions)
525
+ })
526
+ );
518
527
  }
528
+ produceArgs = { ...produceArgs, userMessageId };
519
529
  if (options.contextGate) {
520
530
  const gate = await options.contextGate(produceArgs);
521
531
  if (!gate.proceed) {
@@ -613,6 +623,7 @@ function createChatTurnRoutes(options) {
613
623
  const parts = projected ? toChatMessageParts(projected) : void 0;
614
624
  if (!finalText.trim() && (!parts || parts.length === 0)) {
615
625
  await draft?.discard();
626
+ assistantMessageId = draft?.rowId() ?? null;
616
627
  return;
617
628
  }
618
629
  const usage = producer?.usage?.() ?? {};
@@ -629,13 +640,16 @@ function createChatTurnRoutes(options) {
629
640
  };
630
641
  if (draft) {
631
642
  await draft.finalize(values);
643
+ assistantMessageId = draft.rowId() ?? null;
632
644
  return;
633
645
  }
634
- await options.store.appendMessage({
635
- threadId: payload.threadId,
636
- role: "assistant",
637
- ...values
638
- });
646
+ assistantMessageId = rowIdOf(
647
+ await options.store.appendMessage({
648
+ threadId: payload.threadId,
649
+ role: "assistant",
650
+ ...values
651
+ })
652
+ );
639
653
  },
640
654
  ...options.onTurnComplete ? {
641
655
  // Wired into the engine's completion hook, which fires only when
@@ -650,6 +664,7 @@ function createChatTurnRoutes(options) {
650
664
  finalText,
651
665
  context,
652
666
  failed: runFailed,
667
+ assistantMessageId: assistantRowId(),
653
668
  ...runFailed ? { failureReason: failureReasonOf(lastFailureData) } : {},
654
669
  ...producer?.model ? { model: producer.model } : {},
655
670
  ...failoverInfo ? { modelFailover: failoverInfo } : {}
@@ -2288,6 +2303,7 @@ export {
2288
2303
  promptPartsByteSize,
2289
2304
  reconcileStaleTurnLock,
2290
2305
  resolveChatAttachments,
2306
+ rowIdOf,
2291
2307
  runDetachedTurn,
2292
2308
  sanitizeAttachmentFileName,
2293
2309
  sanitizeUploadFilename,