@ai-matrx/chat 0.3.3 → 0.3.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/agent-copy/export.d.ts +1 -1
  3. package/dist/agent-copy/export.js +5 -16
  4. package/dist/agents/components/context-items/bodies/WebpageBody.js +12 -5
  5. package/dist/agents/components/debug/SandboxFileViewer.js +31 -22
  6. package/dist/agents/components/messages-display/TranscriptIntegrityCopyButton.js +14 -3
  7. package/dist/agents/components/messages-display/assistant/AgentAssistantMessage.js +18 -1
  8. package/dist/agents/components/run/AssistantWarning.js +12 -0
  9. package/dist/agents/components/run/IncompleteNote.d.ts +14 -0
  10. package/dist/agents/components/run/IncompleteNote.js +21 -0
  11. package/dist/agents/components/run-controls/PayloadTab.js +5 -1
  12. package/dist/agents/components/run-controls/panels/BackendTargetPanel.js +12 -1
  13. package/dist/agents/redux/execution-system/active-requests/active-requests.selectors.d.ts +2 -1
  14. package/dist/agents/redux/execution-system/active-requests/active-requests.selectors.js +2 -1
  15. package/dist/agents/redux/execution-system/active-requests/answer-kept-warning.d.ts +7 -0
  16. package/dist/agents/redux/execution-system/active-requests/answer-kept-warning.js +12 -0
  17. package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateCore.js +68 -9
  18. package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateList.js +30 -8
  19. package/dist/agents/redux/execution-system/messages/messages.selectors.d.ts +6 -0
  20. package/dist/agents/redux/execution-system/messages/messages.selectors.js +17 -1
  21. package/dist/agents/redux/execution-system/messages/turn-outcome.d.ts +26 -0
  22. package/dist/agents/redux/execution-system/messages/turn-outcome.js +28 -0
  23. package/dist/cx-chat/components/messages/AssistantMessage.js +4 -1
  24. package/dist/cx-chat/components/messages/UserMessage.js +1 -8
  25. package/dist/cx-conversation/AssistantMessage.js +4 -1
  26. package/dist/cx-conversation/UserMessage.js +1 -8
  27. package/dist/tool-call-visualization/components/ToolTabBodies.js +11 -4
  28. package/dist/voice-agent/components/VoiceDebugPanel.js +22 -3
  29. package/dist/window-panels/windows/agents/ObservationalMemoryWindow.js +20 -5
  30. package/package.json +9 -9
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.3.4 — 2026-10-07
4
+ ### Re-sync before matrx-frontend consumes the published package (P27)
5
+
6
+ - Carries every matrx-frontend `packages/chat` edit after `d8078d15b2` plus the in-flight working-tree
7
+ edits in the shared checkout: an incomplete answer reads the same live and reloaded
8
+ (`messages/turn-outcome`, `active-requests/answer-kept-warning`, `run/IncompleteNote`), ALC-19 copy
9
+ results honored (`copyText` boolean). The kept-out `ToolTabBodies` CopyButton shim stays out.
10
+ - Consumer action: none.
11
+
3
12
  ## 0.3.3 — 2026-10-07
4
13
  ### Re-sync before matrx-frontend consumes the published package (P27)
5
14
 
@@ -25,7 +25,7 @@ export interface ExportItem {
25
25
  export declare function exportFilename(base: string, extension: string): string;
26
26
  export declare function jsonExportItem(data: unknown | (() => unknown), label?: string): ExportItem;
27
27
  export declare function textExportItem(content: string | (() => string), label: string, extension?: string): ExportItem;
28
- /** Rows → CSV. Columns: explicit list, or the union of keys across rows. */
28
+ /** Rows → CSV (Alchemy's one writer). Columns: explicit list, or the union of keys across rows. */
29
29
  export declare function rowsToCsv(rows: Array<Record<string, unknown>>, columns?: Array<{
30
30
  key: string;
31
31
  header: string;
@@ -1,3 +1,4 @@
1
+ import { toDelimitedText } from "@ai-matrx/alchemy/operate/read";
1
2
  function exportFilename(base, extension) {
2
3
  const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
3
4
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -34,27 +35,15 @@ function textExportItem(content, label, extension = "txt") {
34
35
  })
35
36
  };
36
37
  }
37
- function csvEscape(value) {
38
- return /[",\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
39
- }
40
38
  function rowsToCsv(rows, columns) {
41
39
  const cols = columns ?? [...new Set(rows.flatMap((row) => Object.keys(row)))].map((key) => ({
42
40
  key,
43
41
  header: key
44
42
  }));
45
- const lines = [cols.map((c) => csvEscape(c.header)).join(",")];
46
- for (const row of rows) {
47
- lines.push(
48
- cols.map((c) => {
49
- const value = row[c.key];
50
- if (value === null || value === void 0) return "";
51
- return csvEscape(
52
- typeof value === "object" ? JSON.stringify(value) : String(value)
53
- );
54
- }).join(",")
55
- );
56
- }
57
- return lines.join("\n");
43
+ return toDelimitedText(
44
+ cols.map((c) => c.header),
45
+ rows.map((row) => cols.map((c) => row[c.key] ?? null))
46
+ );
58
47
  }
59
48
  function csvExportItem(rows, label = "CSV", columns) {
60
49
  return {
@@ -11,10 +11,7 @@ import {
11
11
  } from "@ai-matrx/design-system";
12
12
  import { toast } from "../../../../host/notify.js";
13
13
  import { WebpageSnapshotView } from "../../../../host/ui-slots.js";
14
- import {
15
- webpageTitle,
16
- webpageUrl
17
- } from "../../../resources/webpage-snapshot.js";
14
+ import { webpageTitle, webpageUrl } from "../../../resources/webpage-snapshot.js";
18
15
  import { Button } from "@ai-matrx/design-system/controls";
19
16
  function firstWebpage(item) {
20
17
  return item.refs.webpages?.[0] ?? null;
@@ -61,7 +58,17 @@ function WebpageFooter({ item }) {
61
58
  return /* @__PURE__ */ jsxs(Fragment, { children: [
62
59
  /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate font-mono text-[11px] text-muted-foreground", children: url }),
63
60
  /* @__PURE__ */ jsxs(Tooltip, { children: [
64
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: copy, "aria-label": "Copy webpage URL", className: "ml-auto" }) }),
61
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
62
+ Button,
63
+ {
64
+ variant: "quiet",
65
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
66
+ glyphTone: copied ? "success" : void 0,
67
+ onClick: copy,
68
+ "aria-label": "Copy webpage URL",
69
+ className: "ml-auto"
70
+ }
71
+ ) }),
65
72
  /* @__PURE__ */ jsx(TooltipContent, { children: "Copy URL" })
66
73
  ] }),
67
74
  /* @__PURE__ */ jsxs(Tooltip, { children: [
@@ -23,28 +23,27 @@ function SandboxFileViewer({
23
23
  const [copied, setCopied] = useState(false);
24
24
  const adapterRef = useRef(null);
25
25
  if (!adapterRef.current || adapterRef.current.instanceId !== sandboxRowId) {
26
- adapterRef.current = createSandboxFilesystemAdapter(sandboxRowId);
26
+ adapterRef.current = createSandboxFilesystemAdapter(
27
+ sandboxRowId
28
+ );
27
29
  }
28
- const load = useCallback(
29
- async (p) => {
30
- const target = p.trim();
31
- if (!target) return;
32
- setLoading(true);
33
- setError(null);
34
- try {
35
- const adapter = adapterRef.current;
36
- if (!adapter) throw new Error("Sandbox adapter not initialized");
37
- const text = await adapter.readFile(target);
38
- setContent(text);
39
- } catch (err) {
40
- setContent(null);
41
- setError(err instanceof Error ? err.message : "Failed to read file");
42
- } finally {
43
- setLoading(false);
44
- }
45
- },
46
- []
47
- );
30
+ const load = useCallback(async (p) => {
31
+ const target = p.trim();
32
+ if (!target) return;
33
+ setLoading(true);
34
+ setError(null);
35
+ try {
36
+ const adapter = adapterRef.current;
37
+ if (!adapter) throw new Error("Sandbox adapter not initialized");
38
+ const text = await adapter.readFile(target);
39
+ setContent(text);
40
+ } catch (err) {
41
+ setContent(null);
42
+ setError(err instanceof Error ? err.message : "Failed to read file");
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }, []);
48
47
  useEffect(() => {
49
48
  if (initialPath) void load(initialPath);
50
49
  }, [initialPath, sandboxRowId, load]);
@@ -91,7 +90,17 @@ function SandboxFileViewer({
91
90
  ),
92
91
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-2 py-1 border-b border-border bg-muted/30", children: [
93
92
  /* @__PURE__ */ jsx("span", { className: "font-mono text-[11px] text-muted-foreground truncate", children: lockPath ? initialPath : content != null ? path : "" }),
94
- content != null && /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: handleCopy, className: "shrink-0", children: "Copy" })
93
+ content != null && /* @__PURE__ */ jsx(
94
+ Button,
95
+ {
96
+ variant: "quiet",
97
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
98
+ glyphTone: copied ? "success" : void 0,
99
+ onClick: handleCopy,
100
+ className: "shrink-0",
101
+ children: "Copy"
102
+ }
103
+ )
95
104
  ] }),
96
105
  /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: loading && content == null ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-3 text-xs text-muted-foreground", children: [
97
106
  /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }),
@@ -43,11 +43,11 @@ function TranscriptIntegrityCopyButtonInner({
43
43
  effectiveVisibleGroupLimit
44
44
  });
45
45
  publish({
46
- "Conversation": conversationId,
46
+ Conversation: conversationId,
47
47
  "Loaded rows": report.spine.loadedRows,
48
48
  "Groups rendered": `${report.groups.filter((g) => g.rendered).length}/${report.groups.length}`,
49
49
  "Stream phase": report.request.streamPhase,
50
- "Anomalies": report.anomalies.length === 0 ? "none" : report.anomalies
50
+ Anomalies: report.anomalies.length === 0 ? "none" : report.anomalies
51
51
  });
52
52
  }, [
53
53
  isActive,
@@ -78,7 +78,18 @@ function TranscriptIntegrityCopyButtonInner({
78
78
  );
79
79
  }
80
80
  };
81
- return /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: /* @__PURE__ */ jsx(Stethoscope, {}), onClick: () => void copy(), title: "Copy transcript integrity report (admin) \u2014 paste it to an agent when a message is missing", "aria-label": "Copy transcript integrity report", "data-testid": "transcript-integrity-copy", className: "absolute top-2 right-4 z-10" });
81
+ return /* @__PURE__ */ jsx(
82
+ Button,
83
+ {
84
+ variant: "quiet",
85
+ icon: /* @__PURE__ */ jsx(Stethoscope, {}),
86
+ onClick: () => void copy(),
87
+ title: "Copy transcript integrity report (admin) \u2014 paste it to an agent when a message is missing",
88
+ "aria-label": "Copy transcript integrity report",
89
+ "data-testid": "transcript-integrity-copy",
90
+ className: "absolute top-2 right-4 z-10"
91
+ }
92
+ );
82
93
  }
83
94
  export {
84
95
  TranscriptIntegrityCopyButton
@@ -34,8 +34,15 @@ import {
34
34
  isFailedRecord,
35
35
  extractRecordError,
36
36
  selectIsLatestAssistantMessage,
37
- persistedBodyBlocks
37
+ persistedBodyBlocks,
38
+ incompleteAnswerReason
38
39
  } from "../../../redux/execution-system/messages/messages.selectors.js";
40
+ import {
41
+ incompleteDetail,
42
+ incompleteLabel,
43
+ isAnswerKeptWarning
44
+ } from "../../../redux/execution-system/messages/turn-outcome.js";
45
+ import { IncompleteNote } from "../../run/IncompleteNote.js";
39
46
  import {
40
47
  isAttachmentMessagePart,
41
48
  isInlineAssistantMedia
@@ -443,6 +450,16 @@ function AgentAssistantMessage({
443
450
  !isClosedStreamSegment && displaySources.length > 0 && /* @__PURE__ */ jsx(MessageSourcesRow, { sources: displaySources, className: "mt-2" }),
444
451
  !isStreamActive && /* @__PURE__ */ jsx(PrefillNote, { metadata: record?.metadata }),
445
452
  !isStreamActive && /* @__PURE__ */ jsx(StoppedNote, { metadata: record?.metadata, requestStatus }),
453
+ !isStreamActive && !visibleWarnings?.some(isAnswerKeptWarning) && (() => {
454
+ const reason = incompleteAnswerReason(record);
455
+ return reason ? /* @__PURE__ */ jsx(
456
+ IncompleteNote,
457
+ {
458
+ label: incompleteLabel(reason),
459
+ detail: incompleteDetail(reason)
460
+ }
461
+ ) : null;
462
+ })(),
446
463
  isStreamActive && !failed && (phase === "text_streaming" || phase === "interstitial") && /* @__PURE__ */ jsx("div", { className: "relative h-0 overflow-visible", children: /* @__PURE__ */ jsx(BreathingOrb, { className: "absolute left-0 top-[3px]", size: 40 }) })
447
464
  ] }),
448
465
  !isClosedStreamSegment && visibleWarnings?.map((warning, index) => /* @__PURE__ */ jsx(
@@ -3,8 +3,20 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { AlertTriangle } from "lucide-react";
5
5
  import { Button } from "@ai-matrx/design-system/controls";
6
+ import { IncompleteNote } from "./IncompleteNote.js";
7
+ import { isAnswerKeptWarning } from "../../redux/execution-system/active-requests/answer-kept-warning.js";
8
+ import { incompleteLabel } from "../../redux/execution-system/messages/turn-outcome.js";
6
9
  function AssistantWarning({ warning }) {
7
10
  const [showDetails, setShowDetails] = useState(false);
11
+ if (isAnswerKeptWarning(warning)) {
12
+ return /* @__PURE__ */ jsx(
13
+ IncompleteNote,
14
+ {
15
+ label: incompleteLabel(warning.code),
16
+ detail: warning.user_message?.trim() || void 0
17
+ }
18
+ );
19
+ }
8
20
  const message = warning.user_message?.trim() || "The response completed with a warning.";
9
21
  const detail = warning.system_message?.trim();
10
22
  const hasDetails = Boolean(
@@ -0,0 +1,14 @@
1
+ interface IncompleteNoteProps {
2
+ /** The short label ("Ending may be cut off"). */
3
+ label: string;
4
+ /** The reason-specific sentence, shown under Details. */
5
+ detail?: string;
6
+ }
7
+ /**
8
+ * The quiet mark on an answer that was delivered and kept but whose end may be
9
+ * missing (recitation stop, length limit). Live (from the stream warning) and
10
+ * after a reload (from the saved finish reason) both render THIS component, so
11
+ * the two never look different. A state marker — not an error, no Retry.
12
+ */
13
+ export declare function IncompleteNote({ label, detail }: IncompleteNoteProps): import("react").JSX.Element;
14
+ export {};
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { AlertTriangle } from "lucide-react";
5
+ import { Button } from "@ai-matrx/design-system/controls";
6
+ function IncompleteNote({ label, detail }) {
7
+ const [open, setOpen] = useState(false);
8
+ return /* @__PURE__ */ jsxs("div", { className: "mt-1 text-xs", "data-testid": "incomplete-note", children: [
9
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-2.5 gap-y-1", children: [
10
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 text-amber-700 dark:text-amber-400", children: [
11
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0" }),
12
+ label
13
+ ] }),
14
+ detail && /* @__PURE__ */ jsx(Button, { variant: "link", onClick: () => setOpen((v) => !v), children: open ? "Hide details" : "Details" })
15
+ ] }),
16
+ detail && open && /* @__PURE__ */ jsx("div", { className: "mt-1 rounded bg-amber-500/10 px-2 py-1.5 text-[11px] leading-relaxed text-muted-foreground", children: detail })
17
+ ] });
18
+ }
19
+ export {
20
+ IncompleteNote
21
+ };
@@ -115,7 +115,11 @@ function roledImagesOf(messages) {
115
115
  continue;
116
116
  }
117
117
  const source = typeof b.url === "string" ? b.url : String(b.file_id ?? "");
118
- rows.push({ role: b.role, variable: variableNameOfImageUrl(source), source });
118
+ rows.push({
119
+ role: b.role,
120
+ variable: variableNameOfImageUrl(source),
121
+ source
122
+ });
119
123
  }
120
124
  }
121
125
  return rows;
@@ -44,7 +44,18 @@ function CopyableValue({
44
44
  children: value
45
45
  }
46
46
  ),
47
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: handleCopy, title: "Copy", "aria-label": "Copy", className: "shrink-0" })
47
+ /* @__PURE__ */ jsx(
48
+ Button,
49
+ {
50
+ variant: "quiet",
51
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
52
+ glyphTone: copied ? "success" : void 0,
53
+ onClick: handleCopy,
54
+ title: "Copy",
55
+ "aria-label": "Copy",
56
+ className: "shrink-0"
57
+ }
58
+ )
48
59
  ] });
49
60
  }
50
61
  function Row({
@@ -3356,7 +3356,8 @@ export declare const selectHighWarnings: (requestId: string) => ((state: {
3356
3356
  argsMemoize: typeof import("reselect").weakMapMemoize;
3357
3357
  };
3358
3358
  /**
3359
- * High-severity warnings plus any warning whose `code` is explicitly
3359
+ * High-severity warnings, any warning that says the answer was kept (an
3360
+ * incomplete answer, never silent) and any warning whose `code` is explicitly
3360
3361
  * promoted (see `PROMOTED_WARNING_CODES`). This is the set `AssistantWarning`
3361
3362
  * actually renders inline — `selectHighWarnings` stays available for callers
3362
3363
  * that want the stricter, level-only cut.
@@ -3,6 +3,7 @@ import {
3
3
  liveAgentCallTrace
4
4
  } from "../utils/agent-call-trace.js";
5
5
  import { createSelector } from "@reduxjs/toolkit";
6
+ import { isAnswerKeptWarning } from "./answer-kept-warning.js";
6
7
  import { blockMediaFileId } from "../utils/block-media-identity.js";
7
8
  import { DECISION_ANSWERS_BLOCK_TYPE } from "../../../../utils/content-ir/decision-answers-ids.js";
8
9
  import { decisionAnswersText } from "@ai-matrx/agents/presentation/decision-answers";
@@ -872,7 +873,7 @@ const selectVisibleWarnings = (requestId) => createSelector(
872
873
  (warnings) => {
873
874
  if (!warnings) return void 0;
874
875
  return warnings.filter(
875
- (w) => w.level === "high" || PROMOTED_WARNING_CODES.has(w.code)
876
+ (w) => w.level === "high" || PROMOTED_WARNING_CODES.has(w.code) || isAnswerKeptWarning(w)
876
877
  );
877
878
  }
878
879
  );
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A stream warning that says "the answer was delivered and kept, but the
3
+ * provider stopped early so the end may be missing". Leaf module (no store
4
+ * imports) so selectors on either side can read it.
5
+ */
6
+ import type { WarningPayload } from "@ai-matrx/agents/generated/stream-events";
7
+ export declare function isAnswerKeptWarning(warning: WarningPayload): boolean;
@@ -0,0 +1,12 @@
1
+ const ANSWER_KEPT_WARNING_CODES = /* @__PURE__ */ new Set([
2
+ "provider_recitation_stop",
3
+ "truncated_response"
4
+ ]);
5
+ function isAnswerKeptWarning(warning) {
6
+ if (ANSWER_KEPT_WARNING_CODES.has(warning.code)) return true;
7
+ const md = warning.metadata;
8
+ return !!md && typeof md === "object" && md.answer_kept === true;
9
+ }
10
+ export {
11
+ isAnswerKeptWarning
12
+ };
@@ -49,7 +49,18 @@ function UuidField({ value, label }) {
49
49
  ),
50
50
  /* @__PURE__ */ jsxs("span", { className: "flex-1 min-w-0 flex items-center gap-1.5", children: [
51
51
  /* @__PURE__ */ jsx("span", { className: "text-[11px] font-mono text-foreground break-all", children: value }),
52
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: copy, title: "Copy UUID", "aria-label": "Copy UUID", className: "shrink-0" })
52
+ /* @__PURE__ */ jsx(
53
+ Button,
54
+ {
55
+ variant: "quiet",
56
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
57
+ glyphTone: copied ? "success" : void 0,
58
+ onClick: copy,
59
+ title: "Copy UUID",
60
+ "aria-label": "Copy UUID",
61
+ className: "shrink-0"
62
+ }
63
+ )
53
64
  ] })
54
65
  ] });
55
66
  }
@@ -131,10 +142,21 @@ function SectionHeader({
131
142
  open ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 text-muted-foreground shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground shrink-0" }),
132
143
  /* @__PURE__ */ jsx("span", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground flex-1", children: title }),
133
144
  count !== void 0 && /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground/60 shrink-0 mr-1", children: count }),
134
- copyValue && /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: (e) => {
135
- e.stopPropagation();
136
- copy();
137
- }, title: "Copy section as JSON", "aria-label": "Copy section as JSON", className: "opacity-0 group-hover:opacity-100 shrink-0" })
145
+ copyValue && /* @__PURE__ */ jsx(
146
+ Button,
147
+ {
148
+ variant: "quiet",
149
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
150
+ glyphTone: copied ? "success" : void 0,
151
+ onClick: (e) => {
152
+ e.stopPropagation();
153
+ copy();
154
+ },
155
+ title: "Copy section as JSON",
156
+ "aria-label": "Copy section as JSON",
157
+ className: "opacity-0 group-hover:opacity-100 shrink-0"
158
+ }
159
+ )
138
160
  ]
139
161
  }
140
162
  )
@@ -195,8 +217,28 @@ function RawJsonSection({ data }) {
195
217
  const { copied, copy } = useCopyText(json);
196
218
  return /* @__PURE__ */ jsxs("div", { className: "border border-border rounded-sm overflow-hidden", children: [
197
219
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-2 py-1.5", children: [
198
- /* @__PURE__ */ jsx(Tile, { variant: "quiet", icon: open ? /* @__PURE__ */ jsx(ChevronDown, {}) : /* @__PURE__ */ jsx(ChevronRight, {}), title: "Raw JSON", onClick: () => setOpen((v) => !v), className: "flex-1" }),
199
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: copy, title: "Copy full JSON", "aria-label": "Copy full JSON", className: "shrink-0" }),
220
+ /* @__PURE__ */ jsx(
221
+ Tile,
222
+ {
223
+ variant: "quiet",
224
+ icon: open ? /* @__PURE__ */ jsx(ChevronDown, {}) : /* @__PURE__ */ jsx(ChevronRight, {}),
225
+ title: "Raw JSON",
226
+ onClick: () => setOpen((v) => !v),
227
+ className: "flex-1"
228
+ }
229
+ ),
230
+ /* @__PURE__ */ jsx(
231
+ Button,
232
+ {
233
+ variant: "quiet",
234
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
235
+ glyphTone: copied ? "success" : void 0,
236
+ onClick: copy,
237
+ title: "Copy full JSON",
238
+ "aria-label": "Copy full JSON",
239
+ className: "shrink-0"
240
+ }
241
+ ),
200
242
  open ? /* @__PURE__ */ jsx(EyeOff, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }) : /* @__PURE__ */ jsx(Eye, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" })
201
243
  ] }),
202
244
  open && /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono text-foreground bg-muted/30 px-3 py-2 overflow-x-auto border-t border-border/40", children: json })
@@ -233,7 +275,14 @@ function InstanceUIStateCore({
233
275
  agentName && instanceTitle !== agentName && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground truncate", children: agentName })
234
276
  ] }),
235
277
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [
236
- /* @__PURE__ */ jsx(Badge, { className: "shrink-0", tone: state.displayMode === "direct" ? "info" : "neutral", children: state.displayMode }),
278
+ /* @__PURE__ */ jsx(
279
+ Badge,
280
+ {
281
+ className: "shrink-0",
282
+ tone: state.displayMode === "direct" ? "info" : "neutral",
283
+ children: state.displayMode
284
+ }
285
+ ),
237
286
  /* @__PURE__ */ jsx(CopyEntireButton, { json: instanceJson })
238
287
  ] })
239
288
  ] }),
@@ -478,7 +527,17 @@ function InstanceUIStateCore({
478
527
  }
479
528
  function CopyEntireButton({ json }) {
480
529
  const { copied, copy } = useCopyText(json);
481
- return /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: copy, title: "Copy full state as JSON", "aria-label": "Copy full state as JSON" });
530
+ return /* @__PURE__ */ jsx(
531
+ Button,
532
+ {
533
+ variant: "quiet",
534
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
535
+ glyphTone: copied ? "success" : void 0,
536
+ onClick: copy,
537
+ title: "Copy full state as JSON",
538
+ "aria-label": "Copy full state as JSON"
539
+ }
540
+ );
482
541
  }
483
542
  export {
484
543
  InstanceUIStateCore
@@ -77,10 +77,21 @@ function InstanceRow({
77
77
  }
78
78
  ),
79
79
  title && /* @__PURE__ */ jsx("span", { className: "text-[11px] font-mono text-muted-foreground/50 shrink-0 hidden group-hover:inline", children: shortId }),
80
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: (e) => {
81
- e.stopPropagation();
82
- copy();
83
- }, title: "Copy conversation ID", "aria-label": "Copy conversation ID", className: "opacity-0 group-hover:opacity-100 shrink-0" })
80
+ /* @__PURE__ */ jsx(
81
+ Button,
82
+ {
83
+ variant: "quiet",
84
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
85
+ glyphTone: copied ? "success" : void 0,
86
+ onClick: (e) => {
87
+ e.stopPropagation();
88
+ copy();
89
+ },
90
+ title: "Copy conversation ID",
91
+ "aria-label": "Copy conversation ID",
92
+ className: "opacity-0 group-hover:opacity-100 shrink-0"
93
+ }
94
+ )
84
95
  ]
85
96
  }
86
97
  );
@@ -129,10 +140,21 @@ function AgentGroup({
129
140
  }
130
141
  ),
131
142
  /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground/50 shrink-0 ml-1", children: group.conversationIds.length }),
132
- group.agentId && /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: (e) => {
133
- e.stopPropagation();
134
- copy();
135
- }, title: "Copy agent ID", "aria-label": "Copy agent ID", className: "opacity-0 group-hover:opacity-100 shrink-0 ml-0.5" })
143
+ group.agentId && /* @__PURE__ */ jsx(
144
+ Button,
145
+ {
146
+ variant: "quiet",
147
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
148
+ glyphTone: copied ? "success" : void 0,
149
+ onClick: (e) => {
150
+ e.stopPropagation();
151
+ copy();
152
+ },
153
+ title: "Copy agent ID",
154
+ "aria-label": "Copy agent ID",
155
+ className: "opacity-0 group-hover:opacity-100 shrink-0 ml-0.5"
156
+ }
157
+ )
136
158
  ]
137
159
  }
138
160
  ),
@@ -177,6 +177,12 @@ export declare function isFailedRecord(record: MessageRecord | undefined): boole
177
177
  * that row reloaded with no error at all.
178
178
  */
179
179
  export declare function abnormalFinishReason(record: MessageRecord | undefined): string | null;
180
+ /**
181
+ * The stop reason of a reloaded turn that is INCOMPLETE rather than failed:
182
+ * an answer-stands stop on a turn that actually holds answer text and is not
183
+ * saved failed. Null otherwise (normal finish, no content, safety stops…).
184
+ */
185
+ export declare function incompleteAnswerReason(record: MessageRecord | undefined): string | null;
180
186
  /** The sentence a reloaded early-stopped turn shows. */
181
187
  export declare function earlyStopMessage(reason: string): string;
182
188
  /**
@@ -285,7 +285,7 @@ function isFailedRecord(record) {
285
285
  if (!!md && typeof md === "object" && !Array.isArray(md) && md.failed === true) {
286
286
  return true;
287
287
  }
288
- return abnormalFinishReason(record) !== null;
288
+ return abnormalFinishReason(record) !== null && incompleteAnswerReason(record) === null;
289
289
  }
290
290
  const NORMAL_FINISH_REASONS = /* @__PURE__ */ new Set([
291
291
  "stop",
@@ -307,6 +307,21 @@ function abnormalFinishReason(record) {
307
307
  const normalized = reason.toLowerCase().replace(/^finishreason\./, "");
308
308
  return NORMAL_FINISH_REASONS.has(normalized) ? null : normalized;
309
309
  }
310
+ const ANSWER_STANDS_REASONS = /* @__PURE__ */ new Set([
311
+ "recitation",
312
+ "max_tokens",
313
+ "length",
314
+ "max_output_tokens"
315
+ ]);
316
+ function incompleteAnswerReason(record) {
317
+ const reason = abnormalFinishReason(record);
318
+ if (!reason || !ANSWER_STANDS_REASONS.has(reason)) return null;
319
+ if (!record || record.status === "failed" || record.error) return null;
320
+ const md = record.metadata;
321
+ if (md && !Array.isArray(md) && md.failed === true)
322
+ return null;
323
+ return extractFlatText(record).trim().length > 0 ? reason : null;
324
+ }
310
325
  function earlyStopMessage(reason) {
311
326
  return reason === "max_tokens" || reason === "length" ? "The response hit its length limit before it finished." : "The response was stopped early by the model provider, so it may be incomplete.";
312
327
  }
@@ -581,6 +596,7 @@ export {
581
596
  extractInspectableText,
582
597
  extractRecordError,
583
598
  fenceBoundarySeparator,
599
+ incompleteAnswerReason,
584
600
  isFailedRecord,
585
601
  persistedBodyBlocks,
586
602
  selectApiEndpointMode,
@@ -0,0 +1,26 @@
1
+ /**
2
+ * ONE verdict for how a turn ended, read the same way live and after reload.
3
+ *
4
+ * - "failed" no usable answer, a safety/other stop, or the server saved it failed
5
+ * - "incomplete" the answer was delivered and stands, but the provider stopped
6
+ * early (recitation stop, length limit): content + a short notice
7
+ * - "complete" a normal finish
8
+ *
9
+ * Live reads the request status + stream warnings; reload reads the saved row
10
+ * (`isFailedRecord` / `incompleteAnswerReason`). A guard test drives both over
11
+ * the same cases so they cannot drift (incomplete-answer-live-equals-reload).
12
+ */
13
+ import type { WarningPayload } from "@ai-matrx/agents/generated/stream-events";
14
+ import type { MessageRecord } from "./messages.slice.js";
15
+ import { isAnswerKeptWarning } from "../active-requests/answer-kept-warning.js";
16
+ export { isAnswerKeptWarning };
17
+ export type TurnOutcome = "complete" | "incomplete" | "failed";
18
+ export declare function liveTurnOutcome(request: {
19
+ status: string;
20
+ warnings?: readonly WarningPayload[];
21
+ } | undefined): TurnOutcome;
22
+ export declare function reloadTurnOutcome(record: MessageRecord | undefined): TurnOutcome;
23
+ /** The short label on an incomplete answer. A label, not prose. */
24
+ export declare function incompleteLabel(reason: string | null): string;
25
+ /** The reason-specific sentence kept under Details, for a reloaded turn. */
26
+ export declare function incompleteDetail(reason: string): string;
@@ -0,0 +1,28 @@
1
+ import {
2
+ earlyStopMessage,
3
+ incompleteAnswerReason,
4
+ isFailedRecord
5
+ } from "./messages.selectors.js";
6
+ import { isAnswerKeptWarning } from "../active-requests/answer-kept-warning.js";
7
+ function liveTurnOutcome(request) {
8
+ if (!request) return "complete";
9
+ if (request.status === "error") return "failed";
10
+ return (request.warnings ?? []).some(isAnswerKeptWarning) ? "incomplete" : "complete";
11
+ }
12
+ function reloadTurnOutcome(record) {
13
+ if (isFailedRecord(record)) return "failed";
14
+ return incompleteAnswerReason(record) ? "incomplete" : "complete";
15
+ }
16
+ function incompleteLabel(reason) {
17
+ return reason === "max_tokens" || reason === "length" || reason === "max_output_tokens" || reason === "truncated_response" ? "Cut off at its length limit" : "Ending may be cut off";
18
+ }
19
+ function incompleteDetail(reason) {
20
+ return earlyStopMessage(reason);
21
+ }
22
+ export {
23
+ incompleteDetail,
24
+ incompleteLabel,
25
+ isAnswerKeptWarning,
26
+ liveTurnOutcome,
27
+ reloadTurnOutcome
28
+ };
@@ -13,7 +13,10 @@ import {
13
13
  Save
14
14
  } from "lucide-react";
15
15
  import { Button } from "@ai-matrx/design-system/controls";
16
- import { MarkdownStream, AudioOutputBlockSkeleton } from "../../../host/markdown-slots.js";
16
+ import {
17
+ MarkdownStream,
18
+ AudioOutputBlockSkeleton
19
+ } from "../../../host/markdown-slots.js";
17
20
  import { useDomCapturePrint } from "../../../conversation/hooks/useDomCapturePrint.js";
18
21
  import { useAppSelector, useAppDispatch } from "../../../store/hooks.js";
19
22
  import { selectMessageHasUnsavedChanges } from "../../_legacy-stubs.js";
@@ -193,14 +193,7 @@ function UserMessage({
193
193
  }
194
194
  ),
195
195
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 justify-end", children: [
196
- /* @__PURE__ */ jsx(
197
- Button,
198
- {
199
- variant: "quiet",
200
- onClick: handleEditCancel,
201
- children: "Cancel"
202
- }
203
- ),
196
+ /* @__PURE__ */ jsx(Button, { variant: "quiet", onClick: handleEditCancel, children: "Cancel" }),
204
197
  /* @__PURE__ */ jsx(
205
198
  Button,
206
199
  {
@@ -13,7 +13,10 @@ import {
13
13
  Save
14
14
  } from "lucide-react";
15
15
  import { Button } from "@ai-matrx/design-system/controls";
16
- import { MarkdownStream, AudioOutputBlockSkeleton } from "../host/markdown-slots.js";
16
+ import {
17
+ MarkdownStream,
18
+ AudioOutputBlockSkeleton
19
+ } from "../host/markdown-slots.js";
17
20
  import { useDomCapturePrint } from "../conversation/hooks/useDomCapturePrint.js";
18
21
  import { useAppSelector, useAppDispatch } from "../store/hooks.js";
19
22
  import { selectMessageHasUnsavedChanges } from "./_legacy-stubs.js";
@@ -193,14 +193,7 @@ function UserMessage({
193
193
  }
194
194
  ),
195
195
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 justify-end", children: [
196
- /* @__PURE__ */ jsx(
197
- Button,
198
- {
199
- variant: "quiet",
200
- onClick: handleEditCancel,
201
- children: "Cancel"
202
- }
203
- ),
196
+ /* @__PURE__ */ jsx(Button, { variant: "quiet", onClick: handleEditCancel, children: "Cancel" }),
204
197
  /* @__PURE__ */ jsx(
205
198
  Button,
206
199
  {
@@ -106,10 +106,17 @@ const ErrorView = ({
106
106
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
107
107
  routed ? /* @__PURE__ */ jsx(Route, { className: "h-3.5 w-3.5 text-muted-foreground" }) : /* @__PURE__ */ jsx(CircleAlert, { className: "h-3.5 w-3.5 text-destructive" }),
108
108
  /* @__PURE__ */ jsx("span", { children: routed ? "Routed" : "Error" }),
109
- entry.errorType && /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "px-1.5 py-0 font-mono text-[10px]", children: [
110
- entry.errorType,
111
- /* @__PURE__ */ jsx(ErrorAlchemyMenu, { error: entry.errorType })
112
- ] })
109
+ entry.errorType && /* @__PURE__ */ jsxs(
110
+ Badge,
111
+ {
112
+ variant: "outline",
113
+ className: "px-1.5 py-0 font-mono text-[10px]",
114
+ children: [
115
+ entry.errorType,
116
+ /* @__PURE__ */ jsx(ErrorAlchemyMenu, { error: entry.errorType })
117
+ ]
118
+ }
119
+ )
113
120
  ] }),
114
121
  /* @__PURE__ */ jsx(
115
122
  CopyButtons,
@@ -85,7 +85,17 @@ function VoiceDebugPanel({
85
85
  };
86
86
  return /* @__PURE__ */ jsxs("div", { className: "pointer-events-auto w-full overflow-hidden rounded-lg border border-border bg-zinc-950/95 text-zinc-100 shadow-lg backdrop-blur", children: [
87
87
  /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center justify-between gap-2 px-3 py-1.5", children: [
88
- /* @__PURE__ */ jsx(Tile, { variant: "quiet", icon: /* @__PURE__ */ jsx(Bug, {}), glyphTone: "success", title: "Live voice debug", onClick: () => setOpen((o) => !o), className: "flex-1" }),
88
+ /* @__PURE__ */ jsx(
89
+ Tile,
90
+ {
91
+ variant: "quiet",
92
+ icon: /* @__PURE__ */ jsx(Bug, {}),
93
+ glyphTone: "success",
94
+ title: "Live voice debug",
95
+ onClick: () => setOpen((o) => !o),
96
+ className: "flex-1"
97
+ }
98
+ ),
89
99
  /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
90
100
  /* @__PURE__ */ jsx(
91
101
  Badge,
@@ -106,7 +116,15 @@ function VoiceDebugPanel({
106
116
  glyphTone: copied ? "success" : void 0
107
117
  }
108
118
  ),
109
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: open ? /* @__PURE__ */ jsx(ChevronDown, {}) : /* @__PURE__ */ jsx(ChevronUp, {}), "aria-label": open ? "Collapse" : "Expand", onClick: () => setOpen((o) => !o) })
119
+ /* @__PURE__ */ jsx(
120
+ Button,
121
+ {
122
+ variant: "quiet",
123
+ icon: open ? /* @__PURE__ */ jsx(ChevronDown, {}) : /* @__PURE__ */ jsx(ChevronUp, {}),
124
+ "aria-label": open ? "Collapse" : "Expand",
125
+ onClick: () => setOpen((o) => !o)
126
+ }
127
+ )
110
128
  ] })
111
129
  ] }),
112
130
  open && /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-t border-zinc-800 px-3 py-2", children: [
@@ -164,7 +182,8 @@ function VoiceDebugPanel({
164
182
  " ",
165
183
  /* @__PURE__ */ jsxs("span", { className: "text-zinc-200", children: [
166
184
  flags.lastEventType ?? "\u2014",
167
- " \xB7 ",
185
+ " \xB7",
186
+ " ",
168
187
  formatRelativeTime(flags.lastEventAt)
169
188
  ] })
170
189
  ] }),
@@ -102,10 +102,21 @@ function SidebarRow({
102
102
  children: row.label
103
103
  }
104
104
  ),
105
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: (e) => {
106
- e.stopPropagation();
107
- copy();
108
- }, title: "Copy conversation id", "aria-label": "Copy conversation id", className: "opacity-0 group-hover:opacity-100 shrink-0" }),
105
+ /* @__PURE__ */ jsx(
106
+ Button,
107
+ {
108
+ variant: "quiet",
109
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
110
+ glyphTone: copied ? "success" : void 0,
111
+ onClick: (e) => {
112
+ e.stopPropagation();
113
+ copy();
114
+ },
115
+ title: "Copy conversation id",
116
+ "aria-label": "Copy conversation id",
117
+ className: "opacity-0 group-hover:opacity-100 shrink-0"
118
+ }
119
+ ),
109
120
  /* @__PURE__ */ jsx(
110
121
  EntityDoorControls,
111
122
  {
@@ -354,7 +365,11 @@ function ObservationalMemoryWindowInner({
354
365
  sourceFeature: "agent-builder",
355
366
  surfaceName: OBSERVATIONAL_MEMORY_SURFACE_NAME,
356
367
  contentSource: { type: "raw" },
357
- entity: selectedConversationId ? { type: "cx_observational_memory", id: selectedConversationId, title: "Observational memory" } : void 0,
368
+ entity: selectedConversationId ? {
369
+ type: "cx_observational_memory",
370
+ id: selectedConversationId,
371
+ title: "Observational memory"
372
+ } : void 0,
358
373
  children: selectedConversationId ? /* @__PURE__ */ jsx(
359
374
  ObservationalMemoryCore,
360
375
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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 per-module subpaths @ai-matrx/chat/<domain>/<module> over a host contract (/host, /host/react) whose only required value is a db; plus the kernel compatibility entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) until P28.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -201,24 +201,24 @@
201
201
  "server-only": "^0.0.1",
202
202
  "uuid": "^14.0.2",
203
203
  "zod": "^4.6.5",
204
+ "@ai-matrx/alchemy": "latest",
204
205
  "@ai-matrx/agents": "latest",
205
206
  "@ai-matrx/browser-audio": "latest",
206
- "@ai-matrx/associations": "latest",
207
- "@ai-matrx/alchemy": "latest",
208
207
  "@ai-matrx/canvas": "latest",
209
208
  "@ai-matrx/content-ir": "latest",
210
209
  "@ai-matrx/content-ir-react": "latest",
211
210
  "@ai-matrx/data": "latest",
212
- "@ai-matrx/diff": "latest",
213
211
  "@ai-matrx/design-system": "latest",
214
- "@ai-matrx/icons": "latest",
215
- "@ai-matrx/media": "latest",
212
+ "@ai-matrx/associations": "latest",
213
+ "@ai-matrx/diff": "latest",
216
214
  "@ai-matrx/kit": "latest",
217
- "@ai-matrx/print": "latest",
215
+ "@ai-matrx/media": "latest",
216
+ "@ai-matrx/icons": "latest",
218
217
  "@ai-matrx/realtime": "latest",
218
+ "@ai-matrx/records": "latest",
219
+ "@ai-matrx/print": "latest",
219
220
  "@ai-matrx/records-ui": "latest",
220
- "@ai-matrx/tap-target": "latest",
221
- "@ai-matrx/records": "latest"
221
+ "@ai-matrx/tap-target": "latest"
222
222
  },
223
223
  "devDependencies": {
224
224
  "@arethetypeswrong/cli": "^0.18.5",