@ai-matrx/chat 0.3.2 → 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.
- package/CHANGELOG.md +18 -0
- package/dist/agent-copy/export.d.ts +1 -1
- package/dist/agent-copy/export.js +5 -16
- package/dist/agents/components/context-items/bodies/WebpageBody.js +12 -5
- package/dist/agents/components/debug/SandboxFileViewer.js +31 -22
- package/dist/agents/components/messages-display/TranscriptIntegrityCopyButton.js +14 -3
- package/dist/agents/components/messages-display/assistant/AgentAssistantMessage.js +18 -1
- package/dist/agents/components/run/AssistantWarning.js +12 -0
- package/dist/agents/components/run/IncompleteNote.d.ts +14 -0
- package/dist/agents/components/run/IncompleteNote.js +21 -0
- package/dist/agents/components/run-controls/PayloadTab.js +5 -1
- package/dist/agents/components/run-controls/panels/BackendTargetPanel.js +12 -1
- package/dist/agents/hooks/useAgentAutoSave.js +8 -3
- package/dist/agents/redux/execution-system/active-requests/active-requests.selectors.d.ts +2 -1
- package/dist/agents/redux/execution-system/active-requests/active-requests.selectors.js +2 -1
- package/dist/agents/redux/execution-system/active-requests/answer-kept-warning.d.ts +7 -0
- package/dist/agents/redux/execution-system/active-requests/answer-kept-warning.js +12 -0
- package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateCore.js +68 -9
- package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateList.js +30 -8
- package/dist/agents/redux/execution-system/messages/messages.selectors.d.ts +16 -0
- package/dist/agents/redux/execution-system/messages/messages.selectors.js +47 -1
- package/dist/agents/redux/execution-system/messages/turn-outcome.d.ts +26 -0
- package/dist/agents/redux/execution-system/messages/turn-outcome.js +28 -0
- package/dist/agents/redux/execution-system/thunks/process-stream.js +17 -0
- package/dist/agents/redux/execution-system/thunks/terminal-request-failure.d.ts +27 -0
- package/dist/agents/redux/execution-system/thunks/terminal-request-failure.js +56 -0
- package/dist/cx-chat/components/messages/AssistantMessage.js +4 -1
- package/dist/cx-chat/components/messages/UserMessage.js +1 -8
- package/dist/cx-conversation/AssistantMessage.js +4 -1
- package/dist/cx-conversation/UserMessage.js +1 -8
- package/dist/tool-call-visualization/components/ToolTabBodies.js +11 -4
- package/dist/voice-agent/components/VoiceDebugPanel.js +22 -3
- package/dist/window-panels/windows/agents/ObservationalMemoryWindow.js +20 -5
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
12
|
+
## 0.3.3 — 2026-10-07
|
|
13
|
+
### Re-sync before matrx-frontend consumes the published package (P27)
|
|
14
|
+
|
|
15
|
+
- Carries every
|
|
16
|
+
matrx-frontend `packages/chat` edit after `7f630d97d9`: a server-ended failed request is a failed turn
|
|
17
|
+
(`thunks/terminal-request-failure`, process-stream, messages selectors), agent autosave recovery.
|
|
18
|
+
Its suite needs the app's content-ir host, so it lives app-side with the other host suites.
|
|
19
|
+
- Consumer action: none.
|
|
20
|
+
|
|
3
21
|
## 0.3.2
|
|
4
22
|
|
|
5
23
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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(
|
|
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(
|
|
26
|
+
adapterRef.current = createSandboxFilesystemAdapter(
|
|
27
|
+
sandboxRowId
|
|
28
|
+
);
|
|
27
29
|
}
|
|
28
|
-
const load = useCallback(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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({
|
|
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(
|
|
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({
|
|
@@ -90,9 +90,14 @@ function useAgentAutoSave(agentId) {
|
|
|
90
90
|
try {
|
|
91
91
|
const snapshot = { _dirty: true };
|
|
92
92
|
if (record._dirtyFields) {
|
|
93
|
-
|
|
94
|
-
record._dirtyFields
|
|
95
|
-
)
|
|
93
|
+
const fields = new Set(
|
|
94
|
+
Object.keys(record._dirtyFields)
|
|
95
|
+
);
|
|
96
|
+
if (fields.has("modelId") || fields.has("settings")) {
|
|
97
|
+
fields.add("modelId");
|
|
98
|
+
fields.add("settings");
|
|
99
|
+
}
|
|
100
|
+
for (const field of fields) {
|
|
96
101
|
snapshot[field] = readField(record, field);
|
|
97
102
|
}
|
|
98
103
|
}
|
|
@@ -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
|
|
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
|
+
};
|
package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateCore.js
CHANGED
|
@@ -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(
|
|
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(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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(
|
|
199
|
-
|
|
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(
|
|
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(
|
|
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
|
package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateList.js
CHANGED
|
@@ -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(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
),
|
|
@@ -169,6 +169,22 @@ export declare function extractInspectableText(record: MessageRecord | undefined
|
|
|
169
169
|
* CONVERSATION_FAILURE_AND_RETRY_FE_GUIDE.md.
|
|
170
170
|
*/
|
|
171
171
|
export declare function isFailedRecord(record: MessageRecord | undefined): boolean;
|
|
172
|
+
/**
|
|
173
|
+
* The provider stop reason a reloaded turn was saved with, when it is NOT a
|
|
174
|
+
* normal finish — or null. A turn the provider stopped early can be saved as
|
|
175
|
+
* an ordinary `active` message with its partial text and only
|
|
176
|
+
* `metadata.finish_reason` telling the truth (a recitation stop, 2026-10-06);
|
|
177
|
+
* that row reloaded with no error at all.
|
|
178
|
+
*/
|
|
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;
|
|
186
|
+
/** The sentence a reloaded early-stopped turn shows. */
|
|
187
|
+
export declare function earlyStopMessage(reason: string): string;
|
|
172
188
|
/**
|
|
173
189
|
* The persisted error text for a failed turn. Prefers the structured top-level
|
|
174
190
|
* `error.message`; falls back to the legacy `metadata.error` string, then to
|
|
@@ -282,7 +282,48 @@ function isFailedRecord(record) {
|
|
|
282
282
|
if (record.status === "failed") return true;
|
|
283
283
|
if (record.error) return true;
|
|
284
284
|
const md = record.metadata;
|
|
285
|
-
|
|
285
|
+
if (!!md && typeof md === "object" && !Array.isArray(md) && md.failed === true) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
return abnormalFinishReason(record) !== null && incompleteAnswerReason(record) === null;
|
|
289
|
+
}
|
|
290
|
+
const NORMAL_FINISH_REASONS = /* @__PURE__ */ new Set([
|
|
291
|
+
"stop",
|
|
292
|
+
"end_turn",
|
|
293
|
+
"stop_sequence",
|
|
294
|
+
"tool_use",
|
|
295
|
+
"tool_calls",
|
|
296
|
+
"function_call",
|
|
297
|
+
"pause_turn",
|
|
298
|
+
"completed",
|
|
299
|
+
"success"
|
|
300
|
+
]);
|
|
301
|
+
function abnormalFinishReason(record) {
|
|
302
|
+
if (!record || record.role !== "assistant") return null;
|
|
303
|
+
const md = record.metadata;
|
|
304
|
+
if (!md || typeof md !== "object" || Array.isArray(md)) return null;
|
|
305
|
+
const reason = md.finish_reason;
|
|
306
|
+
if (typeof reason !== "string" || reason.length === 0) return null;
|
|
307
|
+
const normalized = reason.toLowerCase().replace(/^finishreason\./, "");
|
|
308
|
+
return NORMAL_FINISH_REASONS.has(normalized) ? null : normalized;
|
|
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
|
+
}
|
|
325
|
+
function earlyStopMessage(reason) {
|
|
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.";
|
|
286
327
|
}
|
|
287
328
|
function extractRecordError(record) {
|
|
288
329
|
if (!record) return void 0;
|
|
@@ -294,6 +335,8 @@ function extractRecordError(record) {
|
|
|
294
335
|
const err = md.error;
|
|
295
336
|
if (typeof err === "string" && err.length > 0) return err;
|
|
296
337
|
}
|
|
338
|
+
const stopped = abnormalFinishReason(record);
|
|
339
|
+
if (stopped) return earlyStopMessage(stopped);
|
|
297
340
|
const flat = extractFlatText(record);
|
|
298
341
|
return flat.length > 0 ? flat : void 0;
|
|
299
342
|
}
|
|
@@ -545,12 +588,15 @@ const selectMessagesInterleavedRuns = (conversationId, messageIds) => {
|
|
|
545
588
|
};
|
|
546
589
|
export {
|
|
547
590
|
EMPTY_CONVERSATION_MESSAGES,
|
|
591
|
+
abnormalFinishReason,
|
|
592
|
+
earlyStopMessage,
|
|
548
593
|
extractAnswerDocumentText,
|
|
549
594
|
extractContentBlocks,
|
|
550
595
|
extractFlatText,
|
|
551
596
|
extractInspectableText,
|
|
552
597
|
extractRecordError,
|
|
553
598
|
fenceBoundarySeparator,
|
|
599
|
+
incompleteAnswerReason,
|
|
554
600
|
isFailedRecord,
|
|
555
601
|
persistedBodyBlocks,
|
|
556
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
|
+
};
|
|
@@ -69,6 +69,10 @@ import {
|
|
|
69
69
|
appendCitation
|
|
70
70
|
} from "../active-requests/active-requests.slice.js";
|
|
71
71
|
import { parseNormalizedCitation } from "../messages/message-citations.js";
|
|
72
|
+
import {
|
|
73
|
+
terminalFailureFromCompletion,
|
|
74
|
+
terminalFailureFromRecordUpdate
|
|
75
|
+
} from "./terminal-request-failure.js";
|
|
72
76
|
import { confirmServerSync } from "../conversations/conversations.slice.js";
|
|
73
77
|
import { receivedFsChange } from "../../../../host/ui-slots.js";
|
|
74
78
|
import {
|
|
@@ -430,6 +434,17 @@ async function processStream({
|
|
|
430
434
|
terminalTransportController.abort("post-terminal-grace");
|
|
431
435
|
}, POST_TERMINAL_GRACE_MS);
|
|
432
436
|
};
|
|
437
|
+
const failRequestFromTerminalStatus = (error) => {
|
|
438
|
+
if (!error) return;
|
|
439
|
+
if (getState().activeRequests.byRequestId[requestId]?.status === "error") {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
console.warn(
|
|
443
|
+
`[stream:${requestId.slice(0, 8)}] user request ended FAILED without an error event (${error.error_type}) \u2014 surfacing it as a failed turn.`
|
|
444
|
+
);
|
|
445
|
+
dispatch(setRequestStatus({ requestId, status: "error", error }));
|
|
446
|
+
dispatch(setInstanceStatus({ conversationId, status: "error" }));
|
|
447
|
+
};
|
|
433
448
|
let lastTransportSeq = getState().activeRequests.byRequestId[requestId]?.lastTransportSeq ?? 0;
|
|
434
449
|
let transportStreamId = getState().activeRequests.byRequestId[requestId]?.transportStreamId ?? null;
|
|
435
450
|
let retainedEntry;
|
|
@@ -635,6 +650,7 @@ async function processStream({
|
|
|
635
650
|
}
|
|
636
651
|
if (d.operation === "user_request") {
|
|
637
652
|
dispatch(setCompletion({ requestId, data: d }));
|
|
653
|
+
failRequestFromTerminalStatus(terminalFailureFromCompletion(d));
|
|
638
654
|
completionStats = result;
|
|
639
655
|
const totals = completionStats.total_usage?.total;
|
|
640
656
|
if (totals) {
|
|
@@ -1664,6 +1680,7 @@ async function processStream({
|
|
|
1664
1680
|
refreshUserMessageWithoutBody(d.record_id);
|
|
1665
1681
|
}
|
|
1666
1682
|
} else if (d.table === "user_request") {
|
|
1683
|
+
failRequestFromTerminalStatus(terminalFailureFromRecordUpdate(d));
|
|
1667
1684
|
dispatch(
|
|
1668
1685
|
patchUserRequest({
|
|
1669
1686
|
id: d.record_id,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A request that ENDED FAILED without an `error` event.
|
|
3
|
+
*
|
|
4
|
+
* The server has three ways to say a user request failed, and only one of
|
|
5
|
+
* them is the `error` event: a terminal failure it persisted on purpose (a
|
|
6
|
+
* provider safety / recitation stop, a model refusal, any finish-reason
|
|
7
|
+
* failure) is announced as a `user_request` completion with
|
|
8
|
+
* `status: "failed"` and/or a `record_update` moving the `user_request` row
|
|
9
|
+
* to `failed`, then a normal `end`. Before this module the stream treated
|
|
10
|
+
* both as bookkeeping, the `end` handler marked the request `complete`, and
|
|
11
|
+
* the turn showed NO error — while its half-written content (a flashcard set
|
|
12
|
+
* cut off by a recitation stop, 2026-10-06) sat there looking finished.
|
|
13
|
+
*
|
|
14
|
+
* Pure: reads the event payload, returns the `ErrorPayload` the request
|
|
15
|
+
* should carry, or null when the event is not a terminal failure. It never
|
|
16
|
+
* matches on the message TEXT (the server is free to reword it) — only on
|
|
17
|
+
* the operation/table, the status, and the structured fields.
|
|
18
|
+
*/
|
|
19
|
+
import type { CompletionPayload, ErrorPayload, RecordUpdatePayload } from "@ai-matrx/agents/generated/stream-events";
|
|
20
|
+
/** error_type when the server sent no structured one. */
|
|
21
|
+
export declare const TERMINAL_FAILURE_ERROR_TYPE = "request_failed";
|
|
22
|
+
/** Shown when the server said "failed" and nothing more. */
|
|
23
|
+
export declare const TERMINAL_FAILURE_FALLBACK_MESSAGE = "The response stopped before it finished.";
|
|
24
|
+
/** A `user_request` completion whose status is not success. */
|
|
25
|
+
export declare function terminalFailureFromCompletion(d: CompletionPayload): ErrorPayload | null;
|
|
26
|
+
/** A `record_update` moving the `user_request` row to `failed`. */
|
|
27
|
+
export declare function terminalFailureFromRecordUpdate(d: RecordUpdatePayload): ErrorPayload | null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const TERMINAL_FAILURE_ERROR_TYPE = "request_failed";
|
|
2
|
+
const TERMINAL_FAILURE_FALLBACK_MESSAGE = "The response stopped before it finished.";
|
|
3
|
+
function asRecord(value) {
|
|
4
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
5
|
+
}
|
|
6
|
+
function readStructuredError(raw) {
|
|
7
|
+
if (typeof raw === "string") {
|
|
8
|
+
const trimmed = raw.trim();
|
|
9
|
+
if (trimmed.startsWith("{")) {
|
|
10
|
+
try {
|
|
11
|
+
return readStructuredError(JSON.parse(trimmed));
|
|
12
|
+
} catch {
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return { errorType: null, message: trimmed || null };
|
|
16
|
+
}
|
|
17
|
+
const obj = asRecord(raw);
|
|
18
|
+
if (!obj) return { errorType: null, message: null };
|
|
19
|
+
const errorType = typeof obj.error_type === "string" ? obj.error_type : typeof obj.type === "string" ? obj.type : null;
|
|
20
|
+
const message = typeof obj.user_message === "string" && obj.user_message ? obj.user_message : typeof obj.message === "string" && obj.message ? obj.message : null;
|
|
21
|
+
return { errorType, message };
|
|
22
|
+
}
|
|
23
|
+
function buildPayload(source, fields, finishReason) {
|
|
24
|
+
const structured = readStructuredError(fields?.error);
|
|
25
|
+
const errorType = structured.errorType ?? (typeof fields?.error_type === "string" ? fields.error_type : null) ?? TERMINAL_FAILURE_ERROR_TYPE;
|
|
26
|
+
const message = structured.message ?? TERMINAL_FAILURE_FALLBACK_MESSAGE;
|
|
27
|
+
return {
|
|
28
|
+
error_type: errorType,
|
|
29
|
+
message,
|
|
30
|
+
user_message: message,
|
|
31
|
+
details: {
|
|
32
|
+
source,
|
|
33
|
+
...finishReason ? { finish_reason: finishReason } : {},
|
|
34
|
+
...typeof fields?.reason === "string" ? { reason: fields.reason } : {}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function terminalFailureFromCompletion(d) {
|
|
39
|
+
if (d.operation !== "user_request" || d.status !== "failed") return null;
|
|
40
|
+
const result = asRecord(d.result);
|
|
41
|
+
const metadata = asRecord(result?.metadata);
|
|
42
|
+
const finishReason = typeof result?.finish_reason === "string" ? result.finish_reason : null;
|
|
43
|
+
return buildPayload("completion", metadata ?? result, finishReason);
|
|
44
|
+
}
|
|
45
|
+
function terminalFailureFromRecordUpdate(d) {
|
|
46
|
+
if (d.table !== "user_request" || d.status !== "failed") return null;
|
|
47
|
+
const metadata = asRecord(d.metadata);
|
|
48
|
+
const finishReason = typeof metadata?.finish_reason === "string" ? metadata.finish_reason : null;
|
|
49
|
+
return buildPayload("record_update", metadata, finishReason);
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
TERMINAL_FAILURE_ERROR_TYPE,
|
|
53
|
+
TERMINAL_FAILURE_FALLBACK_MESSAGE,
|
|
54
|
+
terminalFailureFromCompletion,
|
|
55
|
+
terminalFailureFromRecordUpdate
|
|
56
|
+
};
|
|
@@ -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 {
|
|
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 {
|
|
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(
|
|
110
|
-
|
|
111
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 ? {
|
|
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
|
+
"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/
|
|
205
|
-
"@ai-matrx/browser-audio": "latest",
|
|
204
|
+
"@ai-matrx/alchemy": "latest",
|
|
206
205
|
"@ai-matrx/agents": "latest",
|
|
206
|
+
"@ai-matrx/browser-audio": "latest",
|
|
207
207
|
"@ai-matrx/canvas": "latest",
|
|
208
208
|
"@ai-matrx/content-ir": "latest",
|
|
209
209
|
"@ai-matrx/content-ir-react": "latest",
|
|
210
210
|
"@ai-matrx/data": "latest",
|
|
211
211
|
"@ai-matrx/design-system": "latest",
|
|
212
|
+
"@ai-matrx/associations": "latest",
|
|
212
213
|
"@ai-matrx/diff": "latest",
|
|
213
|
-
"@ai-matrx/
|
|
214
|
+
"@ai-matrx/kit": "latest",
|
|
214
215
|
"@ai-matrx/media": "latest",
|
|
216
|
+
"@ai-matrx/icons": "latest",
|
|
215
217
|
"@ai-matrx/realtime": "latest",
|
|
216
218
|
"@ai-matrx/records": "latest",
|
|
219
|
+
"@ai-matrx/print": "latest",
|
|
217
220
|
"@ai-matrx/records-ui": "latest",
|
|
218
|
-
"@ai-matrx/
|
|
219
|
-
"@ai-matrx/tap-target": "latest",
|
|
220
|
-
"@ai-matrx/alchemy": "latest",
|
|
221
|
-
"@ai-matrx/print": "latest"
|
|
221
|
+
"@ai-matrx/tap-target": "latest"
|
|
222
222
|
},
|
|
223
223
|
"devDependencies": {
|
|
224
224
|
"@arethetypeswrong/cli": "^0.18.5",
|