@ai-matrx/chat 0.3.2 → 0.3.3
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 +9 -0
- package/dist/agents/hooks/useAgentAutoSave.js +8 -3
- package/dist/agents/redux/execution-system/messages/messages.selectors.d.ts +10 -0
- package/dist/agents/redux/execution-system/messages/messages.selectors.js +31 -1
- 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/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @ai-matrx/chat
|
|
2
2
|
|
|
3
|
+
## 0.3.3 — 2026-10-07
|
|
4
|
+
### Re-sync before matrx-frontend consumes the published package (P27)
|
|
5
|
+
|
|
6
|
+
- Carries every
|
|
7
|
+
matrx-frontend `packages/chat` edit after `7f630d97d9`: a server-ended failed request is a failed turn
|
|
8
|
+
(`thunks/terminal-request-failure`, process-stream, messages selectors), agent autosave recovery.
|
|
9
|
+
Its suite needs the app's content-ir host, so it lives app-side with the other host suites.
|
|
10
|
+
- Consumer action: none.
|
|
11
|
+
|
|
3
12
|
## 0.3.2
|
|
4
13
|
|
|
5
14
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
@@ -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
|
}
|
|
@@ -169,6 +169,16 @@ 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
|
+
/** The sentence a reloaded early-stopped turn shows. */
|
|
181
|
+
export declare function earlyStopMessage(reason: string): string;
|
|
172
182
|
/**
|
|
173
183
|
* The persisted error text for a failed turn. Prefers the structured top-level
|
|
174
184
|
* `error.message`; falls back to the legacy `metadata.error` string, then to
|
|
@@ -282,7 +282,33 @@ 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;
|
|
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
|
+
function earlyStopMessage(reason) {
|
|
311
|
+
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
312
|
}
|
|
287
313
|
function extractRecordError(record) {
|
|
288
314
|
if (!record) return void 0;
|
|
@@ -294,6 +320,8 @@ function extractRecordError(record) {
|
|
|
294
320
|
const err = md.error;
|
|
295
321
|
if (typeof err === "string" && err.length > 0) return err;
|
|
296
322
|
}
|
|
323
|
+
const stopped = abnormalFinishReason(record);
|
|
324
|
+
if (stopped) return earlyStopMessage(stopped);
|
|
297
325
|
const flat = extractFlatText(record);
|
|
298
326
|
return flat.length > 0 ? flat : void 0;
|
|
299
327
|
}
|
|
@@ -545,6 +573,8 @@ const selectMessagesInterleavedRuns = (conversationId, messageIds) => {
|
|
|
545
573
|
};
|
|
546
574
|
export {
|
|
547
575
|
EMPTY_CONVERSATION_MESSAGES,
|
|
576
|
+
abnormalFinishReason,
|
|
577
|
+
earlyStopMessage,
|
|
548
578
|
extractAnswerDocumentText,
|
|
549
579
|
extractContentBlocks,
|
|
550
580
|
extractFlatText,
|
|
@@ -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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/chat",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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/associations": "latest",
|
|
205
|
-
"@ai-matrx/browser-audio": "latest",
|
|
206
204
|
"@ai-matrx/agents": "latest",
|
|
205
|
+
"@ai-matrx/browser-audio": "latest",
|
|
206
|
+
"@ai-matrx/associations": "latest",
|
|
207
|
+
"@ai-matrx/alchemy": "latest",
|
|
207
208
|
"@ai-matrx/canvas": "latest",
|
|
208
209
|
"@ai-matrx/content-ir": "latest",
|
|
209
210
|
"@ai-matrx/content-ir-react": "latest",
|
|
210
211
|
"@ai-matrx/data": "latest",
|
|
211
|
-
"@ai-matrx/design-system": "latest",
|
|
212
212
|
"@ai-matrx/diff": "latest",
|
|
213
|
+
"@ai-matrx/design-system": "latest",
|
|
213
214
|
"@ai-matrx/icons": "latest",
|
|
214
215
|
"@ai-matrx/media": "latest",
|
|
216
|
+
"@ai-matrx/kit": "latest",
|
|
217
|
+
"@ai-matrx/print": "latest",
|
|
215
218
|
"@ai-matrx/realtime": "latest",
|
|
216
|
-
"@ai-matrx/records": "latest",
|
|
217
219
|
"@ai-matrx/records-ui": "latest",
|
|
218
|
-
"@ai-matrx/kit": "latest",
|
|
219
220
|
"@ai-matrx/tap-target": "latest",
|
|
220
|
-
"@ai-matrx/
|
|
221
|
-
"@ai-matrx/print": "latest"
|
|
221
|
+
"@ai-matrx/records": "latest"
|
|
222
222
|
},
|
|
223
223
|
"devDependencies": {
|
|
224
224
|
"@arethetypeswrong/cli": "^0.18.5",
|