@ai-matrx/chat 0.7.9 → 0.7.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/agents/components/messages-display/AgentConversationDisplay.d.ts +1 -2
  3. package/dist/agents/components/messages-display/AgentConversationDisplay.js +3 -7
  4. package/dist/agents/components/messages-display/assistant/AgentAssistantMessage.d.ts +1 -3
  5. package/dist/agents/components/messages-display/assistant/AgentAssistantMessage.js +1 -23
  6. package/dist/agents/components/messages-display/assistant/AssistantTurnGroup.d.ts +1 -2
  7. package/dist/agents/components/messages-display/assistant/AssistantTurnGroup.js +2 -4
  8. package/dist/agents/components/messages-display/assistant/answerless-turn.d.ts +0 -2
  9. package/dist/agents/components/messages-display/assistant/answerless-turn.js +0 -1
  10. package/dist/agents/components/messages-display/user/AgentUserMessage.js +10 -4
  11. package/dist/agents/components/shared/AgentConversationColumn.js +0 -1
  12. package/dist/agents/components/shared/transcript-layout.d.ts +4 -2
  13. package/dist/agents/hooks/useHeadlessAgentJson.js +9 -1
  14. package/dist/agents/redux/execution-system/thunks/record-run-output-refused.d.ts +14 -0
  15. package/dist/agents/redux/execution-system/thunks/record-run-output-refused.js +24 -0
  16. package/dist/host/defaults/server-api.d.ts +2 -1
  17. package/dist/host/defaults/server-api.js +7 -0
  18. package/dist/host/server/provider-session-usage.d.ts +5 -0
  19. package/dist/host/server/provider-session-usage.js +5 -0
  20. package/dist/voice-agent/hooks/useXaiVoiceSession.js +13 -0
  21. package/dist/voice-agent/usage.d.ts +20 -0
  22. package/dist/voice-agent/usage.js +50 -0
  23. package/package.json +11 -11
  24. package/dist/agents/run/useRunAgent.d.ts +0 -69
  25. package/dist/agents/run/useRunAgent.js +0 -135
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.7.11
4
+ ### continue-agent-json is exported again
5
+
6
+ - **`./agents/redux/execution-system/thunks/continue-agent-json`** is back in the public surface; 0.7.9 dropped it
7
+ from `public-modules.json` while `matrx-frontend`'s Applet builder imports it (`continueAgentJson`).
8
+ - **Consumer action:** none.
9
+
10
+ ## 0.7.10
11
+ ### a refused answer is a failed run (lane M3)
12
+
13
+ - **`useHeadlessAgentJson.run`**: when `coerce` refuses the answer the model gave, the run is recorded as failed
14
+ (`chat.user_request` status `failed`, `error` = the reason, `metadata.output_refused`) through the guarded
15
+ `mergeJsonColumn` write (`recordRunOutputRefused`); a write that does not land is a `console.error`. Before, the run
16
+ stayed `completed` with `error` null forever. The error is re-thrown unchanged for the surface to say plainly.
17
+ - **Consumer action:** none.
18
+
3
19
  ## 0.7.9
4
20
 
5
21
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -15,6 +31,11 @@ No source changes intended and no consumer action required.
15
31
  ## 0.7.7
16
32
  ### Unreleased
17
33
 
34
+ - **No first-load layout shift from assistant messages** (spend register item 32): `AgentAssistantMessage` no longer swaps a fixed-height
35
+ skeleton for the markdown after 140 ms (measured CLS 0.48 on the run page). Persisted answers render in place on first paint.
36
+ `deferColdMarkdown` is removed from `AgentAssistantMessage`, `AssistantTurnGroup` and `AgentConversationDisplay` (the column keeps its
37
+ own history staging). **Consumer action:** none.
38
+
18
39
  - **`continueAgentJson`** (`agents/redux/execution-system/thunks/continue-agent-json`): the NEXT turn of a
19
40
  conversation whose first turn ran headlessly, waited on like the first (`adoptHeadlessAgentJson`). A turn is
20
41
  the person's words (`userInput`) or a turn the HOST writes (`hostTurn` — sent as `host_turn`, never
@@ -8,9 +8,8 @@ interface AgentConversationDisplayProps {
8
8
  */
9
9
  surfaceKey?: string;
10
10
  compact?: boolean;
11
- deferColdMarkdown?: boolean;
12
11
  fallbackVisibleGroupLimit?: number | null;
13
12
  bottomPinned?: boolean;
14
13
  }
15
- export declare function AgentConversationDisplay({ conversationId, surfaceKey, compact, deferColdMarkdown, fallbackVisibleGroupLimit, bottomPinned, }: AgentConversationDisplayProps): import("react").JSX.Element;
14
+ export declare function AgentConversationDisplay({ conversationId, surfaceKey, compact, fallbackVisibleGroupLimit, bottomPinned, }: AgentConversationDisplayProps): import("react").JSX.Element;
16
15
  export {};
@@ -53,7 +53,6 @@ function AgentConversationDisplay({
53
53
  conversationId,
54
54
  surfaceKey,
55
55
  compact = false,
56
- deferColdMarkdown = false,
57
56
  fallbackVisibleGroupLimit = null,
58
57
  bottomPinned = false
59
58
  }) {
@@ -262,8 +261,7 @@ function AgentConversationDisplay({
262
261
  isStreamActive: false,
263
262
  surfaceKey,
264
263
  compact: true,
265
- hideActionBar: true,
266
- deferColdMarkdown
264
+ hideActionBar: true
267
265
  },
268
266
  member.messageId
269
267
  )
@@ -307,8 +305,7 @@ function AgentConversationDisplay({
307
305
  isStreamActive: group.isStreamActive,
308
306
  surfaceKey,
309
307
  compact,
310
- canRetry: group.canRetry,
311
- deferColdMarkdown
308
+ canRetry: group.canRetry
312
309
  }
313
310
  )
314
311
  },
@@ -327,8 +324,7 @@ function AgentConversationDisplay({
327
324
  conversationId,
328
325
  surfaceKey,
329
326
  compact,
330
- members: group.members,
331
- deferColdMarkdown
327
+ members: group.members
332
328
  }
333
329
  )
334
330
  },
@@ -33,8 +33,6 @@ export interface AgentAssistantMessageProps {
33
33
  * button. Retry is non-destructive (the failed turn stays in history).
34
34
  */
35
35
  canRetry?: boolean;
36
- /** Chat cold-load only: show a short text skeleton before DB markdown mounts. */
37
- deferColdMarkdown?: boolean;
38
36
  /**
39
37
  * False for the intermediate iterations of a multi-step agentic turn. Only
40
38
  * the LAST member of a turn carries the answer, so only it may say "this run
@@ -43,4 +41,4 @@ export interface AgentAssistantMessageProps {
43
41
  */
44
42
  isTurnAnswer?: boolean;
45
43
  }
46
- export declare function AgentAssistantMessage({ conversationId, requestId, messageId, isStreamActive, recordMessageIds, streamSlotStart, streamSlotEnd, surfaceKey, hideActionBar, canRetry, deferColdMarkdown, isTurnAnswer, }: AgentAssistantMessageProps): import("react").JSX.Element;
44
+ export declare function AgentAssistantMessage({ conversationId, requestId, messageId, isStreamActive, recordMessageIds, streamSlotStart, streamSlotEnd, surfaceKey, hideActionBar, canRetry, isTurnAnswer, }: AgentAssistantMessageProps): import("react").JSX.Element;
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import {
4
- startTransition,
5
4
  useCallback,
6
5
  useMemo,
7
6
  useState,
@@ -117,7 +116,6 @@ function AgentAssistantMessage({
117
116
  surfaceKey,
118
117
  hideActionBar = false,
119
118
  canRetry = false,
120
- deferColdMarkdown = false,
121
119
  isTurnAnswer = true
122
120
  }) {
123
121
  useDebugContext("AgentAssistantMessage");
@@ -137,17 +135,7 @@ function AgentAssistantMessage({
137
135
  }, [surfaceKey, conversationId, messageId, requestId, isStreamActive]);
138
136
  const dispatch = useAppDispatch();
139
137
  const [retrying, setRetrying] = useState(false);
140
- const shouldDeferColdMarkdown = deferColdMarkdown && !requestId && !!messageId && !isStreamActive;
141
- const [coldMarkdownSettledMessageId, setColdMarkdownSettledMessageId] = useState(null);
142
- const coldMarkdownReady = !shouldDeferColdMarkdown || coldMarkdownSettledMessageId === messageId;
143
138
  const [providerRetryBusyAction, setProviderRetryBusyAction] = useState(null);
144
- useEffect(() => {
145
- if (!shouldDeferColdMarkdown || !messageId) return void 0;
146
- const timer = window.setTimeout(() => {
147
- startTransition(() => setColdMarkdownSettledMessageId(messageId));
148
- }, 140);
149
- return () => window.clearTimeout(timer);
150
- }, [shouldDeferColdMarkdown, messageId]);
151
139
  const { captureRef, isCapturing, captureAsPDF } = useDomCapturePrint();
152
140
  const handleFullPrint = useCallback(() => {
153
141
  captureAsPDF({
@@ -308,7 +296,6 @@ function AgentAssistantMessage({
308
296
  rowParts: extractContentBlocks(record)
309
297
  }),
310
298
  failed,
311
- coldMarkdownReady,
312
299
  messageId,
313
300
  renderedText,
314
301
  attachmentCount: attachmentParts.length,
@@ -394,7 +381,7 @@ function AgentAssistantMessage({
394
381
  "data-message-id": messageId ?? void 0,
395
382
  className: "group/assistant-msg rounded transition-shadow",
396
383
  children: [
397
- !coldMarkdownReady ? /* @__PURE__ */ jsx(AssistantMarkdownSkeleton, {}) : bufferStream && isStreamActive && !failed ? /* @__PURE__ */ jsxs(Fragment, { children: [
384
+ bufferStream && isStreamActive && !failed ? /* @__PURE__ */ jsxs(Fragment, { children: [
398
385
  showProviderRetry && providerRetry && /* @__PURE__ */ jsx(
399
386
  ProviderRetryCard,
400
387
  {
@@ -538,15 +525,6 @@ function readErrorDoor(details) {
538
525
  const { label, href } = door;
539
526
  return typeof label === "string" && typeof href === "string" && href.startsWith("/") ? { label, href } : null;
540
527
  }
541
- function AssistantMarkdownSkeleton() {
542
- return /* @__PURE__ */ jsxs("div", { className: "space-y-2 py-1", "aria-hidden": true, children: [
543
- /* @__PURE__ */ jsx("div", { className: "h-3.5 w-[96%] animate-pulse rounded bg-muted/55" }),
544
- /* @__PURE__ */ jsx("div", { className: "h-3.5 w-[88%] animate-pulse rounded bg-muted/50" }),
545
- /* @__PURE__ */ jsx("div", { className: "h-3.5 w-[72%] animate-pulse rounded bg-muted/45" }),
546
- /* @__PURE__ */ jsx("div", { className: "h-3.5 w-[94%] animate-pulse rounded bg-muted/40" }),
547
- /* @__PURE__ */ jsx("div", { className: "h-3.5 w-[42%] animate-pulse rounded bg-muted/35" })
548
- ] });
549
- }
550
528
  export {
551
529
  AgentAssistantMessage
552
530
  };
@@ -16,7 +16,6 @@ interface AssistantTurnGroupProps {
16
16
  compact?: boolean;
17
17
  /** Every assistant member of this logical turn, in transcript order. */
18
18
  members: AssistantTurnGroupMember[];
19
- deferColdMarkdown?: boolean;
20
19
  }
21
- export declare function AssistantTurnGroup({ conversationId, surfaceKey, compact, members: rawMembers, deferColdMarkdown, }: AssistantTurnGroupProps): import("react").JSX.Element;
20
+ export declare function AssistantTurnGroup({ conversationId, surfaceKey, compact, members: rawMembers, }: AssistantTurnGroupProps): import("react").JSX.Element;
22
21
  export {};
@@ -26,8 +26,7 @@ function AssistantTurnGroup({
26
26
  conversationId,
27
27
  surfaceKey,
28
28
  compact = false,
29
- members: rawMembers,
30
- deferColdMarkdown = false
29
+ members: rawMembers
31
30
  }) {
32
31
  const { captureRef, isCapturing, captureAsPDF } = useDomCapturePrint();
33
32
  const persistedView = useAppSelector(
@@ -96,8 +95,7 @@ function AssistantTurnGroup({
96
95
  surfaceKey,
97
96
  compact,
98
97
  hideActionBar: true,
99
- isTurnAnswer: idx === members.length - 1 && m.streamSlotEnd === void 0,
100
- deferColdMarkdown: deferColdMarkdown && idx < members.length - 1
98
+ isTurnAnswer: idx === members.length - 1 && m.streamSlotEnd === void 0
101
99
  }
102
100
  ) }, m.key))
103
101
  }
@@ -113,8 +113,6 @@ export interface AnswerlessTurnInput {
113
113
  isStreamActive: boolean;
114
114
  /** True when the turn failed — the error path owns that case. */
115
115
  failed: boolean;
116
- /** False while a cold-load skeleton stands in for the persisted markdown. */
117
- coldMarkdownReady: boolean;
118
116
  /** `cx_message.id`; null/undefined until the row settles. */
119
117
  messageId: string | null | undefined;
120
118
  /** The text this turn renders (citation markers included). */
@@ -31,7 +31,6 @@ function isAnswerlessTurn(input) {
31
31
  if (input.isStreamActive) return false;
32
32
  if (input.awaitingPerson) return false;
33
33
  if (input.failed) return false;
34
- if (!input.coldMarkdownReady) return false;
35
34
  if (!input.messageId) return false;
36
35
  if (input.attachmentCount > 0) return false;
37
36
  if (input.mediaBlockCount > 0) return false;
@@ -204,7 +204,9 @@ function AgentUserMessage({
204
204
  bodyBlocks,
205
205
  trimmedText
206
206
  ]);
207
- const compactUserMessage = useTranscriptLayout().userMessage === "compact";
207
+ const userMessageLook = useTranscriptLayout().userMessage;
208
+ const bubbleUserMessage = userMessageLook === "bubble";
209
+ const compactUserMessage = userMessageLook === "compact" || bubbleUserMessage;
208
210
  const { isCollapsed, setIsCollapsed, shouldBeCollapsible, measureRef } = useCollapsibleMessageText(collapseSignature, compactUserMessage ? COMPACT_COLLAPSE_THRESHOLD_PX : void 0);
209
211
  const authoredByHost = metadata?.authored_by === "host";
210
212
  const isSuperAdmin = useAppSelector(selectIsSuperAdmin);
@@ -271,7 +273,8 @@ function AgentUserMessage({
271
273
  return /* @__PURE__ */ jsxs(
272
274
  "div",
273
275
  {
274
- className: cn("group relative", containerMargin),
276
+ className: cn("group relative", containerMargin, bubbleUserMessage && "flex flex-col items-end"),
277
+ "data-user-message-look": userMessageLook,
275
278
  "data-message-id": messageId,
276
279
  onMouseEnter: () => setIsHovered(true),
277
280
  onMouseLeave: () => setIsHovered(false),
@@ -292,11 +295,14 @@ function AgentUserMessage({
292
295
  /* @__PURE__ */ jsx(
293
296
  "div",
294
297
  {
295
- className: compactUserMessage ? (
298
+ className: bubbleUserMessage ? (
299
+ // The person's turn on the trailing side, in a rounded bubble as wide as its words (Messages).
300
+ "w-fit max-w-[80%] min-w-0 bg-muted rounded-2xl px-3.5 py-2 [&_.math-content-wrapper]:my-0 [&_p:last-child]:mb-0"
301
+ ) : compactUserMessage ? (
296
302
  // Tight: no inset from the left, and the markdown paragraph's own margins dropped.
297
303
  "bg-muted rounded-md px-3 py-2 [&_.math-content-wrapper]:my-0 [&_p:last-child]:mb-0"
298
304
  ) : "bg-muted border border-border rounded-lg px-2 py-2",
299
- "data-user-message-style": compactUserMessage ? "compact" : "card",
305
+ "data-user-message-style": bubbleUserMessage ? "bubble" : compactUserMessage ? "compact" : "card",
300
306
  children: /* @__PURE__ */ jsxs("div", { className: "relative min-w-0", children: [
301
307
  /* @__PURE__ */ jsxs(
302
308
  "div",
@@ -299,7 +299,6 @@ function AgentConversationColumn({
299
299
  {
300
300
  conversationId: displayId,
301
301
  surfaceKey,
302
- deferColdMarkdown,
303
302
  bottomPinned: shouldPinColdScroll,
304
303
  fallbackVisibleGroupLimit: deferColdMarkdown ? chatVisibleGroupLimit ?? CHAT_INITIAL_VISIBLE_GROUPS : null
305
304
  }
@@ -8,7 +8,9 @@
8
8
  * - `width`: "reading" (the canonical `max-w-3xl` reading column) | "fill" (the host's pane).
9
9
  * - `align`: "center" | "start" — where a reading-width column sits in a wider pane.
10
10
  * - `userMessage`: "compact" (DEFAULT: a flat tight block; only a genuinely long message
11
- * collapses) | "card" (the bordered card that collapses a long message behind a chevron).
11
+ * collapses) | "card" (the bordered card that collapses a long message behind a chevron) |
12
+ * "bubble" (Messages / Xcode's coding assistant: the person's turn on the trailing side in a
13
+ * rounded bubble as wide as its words, answers full width; collapses like "compact").
12
14
  * - `answerActions`: "hover" (DEFAULT: the action row is revealed on hover / keyboard focus, and
13
15
  * always on touch — its height is reserved, so revealing it moves nothing; a finished answer ends
14
16
  * on its last word) | "latest" (the latest answer's row is always visible, older ones on hover) |
@@ -18,7 +20,7 @@ import { type ReactNode } from "react";
18
20
  export interface TranscriptLayout {
19
21
  width?: "reading" | "fill";
20
22
  align?: "center" | "start";
21
- userMessage?: "card" | "compact";
23
+ userMessage?: "card" | "compact" | "bubble";
22
24
  answerActions?: "shown" | "latest" | "hover";
23
25
  }
24
26
  export declare const DEFAULT_TRANSCRIPT_LAYOUT: Required<TranscriptLayout>;
@@ -3,6 +3,7 @@ import { useCallback, useState } from "react";
3
3
  import { useAppDispatch, useAppSelector, useAppStore } from "../../store/hooks.js";
4
4
  import { selectConversationRequestIds } from "../redux/execution-system/active-requests/active-requests.selectors.js";
5
5
  import { useRetainRequestForViewer } from "../redux/execution-system/active-requests/useRetainRequestForViewer.js";
6
+ import { recordRunOutputRefused } from "../redux/execution-system/thunks/record-run-output-refused.js";
6
7
  import {
7
8
  runHeadlessAgentJson
8
9
  } from "../redux/execution-system/thunks/run-headless-agent-json.js";
@@ -49,7 +50,14 @@ function useHeadlessAgentJson() {
49
50
  ...result.conversationId ? { conversationId: result.conversationId } : {}
50
51
  });
51
52
  }
52
- return opts.coerce ? opts.coerce(result.data, result) : result.data;
53
+ if (!opts.coerce) return result.data;
54
+ try {
55
+ return opts.coerce(result.data, result);
56
+ } catch (refused) {
57
+ const why = refused instanceof Error ? refused.message : String(refused);
58
+ if (result.requestId) void recordRunOutputRefused(result.requestId, why);
59
+ throw refused;
60
+ }
53
61
  } catch (e) {
54
62
  const message = e instanceof Error ? e.message : "Failed to run the agent";
55
63
  setError(message);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * recordRunOutputRefused — a run whose answer the BROWSER refused is a failed run.
3
+ *
4
+ * The server marks a run `completed` the moment the model stops; whether the answer was usable is only
5
+ * known where it is read (the feature's `coerce`, its own check). Before this, such a run stayed
6
+ * `completed` with `error` null forever (Make's describe box, 2026-10-09: "personal" is not one of
7
+ * organization, individual). The refusal is written onto the run's own `chat.user_request` row —
8
+ * status `failed`, `error` = the reason, and `metadata.output_refused` — through the guarded
9
+ * read-modify-write (`mergeJsonColumn`, a version guard, never a blind update).
10
+ *
11
+ * Never silent: a write that does not land is a `console.error` naming the run.
12
+ */
13
+ export declare const OUTPUT_REFUSED_METADATA_KEY = "output_refused";
14
+ export declare function recordRunOutputRefused(requestId: string, reason: string): Promise<boolean>;
@@ -0,0 +1,24 @@
1
+ import { mergeJsonColumn } from "@ai-matrx/data/db";
2
+ import { supabase } from "../../../../host/db.js";
3
+ import { hasBrowserSession } from "../../../../host/identity.js";
4
+ const OUTPUT_REFUSED_METADATA_KEY = "output_refused";
5
+ async function recordRunOutputRefused(requestId, reason) {
6
+ if (!requestId || !await hasBrowserSession()) return false;
7
+ const result = await mergeJsonColumn({
8
+ fetchCurrent: () => supabase.schema("chat").from("user_request").select("id, version, metadata").eq("id", requestId).maybeSingle(),
9
+ readColumn: (row) => row.metadata,
10
+ merge: (current) => ({ ...current, [OUTPUT_REFUSED_METADATA_KEY]: { reason, at: (/* @__PURE__ */ new Date()).toISOString() } }),
11
+ applyUpdate: ({ value, expectedVersion, nextVersion }) => supabase.schema("chat").from("user_request").update({ metadata: value, version: nextVersion, status: "failed", error: reason.slice(0, 500) }).eq("id", requestId).eq("version", expectedVersion).select("id, version, metadata").maybeSingle()
12
+ });
13
+ if (result.status !== "saved") {
14
+ console.error(
15
+ `[run-output-refused] Could not record the refusal on run ${requestId}: ` + (result.status === "error" ? String(result.error) : `write ${result.status}`) + `. The run still reads as completed. Reason: ${reason}`
16
+ );
17
+ return false;
18
+ }
19
+ return true;
20
+ }
21
+ export {
22
+ OUTPUT_REFUSED_METADATA_KEY,
23
+ recordRunOutputRefused
24
+ };
@@ -10,7 +10,7 @@
10
10
  * values here — no admin lane, no admin door, no endpoint overrides, no local
11
11
  * engine, no browser extension (each refusal says so once).
12
12
  */
13
- import { applyOrganizationContextHeader, type MatrxCallError, type MatrxCallResult, type MatrxTransport, type ProviderSessionFailure, type ProviderSessionFailureVerdict } from "@ai-matrx/agents/matrx";
13
+ import { applyOrganizationContextHeader, type MatrxCallError, type MatrxCallResult, type MatrxTransport, type ProviderSessionFailure, type ProviderSessionFailureVerdict, type ProviderSessionUsage } from "@ai-matrx/agents/matrx";
14
14
  import type { TypedStreamEvent } from "@ai-matrx/agents/generated/stream-events";
15
15
  import type { ApiPaths, ApiSchemas as Schemas, DefaultBrokeredCredential, DefaultRequestOptions, GetResult, PatchBody, PatchResult, PathWith, PostBody, PostResult } from "./typed-paths.js";
16
16
  import type { ChatIdentityPort, ChatOrgPort, ChatServerPort } from "../contract.js";
@@ -349,6 +349,7 @@ export declare function createDefaultServerApi(host: () => ServerHostView): {
349
349
  captureErrors?: boolean;
350
350
  }) => Promise<DefaultBrokeredCredential>;
351
351
  reportBrowserProviderFailure: (failure: ProviderSessionFailure) => DefaultServerThunk<Promise<ProviderSessionFailureVerdict | null>>;
352
+ reportBrowserProviderUsage: (usage: ProviderSessionUsage) => void;
352
353
  parseNdjsonStream: (response: Response, signal?: AbortSignal) => {
353
354
  requestId: string | null;
354
355
  conversationId: string | null;
@@ -7,7 +7,9 @@ import {
7
7
  executeMatrxCall,
8
8
  normalizeMatrxError,
9
9
  parseMatrxNdjsonResponse,
10
+ createProviderUsageReporter,
10
11
  reportProviderSessionFailure,
12
+ reportProviderSessionUsage,
11
13
  sendMatrxRequest
12
14
  } from "@ai-matrx/agents/matrx";
13
15
  import { announceOnce } from "../errors.js";
@@ -149,6 +151,9 @@ function createDefaultServerApi(host) {
149
151
  });
150
152
  }
151
153
  const productionUrl = () => baseUrl();
154
+ const usageReporter = createProviderUsageReporter({
155
+ send: (usage) => reportProviderSessionUsage(transport({ source: "providerSessionUsage" }), usage)
156
+ });
152
157
  return {
153
158
  // call-api
154
159
  callApi,
@@ -234,6 +239,8 @@ function createDefaultServerApi(host) {
234
239
  return null;
235
240
  }
236
241
  },
242
+ // what a browser-held provider session spent: queued, retried, billed once per usage_id
243
+ reportBrowserProviderUsage: (usage) => usageReporter.report(usage),
237
244
  // the NDJSON stream every run reads
238
245
  // THE shared parser: a body that breaks mid-run is a TRANSPORT loss (the
239
246
  // run may still finish and is reattachable), never a failed run.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Server seam — what a browser-held provider session spent (`POST /broker/usage`), reported through
3
+ * the configured host's server client. Fire-and-forget: the host queues, retries and de-duplicates.
4
+ */
5
+ export declare const reportBrowserProviderUsage: (usage: import("@ai-matrx/agents").ProviderSessionUsage) => void;
@@ -0,0 +1,5 @@
1
+ import { forwardServer } from "../server.js";
2
+ const reportBrowserProviderUsage = forwardServer("reportBrowserProviderUsage");
3
+ export {
4
+ reportBrowserProviderUsage
5
+ };
@@ -53,6 +53,8 @@ import {
53
53
  createXaiClient
54
54
  } from "../transport/xaiClient.js";
55
55
  import { reportBrowserProviderFailure } from "../../host/server/provider-session-failure.js";
56
+ import { reportBrowserProviderUsage } from "../../host/server/provider-session-usage.js";
57
+ import { createXaiUsageMeter } from "../usage.js";
56
58
  import { transcriptTextFromEvent } from "../transport/serverEvents.js";
57
59
  import {
58
60
  voiceDebugLog,
@@ -103,6 +105,13 @@ function useXaiVoiceSession(opts) {
103
105
  const voiceIdRef = useRef(voiceId);
104
106
  const realtimeModelRef = useRef(realtimeModel);
105
107
  realtimeModelRef.current = realtimeModel;
108
+ const usageMeterRef = useRef(null);
109
+ if (!usageMeterRef.current) {
110
+ usageMeterRef.current = createXaiUsageMeter({
111
+ model: () => realtimeModelRef.current,
112
+ report: (usage) => reportBrowserProviderUsage(usage)
113
+ });
114
+ }
106
115
  const instructionsRef = useRef(instructions);
107
116
  const toolsRef = useRef(tools);
108
117
  voiceIdRef.current = voiceId;
@@ -334,6 +343,7 @@ function useXaiVoiceSession(opts) {
334
343
  switch (event.type) {
335
344
  case "session.created":
336
345
  voiceDebugLog(instanceId, "info", "ws.session.created");
346
+ usageMeterRef.current?.started(event.session.id);
337
347
  break;
338
348
  case "session.updated": {
339
349
  voiceDebugLog(
@@ -523,6 +533,7 @@ function useXaiVoiceSession(opts) {
523
533
  break;
524
534
  }
525
535
  case "response.done": {
536
+ usageMeterRef.current?.responseDone(event.response);
526
537
  runFlush();
527
538
  if (!pendingAssistantTurnIdRef.current) break;
528
539
  const playedMs = playbackRef.current?.markTurnEnded() ?? 0;
@@ -658,6 +669,7 @@ function useXaiVoiceSession(opts) {
658
669
  assistantTurnStartedAtMsRef.current = null;
659
670
  assistantAudioStartedAtMsRef.current = null;
660
671
  firstAudioReceivedRef.current = false;
672
+ usageMeterRef.current?.ended();
661
673
  xaiClientRef.current?.disconnect();
662
674
  clearSessionSubscriptions();
663
675
  await captureRef.current?.stop();
@@ -771,6 +783,7 @@ function useXaiVoiceSession(opts) {
771
783
  const unsubEvent = client.onEvent(handleServerEvent);
772
784
  const unsubError = client.onError(handleClientError);
773
785
  const unsubClose = client.onClose((info) => {
786
+ usageMeterRef.current?.ended();
774
787
  voiceDebugIncr(instanceId, "closeCount");
775
788
  voiceDebugSetFlags(instanceId, {
776
789
  lastCloseIntentional: info.intentional,
@@ -0,0 +1,20 @@
1
+ import type { ProviderSessionUsage } from "@ai-matrx/agents/matrx";
2
+ import type { XaiResponseUsage } from "./transport/serverEvents.js";
3
+ export interface XaiUsageMeterOptions {
4
+ /** The realtime model the session opened with; unresolved = nothing priceable to report. */
5
+ model: () => string | null | undefined;
6
+ report: (usage: ProviderSessionUsage) => void;
7
+ now?: () => number;
8
+ }
9
+ export interface XaiUsageMeter {
10
+ /** Session opened: start the clock and remember xAI's session id. */
11
+ started(sessionId: string | null | undefined): void;
12
+ /** One `response.done`: report its tokens plus the session time since the last report. */
13
+ responseDone(response: {
14
+ id: string;
15
+ usage?: XaiResponseUsage;
16
+ }): void;
17
+ /** Session closed (clean or dropped): report the trailing slice of session time. Idempotent. */
18
+ ended(): void;
19
+ }
20
+ export declare function createXaiUsageMeter(options: XaiUsageMeterOptions): XaiUsageMeter;
@@ -0,0 +1,50 @@
1
+ function createXaiUsageMeter(options) {
2
+ const now = options.now ?? (() => Date.now());
3
+ let sessionId = null;
4
+ let sliceStartedAt = null;
5
+ let ordinal = 0;
6
+ function takeSeconds() {
7
+ if (sliceStartedAt === null) return 0;
8
+ const at = now();
9
+ const seconds = Math.max(0, (at - sliceStartedAt) / 1e3);
10
+ sliceStartedAt = at;
11
+ return seconds;
12
+ }
13
+ return {
14
+ started(id) {
15
+ sessionId = id?.trim() || null;
16
+ sliceStartedAt = now();
17
+ ordinal = 0;
18
+ },
19
+ responseDone(response) {
20
+ const model = options.model()?.trim();
21
+ if (!model || !response.id) return;
22
+ options.report({
23
+ provider: "xai",
24
+ model,
25
+ usage_id: response.id,
26
+ input_tokens: response.usage?.input_tokens,
27
+ output_tokens: response.usage?.output_tokens,
28
+ seconds: takeSeconds()
29
+ });
30
+ },
31
+ ended() {
32
+ const model = options.model()?.trim();
33
+ const startedAt = sliceStartedAt;
34
+ sliceStartedAt = null;
35
+ if (!model || startedAt === null) return;
36
+ const seconds = Math.max(0, (now() - startedAt) / 1e3);
37
+ if (seconds < 1) return;
38
+ ordinal += 1;
39
+ options.report({
40
+ provider: "xai",
41
+ model,
42
+ usage_id: `${sessionId ?? `session-${startedAt}`}:end:${ordinal}`,
43
+ seconds
44
+ });
45
+ }
46
+ };
47
+ }
48
+ export {
49
+ createXaiUsageMeter
50
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.7.9",
3
+ "version": "0.7.11",
4
4
  "description": "THE one AI Matrx person-to-agent chat system: the full chat (running agents, conversations, tool calls, streaming, context, surfaces, voice) as explicit per-module subpaths @ai-matrx/chat/<domain>/<module> (no wildcards: scripts/public-modules.json) over a host contract (/host, /host/react) whose only required value is a db. One chat, one renderer: the old kernel entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) are retired.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -2140,6 +2140,11 @@
2140
2140
  "import": "./dist/agents/redux/execution-system/thunks/call-conversation-api.js",
2141
2141
  "default": "./dist/agents/redux/execution-system/thunks/call-conversation-api.js"
2142
2142
  },
2143
+ "./agents/redux/execution-system/thunks/continue-agent-json": {
2144
+ "types": "./dist/agents/redux/execution-system/thunks/continue-agent-json.d.ts",
2145
+ "import": "./dist/agents/redux/execution-system/thunks/continue-agent-json.js",
2146
+ "default": "./dist/agents/redux/execution-system/thunks/continue-agent-json.js"
2147
+ },
2143
2148
  "./agents/redux/execution-system/thunks/conversation-bundle": {
2144
2149
  "types": "./dist/agents/redux/execution-system/thunks/conversation-bundle.d.ts",
2145
2150
  "import": "./dist/agents/redux/execution-system/thunks/conversation-bundle.js",
@@ -2435,11 +2440,6 @@
2435
2440
  "import": "./dist/agents/route/buildSystemAgentAiPayload.js",
2436
2441
  "default": "./dist/agents/route/buildSystemAgentAiPayload.js"
2437
2442
  },
2438
- "./agents/run/useRunAgent": {
2439
- "types": "./dist/agents/run/useRunAgent.d.ts",
2440
- "import": "./dist/agents/run/useRunAgent.js",
2441
- "default": "./dist/agents/run/useRunAgent.js"
2442
- },
2443
2443
  "./agents/runtime-reconnect/ServerOperationBanner": {
2444
2444
  "types": "./dist/agents/runtime-reconnect/ServerOperationBanner.d.ts",
2445
2445
  "import": "./dist/agents/runtime-reconnect/ServerOperationBanner.js",
@@ -4681,25 +4681,25 @@
4681
4681
  "server-only": "^0.0.1",
4682
4682
  "uuid": "^14.0.2",
4683
4683
  "zod": "^4.6.5",
4684
- "@ai-matrx/agents": "latest",
4685
4684
  "@ai-matrx/alchemy": "latest",
4686
4685
  "@ai-matrx/associations": "latest",
4687
4686
  "@ai-matrx/browser-audio": "latest",
4688
4687
  "@ai-matrx/canvas": "latest",
4689
4688
  "@ai-matrx/content-ir": "latest",
4689
+ "@ai-matrx/agents": "latest",
4690
+ "@ai-matrx/content-ir-react": "latest",
4690
4691
  "@ai-matrx/data": "latest",
4691
4692
  "@ai-matrx/design-system": "latest",
4693
+ "@ai-matrx/icons": "latest",
4692
4694
  "@ai-matrx/kit": "latest",
4693
4695
  "@ai-matrx/media": "latest",
4694
- "@ai-matrx/icons": "latest",
4695
4696
  "@ai-matrx/print": "latest",
4696
- "@ai-matrx/diff": "latest",
4697
4697
  "@ai-matrx/realtime": "latest",
4698
4698
  "@ai-matrx/records": "latest",
4699
4699
  "@ai-matrx/records-ui": "latest",
4700
4700
  "@ai-matrx/rich-content": "latest",
4701
4701
  "@ai-matrx/rich-editor": "latest",
4702
- "@ai-matrx/content-ir-react": "latest"
4702
+ "@ai-matrx/diff": "latest"
4703
4703
  },
4704
4704
  "devDependencies": {
4705
4705
  "@arethetypeswrong/cli": "^0.18.5",
@@ -4818,5 +4818,5 @@
4818
4818
  "check:side-effects": "node ../scripts/check-side-effects-listed.mjs . && node ../scripts/check-side-effects-listed.mjs --self-test",
4819
4819
  "check:public-surface": "node ./scripts/sync-public-surface.mjs --check && node ./scripts/sync-public-surface.mjs --self-test"
4820
4820
  },
4821
- "gitHead": "6f80fabd57ab4f61aa950c7e2d9b1f3309797abd"
4821
+ "gitHead": "db3bd1fad7851f7648e0aeee094cf4df6152b3ae"
4822
4822
  }
@@ -1,69 +0,0 @@
1
- import type { Action } from "redux";
2
- import type { ThunkAction } from "redux-thunk";
3
- import type { ChatRootState } from "../../store/root-state.js";
4
- import { type CallScope, type LLMParamsBody } from "../../host/server/call-api.js";
5
- import type { components } from "@ai-matrx/agents/generated/api-types";
6
- export interface RunAgentArgs {
7
- /** Live agent id (UUID) or slug. */
8
- agentId: string;
9
- /** The user message sent to the agent (optional for variable-only agents). */
10
- userInput?: string;
11
- /** Variable name → value map, filling the agent's declared variables. */
12
- variables?: Record<string, string>;
13
- /** Per-run model/config overrides (temperature, ai_model_id, …). */
14
- configOverrides?: LLMParamsBody;
15
- /** Entity-local scope. Explicit values beat the user's global active context. */
16
- organizationId?: string;
17
- projectId?: string;
18
- taskId?: string;
19
- /**
20
- * Stable durable-entity identity. The server reloads this row and uses its
21
- * saved scope; the browser does not get to redefine that scope.
22
- */
23
- contextAnchor?: components["schemas"]["ContextAnchor"];
24
- /** Durable producer attribution for the conversation and usage ledger. */
25
- sourceApp: string;
26
- sourceFeature: string;
27
- /**
28
- * Interactivity attestation — the ONLY input a client has into
29
- * `origin_class`. "user" (the default) is a person's gesture on the screen
30
- * that calls this hook; client code firing a run with no gesture behind it
31
- * passes "auto". Omitting it entirely is what makes the server class our own
32
- * screens as unattested API callers.
33
- */
34
- initiation?: "user" | "auto";
35
- /** Abort the in-flight run. */
36
- signal?: AbortSignal;
37
- /** Stream chunk-by-chunk text as it arrives (e.g. to show live progress). */
38
- onChunk?: (fullText: string) => void;
39
- }
40
- export interface UseRunAgent {
41
- /** Run the agent and resolve with the full accumulated text output. */
42
- run: (args: RunAgentArgs) => Promise<string>;
43
- running: boolean;
44
- error: string | null;
45
- reset: () => void;
46
- }
47
- /** Body type for POST /ai/agents/{agent_id} with transport-injected scope. */
48
- type RunAgentBody = Omit<components["schemas"]["AgentStartRequest"], "organization_id" | "project_id" | "task_id"> & Partial<Pick<components["schemas"]["AgentStartRequest"], "organization_id" | "project_id" | "task_id">>;
49
- export declare function buildRunAgentRequest(args: RunAgentArgs): {
50
- body: RunAgentBody;
51
- scopeOverrides: Partial<CallScope>;
52
- };
53
- /**
54
- * Run a one-shot agent through the published package client and resolve with
55
- * the full text output. Thunk-shaped so the hook (and thunk-style callers)
56
- * share ONE implementation with real `getState` access.
57
- *
58
- * Result semantics (parity with the pre-package flow, proven by
59
- * `useRunAgent.test.ts` + `run-agent-via-matrx.test.ts`):
60
- * - a fatal `error` event → throw its `user_message` → `message` → fallback;
61
- * - a `failed`/`cancelled` `user_request` completion → throw
62
- * `result.error` → `result.user_message` → "The agent run <status>"
63
- * (when both fire, the error event wins — the package/projector ruling);
64
- * - otherwise resolve accumulated chunk text, falling back to the
65
- * completion's `result.output`.
66
- */
67
- export declare function runAgentViaMatrxClient(args: RunAgentArgs): ThunkAction<Promise<string>, ChatRootState, unknown, Action>;
68
- export declare function useRunAgent(): UseRunAgent;
69
- export {};
@@ -1,135 +0,0 @@
1
- "use client";
2
- import { useCallback, useRef, useState } from "react";
3
- import { useAppDispatch } from "../../store/hooks.js";
4
- import {
5
- startAgentRun
6
- } from "@ai-matrx/agents/matrx";
7
- import {
8
- createAgentRequestProjection,
9
- projectAgentEvent
10
- } from "@ai-matrx/agents/projection/request";
11
- import {
12
- buildRequestBody,
13
- resolveScope,
14
- waitForAuthReady
15
- } from "../../host/server/call-api.js";
16
- import { createMatrxTransport } from "../../host/server/matrx-transport.js";
17
- import { applyDesktopTargetToRequestBody } from "@ai-matrx/agents/matrx";
18
- import { extractErrorMessage } from "@ai-matrx/data/net";
19
- import { selectDesktopTargetInstanceId } from "../../host/prefs.js";
20
- function buildRunAgentRequest(args) {
21
- const scopeOverrides = {};
22
- if (args.organizationId !== void 0) {
23
- scopeOverrides.organization_id = args.organizationId;
24
- }
25
- if (args.projectId !== void 0) scopeOverrides.project_id = args.projectId;
26
- if (args.taskId !== void 0) scopeOverrides.task_id = args.taskId;
27
- return {
28
- body: {
29
- // One-shot run: a freshly minted id for correlation, is_new so the
30
- // server never looks anything up, store:false so nothing is written.
31
- conversation_id: crypto.randomUUID(),
32
- is_new: true,
33
- store: false,
34
- user_input: args.userInput ?? null,
35
- variables: args.variables && Object.keys(args.variables).length > 0 ? args.variables : void 0,
36
- config_overrides: args.configOverrides,
37
- source_app: args.sourceApp,
38
- source_feature: args.sourceFeature,
39
- initiation: args.initiation ?? "user",
40
- context_anchor: args.contextAnchor,
41
- stream: true,
42
- debug: false
43
- },
44
- scopeOverrides
45
- };
46
- }
47
- const stringField = (bag, key) => {
48
- if (typeof bag !== "object" || bag === null) return null;
49
- const value = bag[key];
50
- return typeof value === "string" && value ? value : null;
51
- };
52
- function runAgentViaMatrxClient(args) {
53
- return async (_dispatch, getState) => {
54
- await waitForAuthReady(getState);
55
- const state = getState();
56
- const { body, scopeOverrides } = buildRunAgentRequest(args);
57
- const scope = resolveScope(state, scopeOverrides);
58
- const request = buildRequestBody(body, scope);
59
- const desktopTargetInstanceId = selectDesktopTargetInstanceId(state);
60
- if (desktopTargetInstanceId) {
61
- applyDesktopTargetToRequestBody(request, desktopTargetInstanceId);
62
- }
63
- const transport = createMatrxTransport(getState, {
64
- organizationId: scope.organization_id,
65
- source: "runAgentViaMatrxClient"
66
- });
67
- const handle = await startAgentRun(
68
- transport,
69
- args.agentId,
70
- request,
71
- args.signal ? { signal: args.signal } : {}
72
- );
73
- let projection = createAgentRequestProjection({
74
- requestId: handle.requestId ?? String(request.conversation_id),
75
- conversationId: handle.conversationId
76
- });
77
- for await (const envelope of handle.events) {
78
- const previousAnswer = projection.answer;
79
- projection = projectAgentEvent(projection, envelope);
80
- if (projection.answer !== previousAnswer) {
81
- args.onChunk?.(projection.answer);
82
- }
83
- }
84
- if (projection.error) {
85
- throw new Error(
86
- stringField(projection.error, "user_message") ?? stringField(projection.error, "message") ?? "The agent run failed"
87
- );
88
- }
89
- const completion = projection.completion;
90
- const completionStatus = typeof completion?.status === "string" ? completion.status : null;
91
- if (completionStatus === "failed" || completionStatus === "cancelled") {
92
- const result = completion?.result;
93
- throw new Error(
94
- stringField(result, "error") ?? stringField(result, "user_message") ?? `The agent run ${completionStatus}`
95
- );
96
- }
97
- if (projection.answer) return projection.answer;
98
- return stringField(completion?.result, "output") ?? "";
99
- };
100
- }
101
- function useRunAgent() {
102
- const dispatch = useAppDispatch();
103
- const [running, setRunning] = useState(false);
104
- const [error, setError] = useState(null);
105
- const runningRef = useRef(false);
106
- const reset = useCallback(() => {
107
- setRunning(false);
108
- setError(null);
109
- runningRef.current = false;
110
- }, []);
111
- const run = useCallback(
112
- async (args) => {
113
- setRunning(true);
114
- setError(null);
115
- runningRef.current = true;
116
- try {
117
- return await dispatch(runAgentViaMatrxClient(args));
118
- } catch (err) {
119
- const message = extractErrorMessage(err);
120
- setError(message);
121
- throw err instanceof Error ? err : new Error(message);
122
- } finally {
123
- runningRef.current = false;
124
- setRunning(false);
125
- }
126
- },
127
- [dispatch]
128
- );
129
- return { run, running, error, reset };
130
- }
131
- export {
132
- buildRunAgentRequest,
133
- runAgentViaMatrxClient,
134
- useRunAgent
135
- };