@ai-matrx/chat 0.7.11 → 0.7.13
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 +28 -0
- package/dist/agents/components/messages-display/assistant/AgentAssistantMessage.js +2 -1
- package/dist/agents/hooks/useFloatingAgentRun.d.ts +11 -1
- package/dist/agents/hooks/useFloatingAgentRun.js +12 -0
- package/dist/agents/hooks/useHeadlessAgentJson.d.ts +14 -0
- package/dist/agents/hooks/useHeadlessAgentJson.js +48 -26
- package/dist/agents/hooks/useLiveAgentRun.d.ts +8 -1
- package/dist/agents/hooks/useLiveAgentRun.js +15 -0
- package/dist/agents/redux/execution-system/instance-variable-values/instance-variable-values.selectors.d.ts +70 -0
- package/dist/agents/redux/execution-system/instance-variable-values/instance-variable-values.selectors.js +15 -1
- package/dist/agents/redux/execution-system/thunks/reattach-headless-agent-json.d.ts +29 -0
- package/dist/agents/redux/execution-system/thunks/reattach-headless-agent-json.js +89 -0
- package/dist/agents/redux/execution-system/thunks/record-run-output-refused.d.ts +10 -1
- package/dist/agents/redux/execution-system/thunks/record-run-output-refused.js +24 -6
- package/dist/ui/markdown-stream/MarkdownStream.d.ts +2 -1
- package/dist/ui/markdown-stream/MarkdownStream.js +24 -5
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @ai-matrx/chat
|
|
2
2
|
|
|
3
|
+
## 0.7.13
|
|
4
|
+
### Unreleased
|
|
5
|
+
### a refused answer fails the run's own row (lane F13)
|
|
6
|
+
|
|
7
|
+
- **`recordRunOutputRefused(runRowId, reason, requestId?)`** writes against the `chat.user_request` row id the server announced
|
|
8
|
+
(`record_reserved`), read with the new **`runRowIdOf(state, requestId)`**. 0.7.10 wrote against the client's Redux key
|
|
9
|
+
(`req_<uuid>`), which no row has: every refused run on Make stayed `completed`, error null (live 2026-10-09). A refusal with
|
|
10
|
+
no row id is a `console.error`, never a silent no-op.
|
|
11
|
+
- Tests: `a-refused-answer-fails-the-servers-run-row.test.ts`.
|
|
12
|
+
- **Consumer action:** none (`useHeadlessAgentJson` is the only caller).
|
|
13
|
+
|
|
14
|
+
## 0.7.12
|
|
15
|
+
### Unreleased
|
|
16
|
+
### The variables a run used (Agent Factory R59)
|
|
17
|
+
|
|
18
|
+
- **`selectVariablesTheRunUsed(conversationId)`** (instance-variable-values selectors): the values a run's first turn was sent
|
|
19
|
+
with, frozen at send; before a send, the request merge. The builder's Side Chat helper reads it so a later edit of the field
|
|
20
|
+
cannot rewrite what the run was given.
|
|
21
|
+
- **Consumer action:** none.
|
|
22
|
+
|
|
23
|
+
### A persisted answer paints with its markdown (spend register item 32, cold-load CLS at 1440)
|
|
24
|
+
|
|
25
|
+
- **`MarkdownStream`**: the engine chunk is requested when a conversation surface's module loads (`preloadMarkdownStream`), kept once
|
|
26
|
+
resolved, and rendered synchronously on the first render of a new instance. The per-instance `mounted` effect gate (an extra commit
|
|
27
|
+
after paint) is now `useSyncExternalStore` (still client-only: server and hydration render nothing). Before, the answer block painted
|
|
28
|
+
as a 36 px header and grew to its full height ~200 ms later, shifting everything below it (CLS 0.06-0.28 at 1440).
|
|
29
|
+
- **Consumer action:** none.
|
|
30
|
+
|
|
3
31
|
## 0.7.11
|
|
4
32
|
### continue-agent-json is exported again
|
|
5
33
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
useState,
|
|
7
7
|
useEffect
|
|
8
8
|
} from "react";
|
|
9
|
-
import MarkdownStream from "../../../../ui/markdown-stream/MarkdownStream.js";
|
|
9
|
+
import MarkdownStream, { preloadMarkdownStream } from "../../../../ui/markdown-stream/MarkdownStream.js";
|
|
10
10
|
import "../rich-document/chat-message-source.js";
|
|
11
11
|
import { RecordAnnotations, annotationRecordOf } from "@ai-matrx/rich-content/host/app-bindings";
|
|
12
12
|
import { useAppDispatch, useAppSelector } from "../../../../store/hooks.js";
|
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
} from "../../../../host/ui-slots.js";
|
|
92
92
|
import { ErrorAlchemyMenu } from "../../../../host/ui-slots.js";
|
|
93
93
|
import { useInPlaceTrigger } from "@ai-matrx/rich-editor/in-place/EditInPlace";
|
|
94
|
+
void preloadMarkdownStream();
|
|
94
95
|
const _NO_LIVE_SOURCES = [];
|
|
95
96
|
const _selectNoLiveSources = () => _NO_LIVE_SOURCES;
|
|
96
97
|
function retryErrorMessage(err) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type LiveAgentRunOptions } from "./useLiveAgentRun.js";
|
|
1
|
+
import { type LiveAgentReattachOptions, type LiveAgentRunOptions } from "./useLiveAgentRun.js";
|
|
2
2
|
/**
|
|
3
3
|
* The words a failed launch shows in its window: the server's own reason when
|
|
4
4
|
* the run carried one (a Mandate with no Holder names the rungs it checked),
|
|
@@ -83,6 +83,16 @@ export interface UseFloatingAgentRun {
|
|
|
83
83
|
run: <T = unknown>(opts: LiveAgentRunOptions<T> & {
|
|
84
84
|
label?: string;
|
|
85
85
|
}) => Promise<T>;
|
|
86
|
+
/**
|
|
87
|
+
* Pick an EXISTING run back up — a reload or a paused tab must never start a
|
|
88
|
+
* second one. The conversation id is what `onConversationCreated` handed out
|
|
89
|
+
* at launch. Resolves with the finished result when the run is done, or
|
|
90
|
+
* follows it to the end when it is still going, in the same window. No run is
|
|
91
|
+
* started.
|
|
92
|
+
*/
|
|
93
|
+
reattach: <T = unknown>(conversationId: string, opts: LiveAgentReattachOptions<T> & {
|
|
94
|
+
label?: string;
|
|
95
|
+
}) => Promise<T>;
|
|
86
96
|
isRunning: boolean;
|
|
87
97
|
error: string | null;
|
|
88
98
|
conversationId: string | null;
|
|
@@ -123,8 +123,20 @@ function useFloatingAgentRun(options = {}) {
|
|
|
123
123
|
throw error;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
+
async function reattach(existingConversationId, opts) {
|
|
127
|
+
const { label, ...reattachOptions } = opts;
|
|
128
|
+
const bound = floating.start(label ?? options.label ?? "AI is working");
|
|
129
|
+
bound.bind(existingConversationId);
|
|
130
|
+
try {
|
|
131
|
+
return await live.reattach(existingConversationId, reattachOptions);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
bound.fail(floatingFailureReason(error));
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
126
137
|
return {
|
|
127
138
|
run,
|
|
139
|
+
reattach,
|
|
128
140
|
isRunning: live.isRunning,
|
|
129
141
|
error: live.error,
|
|
130
142
|
conversationId: live.conversationId,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReattachHeadlessAgentJsonOptions } from "../redux/execution-system/thunks/reattach-headless-agent-json.js";
|
|
1
2
|
import { type HeadlessAgentJsonOptions, type HeadlessAgentJsonResult } from "../redux/execution-system/thunks/run-headless-agent-json.js";
|
|
2
3
|
/**
|
|
3
4
|
* What `run()` throws. It carries the run's TECHNICAL reason (when the run
|
|
@@ -27,8 +28,21 @@ export interface HeadlessAgentJsonRunOptions<T> extends HeadlessAgentJsonOptions
|
|
|
27
28
|
*/
|
|
28
29
|
coerce?: (value: unknown, result: HeadlessAgentJsonResult) => T;
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Reopen a run that ALREADY exists (running or finished) — never starts one.
|
|
33
|
+
* `conversationId` is what `onConversationCreated` handed out at launch.
|
|
34
|
+
*/
|
|
35
|
+
export interface HeadlessAgentJsonReattachOptions<T> extends Omit<ReattachHeadlessAgentJsonOptions, "conversationId"> {
|
|
36
|
+
coerce?: (value: unknown, result: HeadlessAgentJsonResult) => T;
|
|
37
|
+
}
|
|
30
38
|
export interface UseHeadlessAgentJson {
|
|
31
39
|
run: <T = unknown>(opts: HeadlessAgentJsonRunOptions<T>) => Promise<T>;
|
|
40
|
+
/**
|
|
41
|
+
* Pick an existing run back up: its finished result when it is done, or
|
|
42
|
+
* follow it to the end when it is still going. No new run is ever started.
|
|
43
|
+
* Throws exactly like `run`.
|
|
44
|
+
*/
|
|
45
|
+
reattach: <T = unknown>(conversationId: string, opts: HeadlessAgentJsonReattachOptions<T>) => Promise<T>;
|
|
32
46
|
isRunning: boolean;
|
|
33
47
|
error: string | null;
|
|
34
48
|
/** Conversation id of the in-flight run — set BEFORE the stream starts. */
|
|
@@ -3,7 +3,10 @@ import { useCallback, useState } from "react";
|
|
|
3
3
|
import { useAppDispatch, useAppSelector, useAppStore } from "../../store/hooks.js";
|
|
4
4
|
import { selectConversationRequestIds } from "../redux/execution-system/active-requests/active-requests.selectors.js";
|
|
5
5
|
import { useRetainRequestForViewer } from "../redux/execution-system/active-requests/useRetainRequestForViewer.js";
|
|
6
|
-
import { recordRunOutputRefused } from "../redux/execution-system/thunks/record-run-output-refused.js";
|
|
6
|
+
import { recordRunOutputRefused, runRowIdOf } from "../redux/execution-system/thunks/record-run-output-refused.js";
|
|
7
|
+
import {
|
|
8
|
+
reattachHeadlessAgentJson
|
|
9
|
+
} from "../redux/execution-system/thunks/reattach-headless-agent-json.js";
|
|
7
10
|
import {
|
|
8
11
|
runHeadlessAgentJson
|
|
9
12
|
} from "../redux/execution-system/thunks/run-headless-agent-json.js";
|
|
@@ -31,33 +34,29 @@ function useHeadlessAgentJson() {
|
|
|
31
34
|
return ids.length > 0 ? ids[ids.length - 1] : null;
|
|
32
35
|
});
|
|
33
36
|
useRetainRequestForViewer(activeRequestId, "useHeadlessAgentJson");
|
|
34
|
-
async function
|
|
37
|
+
async function settle(result, coerce) {
|
|
38
|
+
if (!result.success) {
|
|
39
|
+
throw new HeadlessAgentRunError(result.error ?? "The agent run failed", {
|
|
40
|
+
...result.errorDetail ? { detail: result.errorDetail } : {},
|
|
41
|
+
...result.requestId ? { requestId: result.requestId } : {},
|
|
42
|
+
...result.conversationId ? { conversationId: result.conversationId } : {}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (!coerce) return result.data;
|
|
46
|
+
try {
|
|
47
|
+
return coerce(result.data, result);
|
|
48
|
+
} catch (refused) {
|
|
49
|
+
const why = refused instanceof Error ? refused.message : String(refused);
|
|
50
|
+
const rowId = result.requestId ? runRowIdOf(store.getState(), result.requestId) : null;
|
|
51
|
+
void recordRunOutputRefused(rowId, why, result.requestId);
|
|
52
|
+
throw refused;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function guarded(work) {
|
|
35
56
|
setIsRunning(true);
|
|
36
57
|
setError(null);
|
|
37
|
-
setConversationId(null);
|
|
38
58
|
try {
|
|
39
|
-
|
|
40
|
-
...opts,
|
|
41
|
-
onConversationCreated: (cid) => {
|
|
42
|
-
setConversationId(cid);
|
|
43
|
-
opts.onConversationCreated?.(cid);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
if (!result.success) {
|
|
47
|
-
throw new HeadlessAgentRunError(result.error ?? "The agent run failed", {
|
|
48
|
-
...result.errorDetail ? { detail: result.errorDetail } : {},
|
|
49
|
-
...result.requestId ? { requestId: result.requestId } : {},
|
|
50
|
-
...result.conversationId ? { conversationId: result.conversationId } : {}
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
if (!opts.coerce) return result.data;
|
|
54
|
-
try {
|
|
55
|
-
return opts.coerce(result.data, result);
|
|
56
|
-
} catch (refused) {
|
|
57
|
-
const why = refused instanceof Error ? refused.message : String(refused);
|
|
58
|
-
if (result.requestId) void recordRunOutputRefused(result.requestId, why);
|
|
59
|
-
throw refused;
|
|
60
|
-
}
|
|
59
|
+
return await work();
|
|
61
60
|
} catch (e) {
|
|
62
61
|
const message = e instanceof Error ? e.message : "Failed to run the agent";
|
|
63
62
|
setError(message);
|
|
@@ -66,11 +65,34 @@ function useHeadlessAgentJson() {
|
|
|
66
65
|
setIsRunning(false);
|
|
67
66
|
}
|
|
68
67
|
}
|
|
68
|
+
async function run(opts) {
|
|
69
|
+
setConversationId(null);
|
|
70
|
+
return guarded(async () => {
|
|
71
|
+
const result = await runHeadlessAgentJson(dispatch, store.getState, {
|
|
72
|
+
...opts,
|
|
73
|
+
onConversationCreated: (cid) => {
|
|
74
|
+
setConversationId(cid);
|
|
75
|
+
opts.onConversationCreated?.(cid);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return settle(result, opts.coerce);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function reattach(existingConversationId, opts) {
|
|
82
|
+
setConversationId(existingConversationId);
|
|
83
|
+
return guarded(async () => {
|
|
84
|
+
const result = await reattachHeadlessAgentJson(dispatch, store.getState, {
|
|
85
|
+
...opts,
|
|
86
|
+
conversationId: existingConversationId
|
|
87
|
+
});
|
|
88
|
+
return settle(result, opts.coerce);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
69
91
|
const reset = useCallback(() => {
|
|
70
92
|
setError(null);
|
|
71
93
|
setConversationId(null);
|
|
72
94
|
}, []);
|
|
73
|
-
return { run, isRunning, error, conversationId, activeRequestId, reset };
|
|
95
|
+
return { run, reattach, isRunning, error, conversationId, activeRequestId, reset };
|
|
74
96
|
}
|
|
75
97
|
export {
|
|
76
98
|
HeadlessAgentRunError,
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { type HeadlessAgentJsonRunOptions } from "./useHeadlessAgentJson.js";
|
|
1
|
+
import { type HeadlessAgentJsonReattachOptions, type HeadlessAgentJsonRunOptions } from "./useHeadlessAgentJson.js";
|
|
2
2
|
export type LiveAgentRunOptions<T> = Omit<HeadlessAgentJsonRunOptions<T>, "displayMode" | "keepInstance">;
|
|
3
|
+
export type LiveAgentReattachOptions<T> = Omit<HeadlessAgentJsonReattachOptions<T>, "keepInstance">;
|
|
3
4
|
export interface UseLiveAgentRun {
|
|
5
|
+
/**
|
|
6
|
+
* Pick an EXISTING run back up (a reload, a paused tab) — its finished
|
|
7
|
+
* result, or follow it to the end. Never starts a run; the live handle
|
|
8
|
+
* (`conversationId`) is set before anything is read.
|
|
9
|
+
*/
|
|
10
|
+
reattach: <T = unknown>(conversationId: string, opts: LiveAgentReattachOptions<T>) => Promise<T>;
|
|
4
11
|
/** Run the agent; the live handle (`conversationId`) is set before the stream. */
|
|
5
12
|
run: <T = unknown>(opts: LiveAgentRunOptions<T>) => Promise<T>;
|
|
6
13
|
isRunning: boolean;
|
|
@@ -38,6 +38,20 @@ function useLiveAgentRun() {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
async function reattach(existingConversationId, opts) {
|
|
42
|
+
if (ownedConversationRef.current !== existingConversationId) releaseOwned();
|
|
43
|
+
setDismissed(false);
|
|
44
|
+
dismissedRef.current = false;
|
|
45
|
+
ownedConversationRef.current = existingConversationId;
|
|
46
|
+
try {
|
|
47
|
+
return await headless.reattach(existingConversationId, {
|
|
48
|
+
...opts,
|
|
49
|
+
keepInstance: true
|
|
50
|
+
});
|
|
51
|
+
} finally {
|
|
52
|
+
if (dismissedRef.current) releaseOwned();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
41
55
|
const dismiss = () => {
|
|
42
56
|
setDismissed(true);
|
|
43
57
|
dismissedRef.current = true;
|
|
@@ -58,6 +72,7 @@ function useLiveAgentRun() {
|
|
|
58
72
|
);
|
|
59
73
|
return {
|
|
60
74
|
run,
|
|
75
|
+
reattach,
|
|
61
76
|
dismiss,
|
|
62
77
|
isRunning: headless.isRunning,
|
|
63
78
|
error: headless.error,
|
|
@@ -105,6 +105,76 @@ export declare const selectOwnVariableValues: (conversationId: string) => ((stat
|
|
|
105
105
|
memoize: typeof import("reselect").weakMapMemoize;
|
|
106
106
|
argsMemoize: typeof import("reselect").weakMapMemoize;
|
|
107
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* The variables a run USED: the values its first turn was sent with — frozen at
|
|
110
|
+
* send, host-wired names included — so a later edit of the variable field never
|
|
111
|
+
* rewrites what the run was given. Before any send it is what the next send would
|
|
112
|
+
* carry (the request merge, defaults included). Read by the agent builder's Side
|
|
113
|
+
* Chat helper to judge a test run.
|
|
114
|
+
*/
|
|
115
|
+
export declare const selectVariablesTheRunUsed: (conversationId: string) => ((state: {
|
|
116
|
+
voiceAgent: import("../../../../voice-agent/types.js").VoiceAgentState;
|
|
117
|
+
agentDefinition: import("../../../types/agent-definition.types.js").AgentDefinitionSliceState;
|
|
118
|
+
conversationList: import("../../conversation-list/conversation-list.types.js").ConversationListState;
|
|
119
|
+
conversationHistory: import("../../conversation-history/types.js").ConversationHistoryState;
|
|
120
|
+
agentShortcut: import("../../agent-shortcuts/types.js").AgentShortcutSliceState;
|
|
121
|
+
agentShortcutCategory: import("../../agent-shortcut-categories/types.js").AgentShortcutCategorySliceState;
|
|
122
|
+
surfaceUserState: import("../../../../surfaces/redux/userStateSlice.js").SurfaceUserStateSlice;
|
|
123
|
+
conversations: import("../conversations/conversations.slice.js").ConversationsState;
|
|
124
|
+
chatIncognito: import("../../chat/chat-incognito.slice.js").ChatIncognitoState;
|
|
125
|
+
chatRoute: import("../../chat/chat-route.slice.js").ChatRouteState;
|
|
126
|
+
instanceModelOverrides: import("../instance-model-overrides/instance-model-overrides.slice.js").InstanceModelOverridesState;
|
|
127
|
+
instanceInputCapabilities: import("../instance-input-capabilities/instance-input-capabilities.slice.js").InstanceInputCapabilitiesState;
|
|
128
|
+
instanceVariableValues: import("./instance-variable-values.slice.js").InstanceVariableValuesState;
|
|
129
|
+
instanceResources: import("../instance-resources/instance-resources.slice.js").InstanceResourcesState;
|
|
130
|
+
instanceContext: import("../instance-context/instance-context.slice.js").InstanceContextState;
|
|
131
|
+
instanceWorkingDocument: import("../instance-working-document/instance-working-document.slice.js").InstanceWorkingDocumentSliceState;
|
|
132
|
+
instanceUserInput: import("../instance-user-input/instance-user-input.slice.js").InstanceUserInputSliceState;
|
|
133
|
+
instanceClientTools: import("../instance-client-tools/instance-client-tools.slice.js").InstanceClientToolsState;
|
|
134
|
+
pendingAsks: import("../../../ui-first-tools/redux/pending-asks.slice.js").PendingAsksState;
|
|
135
|
+
conversationInbox: import("../inbox/inbox.slice.js").ConversationInboxState;
|
|
136
|
+
agentLists: import("../../../ui-first-tools/redux/agent-lists.slice.js").AgentListsState;
|
|
137
|
+
instanceUIState: import("../instance-ui-state/instance-ui-state.slice.js").InstanceUIStateSlice;
|
|
138
|
+
activeTools: import("../active-tools/active-tools.slice.js").ActiveToolsState;
|
|
139
|
+
activeRequests: import("../active-requests/active-requests.slice.js").ActiveRequestsState;
|
|
140
|
+
runSets: import("../run-sets/run-sets.slice.js").RunSetsState;
|
|
141
|
+
messages: import("../messages/messages.slice.js").MessagesState;
|
|
142
|
+
observability: import("../observability/observability.slice.js").ObservabilityState;
|
|
143
|
+
contextState: import("../context-state/context-state.slice.js").ContextStateSliceState;
|
|
144
|
+
observationalMemory: import("../observational-memory/observational-memory.slice.js").ObservationalMemoryState;
|
|
145
|
+
cacheBypass: import("../message-crud/cache-bypass.slice.js").CacheBypassState;
|
|
146
|
+
conversationFocus: import("../conversation-focus/conversation-focus.slice.js").ConversationFocusState;
|
|
147
|
+
surfaces: import("../../surfaces/surfaces.slice.js").SurfacesState;
|
|
148
|
+
surfacesCatalog: import("../../../../surfaces/redux/surfacesCatalogSlice.js").SurfacesCatalogSliceState;
|
|
149
|
+
agentSurfaceBindings: import("../../../../surfaces/redux/agentSurfaceBindingsSlice.js").AgentSurfaceBindingsSliceState;
|
|
150
|
+
surfaceConfig: import("../../../../surfaces/redux/surfaceConfigSlice.js").SurfaceConfigSliceState;
|
|
151
|
+
agentAssistantMarkdownDraft: import("../../agent-assistant-markdown-draft.slice.js").AgentAssistantMarkdownDraftState;
|
|
152
|
+
mcp: import("../../mcp/mcp.slice.js").McpSliceState;
|
|
153
|
+
proposedDirectives: import("../../proposed-directives/proposedDirectivesSlice.js").ProposedDirectivesState;
|
|
154
|
+
conversationAttachments: import("../../../connectors/attachments.slice.js").AttachmentsSliceState;
|
|
155
|
+
chatHost: import("../../../../store/chat-host.slice.js").ChatHostState;
|
|
156
|
+
hostConversations: import("../host-conversations/host-conversations.slice.js").HostConversationsState;
|
|
157
|
+
} & import("../../../../store/root-state.js").ChatHostReadSlices) => Record<string, unknown>) & {
|
|
158
|
+
clearCache: () => void;
|
|
159
|
+
resultsCount: () => number;
|
|
160
|
+
resetResultsCount: () => void;
|
|
161
|
+
} & {
|
|
162
|
+
resultFunc: (resultFuncArgs_0: import("./instance-variable-values.slice.js").InstanceVariableValuesEntry) => Record<string, unknown>;
|
|
163
|
+
memoizedResultFunc: ((resultFuncArgs_0: import("./instance-variable-values.slice.js").InstanceVariableValuesEntry) => Record<string, unknown>) & {
|
|
164
|
+
clearCache: () => void;
|
|
165
|
+
resultsCount: () => number;
|
|
166
|
+
resetResultsCount: () => void;
|
|
167
|
+
};
|
|
168
|
+
lastResult: () => Record<string, unknown>;
|
|
169
|
+
dependencies: [(state: ChatRootState) => import("./instance-variable-values.slice.js").InstanceVariableValuesEntry];
|
|
170
|
+
recomputations: () => number;
|
|
171
|
+
resetRecomputations: () => void;
|
|
172
|
+
dependencyRecomputations: () => number;
|
|
173
|
+
resetDependencyRecomputations: () => void;
|
|
174
|
+
} & {
|
|
175
|
+
memoize: typeof import("reselect").weakMapMemoize;
|
|
176
|
+
argsMemoize: typeof import("reselect").weakMapMemoize;
|
|
177
|
+
};
|
|
108
178
|
/** Immutable first-turn values, excluding names the host supplied. */
|
|
109
179
|
export declare const selectOwnSubmittedFirstTurnValues: (conversationId: string) => ((state: {
|
|
110
180
|
voiceAgent: import("../../../../voice-agent/types.js").VoiceAgentState;
|
|
@@ -25,6 +25,19 @@ const selectOwnVariableValues = (conversationId) => createSelector(
|
|
|
25
25
|
return Object.keys(own).length === 0 ? EMPTY_RECORD : own;
|
|
26
26
|
}
|
|
27
27
|
);
|
|
28
|
+
const selectVariablesTheRunUsed = (conversationId) => createSelector(
|
|
29
|
+
(state) => state.instanceVariableValues.byConversationId[conversationId],
|
|
30
|
+
(entry) => {
|
|
31
|
+
if (!entry) return EMPTY_RECORD;
|
|
32
|
+
if (entry.submittedFirstTurnValues) return entry.submittedFirstTurnValues;
|
|
33
|
+
const merged = resolveVariablesForRequest({
|
|
34
|
+
definitions: entry.definitions,
|
|
35
|
+
userValues: entry.userValues,
|
|
36
|
+
scopeValues: entry.scopeValues
|
|
37
|
+
});
|
|
38
|
+
return Object.keys(merged).length > 0 ? merged : EMPTY_RECORD;
|
|
39
|
+
}
|
|
40
|
+
);
|
|
28
41
|
const selectOwnSubmittedFirstTurnValues = (conversationId) => createSelector(
|
|
29
42
|
(state) => state.instanceVariableValues.byConversationId[conversationId],
|
|
30
43
|
(entry) => {
|
|
@@ -134,5 +147,6 @@ export {
|
|
|
134
147
|
selectScopeVariableValues,
|
|
135
148
|
selectUserVariableValues,
|
|
136
149
|
selectVariableProvenance,
|
|
137
|
-
selectVariablesForRequest
|
|
150
|
+
selectVariablesForRequest,
|
|
151
|
+
selectVariablesTheRunUsed
|
|
138
152
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reattachHeadlessAgentJson — the sibling of `runHeadlessAgentJson` for a run
|
|
3
|
+
* that is ALREADY running or finished: a reload or a paused tab must never
|
|
4
|
+
* start a second paid run, it picks the first one back up.
|
|
5
|
+
*
|
|
6
|
+
* It owns no run path of its own. Every step is an existing primitive:
|
|
7
|
+
*
|
|
8
|
+
* 1. `loadConversation` — the saved conversation (a finished run's
|
|
9
|
+
* last message is read from it)
|
|
10
|
+
* 2. `surfaceColdPendingCalls` — a delegated tool prompt nobody answered
|
|
11
|
+
* 3. `reconnectServerOperation` — `/runtime/operations/by-link/conversation`:
|
|
12
|
+
* if the server is still working, rejoin
|
|
13
|
+
* and follow it to terminal through the
|
|
14
|
+
* canonical stream pipeline (so the same
|
|
15
|
+
* LiveRunWindow bound to this conversation
|
|
16
|
+
* streams it), then reload the saved turn
|
|
17
|
+
* 4. `adoptHeadlessAgentJson` — THE result-resolution rule, over the
|
|
18
|
+
* request that is now in Redux
|
|
19
|
+
*
|
|
20
|
+
* A stream this very page still owns (`hasAbortController`) is never re-read:
|
|
21
|
+
* a reload would clobber it, so steps 1–3 are skipped and the live request is
|
|
22
|
+
* simply waited on. Never throws — resolves a structured result.
|
|
23
|
+
*/
|
|
24
|
+
import type { ChatDispatch, ChatRootState } from "../../../../store/root-state.js";
|
|
25
|
+
import { type AdoptedAgentJsonOptions, type HeadlessAgentJsonResult } from "./run-headless-agent-json.js";
|
|
26
|
+
export interface ReattachHeadlessAgentJsonOptions extends Omit<AdoptedAgentJsonOptions, "requestId" | "conversationId"> {
|
|
27
|
+
conversationId: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function reattachHeadlessAgentJson(dispatch: ChatDispatch, getState: () => ChatRootState, opts: ReattachHeadlessAgentJsonOptions): Promise<HeadlessAgentJsonResult>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { extractErrorMessage } from "@ai-matrx/data/net";
|
|
2
|
+
import { extractFirstJson } from "@ai-matrx/content-ir/json-extract";
|
|
3
|
+
import { captureError } from "../../../../host/diagnostics.js";
|
|
4
|
+
import { reconnectServerOperation } from "../../../runtime-reconnect/reconnect-server-operation.thunk.js";
|
|
5
|
+
import { selectRequestStatus } from "../active-requests/active-requests.selectors.js";
|
|
6
|
+
import { selectLatestAnswerText as selectCommittedAnswerText } from "../messages/messages.selectors.js";
|
|
7
|
+
import { selectLatestRequestId } from "../selectors/aggregate.selectors.js";
|
|
8
|
+
import { hasAbortController } from "./abort-registry.js";
|
|
9
|
+
import { loadConversation } from "./load-conversation.thunk.js";
|
|
10
|
+
import { surfaceColdPendingCalls } from "./surface-cold-pending-calls.thunk.js";
|
|
11
|
+
import {
|
|
12
|
+
adoptHeadlessAgentJson
|
|
13
|
+
} from "./run-headless-agent-json.js";
|
|
14
|
+
const REATTACH_SETTLE_MS = 300;
|
|
15
|
+
async function reattachHeadlessAgentJson(dispatch, getState, opts) {
|
|
16
|
+
const { conversationId } = opts;
|
|
17
|
+
const failure = (error, errorDetail) => ({
|
|
18
|
+
success: false,
|
|
19
|
+
data: null,
|
|
20
|
+
fullResponse: selectCommittedAnswerText(conversationId)(getState()),
|
|
21
|
+
error,
|
|
22
|
+
errorDetail,
|
|
23
|
+
conversationId
|
|
24
|
+
});
|
|
25
|
+
if (!hasAbortController(conversationId)) {
|
|
26
|
+
try {
|
|
27
|
+
await dispatch(loadConversation({ conversationId })).unwrap();
|
|
28
|
+
} catch (err) {
|
|
29
|
+
const detail = extractErrorMessage(err);
|
|
30
|
+
captureError({
|
|
31
|
+
source: "agent-json-result",
|
|
32
|
+
message: `Reattach could not read conversation ${conversationId}: ${detail}`,
|
|
33
|
+
conversationId,
|
|
34
|
+
raw: { surfaceKey: opts.surfaceKey, detail }
|
|
35
|
+
});
|
|
36
|
+
return failure("That run could not be opened again.", detail);
|
|
37
|
+
}
|
|
38
|
+
void dispatch(surfaceColdPendingCalls(conversationId));
|
|
39
|
+
try {
|
|
40
|
+
await dispatch(
|
|
41
|
+
reconnectServerOperation({ conversationId, source: "cold-load" })
|
|
42
|
+
).unwrap();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const detail = extractErrorMessage(err);
|
|
45
|
+
captureError({
|
|
46
|
+
source: "agent-json-result",
|
|
47
|
+
message: `Reattach could not follow conversation ${conversationId}: ${detail}`,
|
|
48
|
+
conversationId,
|
|
49
|
+
raw: { surfaceKey: opts.surfaceKey, detail }
|
|
50
|
+
});
|
|
51
|
+
return failure("That run could not be followed to its end.", detail);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const requestId = selectLatestRequestId(conversationId)(getState());
|
|
55
|
+
const status = requestId ? selectRequestStatus(requestId)(getState()) : void 0;
|
|
56
|
+
const fullResponse = selectCommittedAnswerText(conversationId)(getState());
|
|
57
|
+
const finished = status === void 0 || status === "complete";
|
|
58
|
+
if (fullResponse.trim() && finished && !hasAbortController(conversationId)) {
|
|
59
|
+
const expect = opts.expect ?? "json";
|
|
60
|
+
const data = expect === "text" ? fullResponse.trim() : extractFirstJson(fullResponse, { allowFuzzy: true })?.value ?? null;
|
|
61
|
+
if (data != null) {
|
|
62
|
+
const result = {
|
|
63
|
+
success: true,
|
|
64
|
+
data,
|
|
65
|
+
fullResponse,
|
|
66
|
+
conversationId,
|
|
67
|
+
...requestId ? { requestId } : {}
|
|
68
|
+
};
|
|
69
|
+
await opts.onResult?.(result);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (requestId) {
|
|
74
|
+
return adoptHeadlessAgentJson(dispatch, getState, {
|
|
75
|
+
...opts,
|
|
76
|
+
requestId,
|
|
77
|
+
settleMs: opts.settleMs ?? REATTACH_SETTLE_MS,
|
|
78
|
+
// The window keeps showing the finished run; the hook owns teardown.
|
|
79
|
+
keepInstance: opts.keepInstance ?? true
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return failure(
|
|
83
|
+
"That run has no saved result to open.",
|
|
84
|
+
"reattach: the conversation holds no request and no readable answer"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
reattachHeadlessAgentJson
|
|
89
|
+
};
|
|
@@ -10,5 +10,14 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Never silent: a write that does not land is a `console.error` naming the run.
|
|
12
12
|
*/
|
|
13
|
+
import type { ChatRootState } from "../../../../store/root-state.js";
|
|
13
14
|
export declare const OUTPUT_REFUSED_METADATA_KEY = "output_refused";
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The run's own `chat.user_request` row id, as the server announced it (`record_reserved` for the
|
|
17
|
+
* `user_request` table, or a message reservation's `parent_refs.user_request_id`). The client's
|
|
18
|
+
* `requestId` (`req_<uuid>`) is a Redux key the database has never seen — writing against it matched
|
|
19
|
+
* no row, so every refusal before 0.7.12 left the run `completed` (Make's describe box, 2026-10-09).
|
|
20
|
+
* Null when the stream announced no row.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runRowIdOf(state: ChatRootState, requestId: string): string | null;
|
|
23
|
+
export declare function recordRunOutputRefused(runRowId: string | null, reason: string, requestId?: string): Promise<boolean>;
|
|
@@ -2,17 +2,34 @@ import { mergeJsonColumn } from "@ai-matrx/data/db";
|
|
|
2
2
|
import { supabase } from "../../../../host/db.js";
|
|
3
3
|
import { hasBrowserSession } from "../../../../host/identity.js";
|
|
4
4
|
const OUTPUT_REFUSED_METADATA_KEY = "output_refused";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const ROW_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
+
function runRowIdOf(state, requestId) {
|
|
7
|
+
const reservations = state.activeRequests.byRequestId[requestId]?.reservations;
|
|
8
|
+
if (!reservations) return null;
|
|
9
|
+
const all = Object.values(reservations);
|
|
10
|
+
const own = all.find((r) => r.table === "user_request" && ROW_ID.test(r.recordId));
|
|
11
|
+
if (own) return own.recordId;
|
|
12
|
+
const parent = all.map((r) => r.parentRefs?.user_request_id).find((id) => typeof id === "string" && ROW_ID.test(id));
|
|
13
|
+
return parent ?? null;
|
|
14
|
+
}
|
|
15
|
+
async function recordRunOutputRefused(runRowId, reason, requestId) {
|
|
16
|
+
if (!runRowId || !ROW_ID.test(runRowId)) {
|
|
17
|
+
console.error(
|
|
18
|
+
`[run-output-refused] Run ${requestId ?? "?"} announced no chat.user_request row id (got ${runRowId ?? "none"}); the refusal cannot be recorded and the run still reads as completed. Reason: ${reason}`
|
|
19
|
+
);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const requestRow = runRowId;
|
|
23
|
+
if (!await hasBrowserSession()) return false;
|
|
7
24
|
const result = await mergeJsonColumn({
|
|
8
|
-
fetchCurrent: () => supabase.schema("chat").from("user_request").select("id, version, metadata").eq("id",
|
|
25
|
+
fetchCurrent: () => supabase.schema("chat").from("user_request").select("id, version, metadata").eq("id", requestRow).maybeSingle(),
|
|
9
26
|
readColumn: (row) => row.metadata,
|
|
10
27
|
merge: (current) => ({ ...current, [OUTPUT_REFUSED_METADATA_KEY]: { reason, at: (/* @__PURE__ */ new Date()).toISOString() } }),
|
|
11
|
-
applyUpdate: ({ value, expectedVersion, nextVersion }) => supabase.schema("chat").from("user_request").update({ metadata: value, version: nextVersion, status: "failed", error: reason.slice(0, 500) }).eq("id",
|
|
28
|
+
applyUpdate: ({ value, expectedVersion, nextVersion }) => supabase.schema("chat").from("user_request").update({ metadata: value, version: nextVersion, status: "failed", error: reason.slice(0, 500) }).eq("id", requestRow).eq("version", expectedVersion).select("id, version, metadata").maybeSingle()
|
|
12
29
|
});
|
|
13
30
|
if (result.status !== "saved") {
|
|
14
31
|
console.error(
|
|
15
|
-
`[run-output-refused] Could not record the refusal on run ${
|
|
32
|
+
`[run-output-refused] Could not record the refusal on run ${requestRow}: ` + (result.status === "error" ? String(result.error) : `write ${result.status}`) + `. The run still reads as completed. Reason: ${reason}`
|
|
16
33
|
);
|
|
17
34
|
return false;
|
|
18
35
|
}
|
|
@@ -20,5 +37,6 @@ async function recordRunOutputRefused(requestId, reason) {
|
|
|
20
37
|
}
|
|
21
38
|
export {
|
|
22
39
|
OUTPUT_REFUSED_METADATA_KEY,
|
|
23
|
-
recordRunOutputRefused
|
|
40
|
+
recordRunOutputRefused,
|
|
41
|
+
runRowIdOf
|
|
24
42
|
};
|
|
@@ -83,6 +83,7 @@ export interface MarkdownStreamProps {
|
|
|
83
83
|
*/
|
|
84
84
|
imagePolicy?: "self" | "other" | "ai" | "inherit";
|
|
85
85
|
}
|
|
86
|
-
declare function
|
|
86
|
+
export declare function preloadMarkdownStream(): Promise<void>;
|
|
87
|
+
declare function MarkdownStream(props: MarkdownStreamProps): import("react").FunctionComponentElement<import("react").SuspenseProps> | import("react").ReactElement<MarkdownStreamProps, string | import("react").JSXElementConstructor<any>> | null;
|
|
87
88
|
export { MarkdownStream };
|
|
88
89
|
export default MarkdownStream;
|
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { createElement, lazy, Suspense,
|
|
2
|
+
import { createElement, lazy, Suspense, useSyncExternalStore } from "react";
|
|
3
3
|
const MarkdownStreamImpl = lazy(() => import("./MarkdownStreamImpl.js"));
|
|
4
|
+
let loadedImpl = null;
|
|
5
|
+
let loading = null;
|
|
6
|
+
const readyListeners = /* @__PURE__ */ new Set();
|
|
7
|
+
function preloadMarkdownStream() {
|
|
8
|
+
if (typeof window === "undefined") return Promise.resolve();
|
|
9
|
+
loading ??= import("./MarkdownStreamImpl.js").then((m) => {
|
|
10
|
+
loadedImpl = m.default;
|
|
11
|
+
for (const notify of readyListeners) notify();
|
|
12
|
+
});
|
|
13
|
+
return loading;
|
|
14
|
+
}
|
|
15
|
+
const subscribeReady = (notify) => {
|
|
16
|
+
readyListeners.add(notify);
|
|
17
|
+
return () => readyListeners.delete(notify);
|
|
18
|
+
};
|
|
19
|
+
const clientReady = () => true;
|
|
20
|
+
const serverNotReady = () => false;
|
|
4
21
|
function MarkdownStream(props) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
if (
|
|
22
|
+
const client = useSyncExternalStore(subscribeReady, clientReady, serverNotReady);
|
|
23
|
+
if (!client) return null;
|
|
24
|
+
if (loadedImpl) return createElement(loadedImpl, props);
|
|
25
|
+
void preloadMarkdownStream();
|
|
8
26
|
return createElement(Suspense, { fallback: null }, createElement(MarkdownStreamImpl, props));
|
|
9
27
|
}
|
|
10
28
|
var MarkdownStream_default = MarkdownStream;
|
|
11
29
|
export {
|
|
12
30
|
MarkdownStream,
|
|
13
|
-
MarkdownStream_default as default
|
|
31
|
+
MarkdownStream_default as default,
|
|
32
|
+
preloadMarkdownStream
|
|
14
33
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/chat",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.13",
|
|
4
4
|
"description": "THE one AI Matrx person-to-agent chat system: the full chat (running agents, conversations, tool calls, streaming, context, surfaces, voice) as explicit per-module subpaths @ai-matrx/chat/<domain>/<module> (no wildcards: scripts/public-modules.json) over a host contract (/host, /host/react) whose only required value is a db. One chat, one renderer: the old kernel entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) are retired.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -4686,20 +4686,20 @@
|
|
|
4686
4686
|
"@ai-matrx/browser-audio": "latest",
|
|
4687
4687
|
"@ai-matrx/canvas": "latest",
|
|
4688
4688
|
"@ai-matrx/content-ir": "latest",
|
|
4689
|
-
"@ai-matrx/agents": "latest",
|
|
4690
4689
|
"@ai-matrx/content-ir-react": "latest",
|
|
4691
|
-
"@ai-matrx/
|
|
4690
|
+
"@ai-matrx/agents": "latest",
|
|
4692
4691
|
"@ai-matrx/design-system": "latest",
|
|
4692
|
+
"@ai-matrx/diff": "latest",
|
|
4693
4693
|
"@ai-matrx/icons": "latest",
|
|
4694
4694
|
"@ai-matrx/kit": "latest",
|
|
4695
4695
|
"@ai-matrx/media": "latest",
|
|
4696
|
-
"@ai-matrx/print": "latest",
|
|
4697
4696
|
"@ai-matrx/realtime": "latest",
|
|
4698
4697
|
"@ai-matrx/records": "latest",
|
|
4699
|
-
"@ai-matrx/
|
|
4698
|
+
"@ai-matrx/data": "latest",
|
|
4700
4699
|
"@ai-matrx/rich-content": "latest",
|
|
4700
|
+
"@ai-matrx/print": "latest",
|
|
4701
4701
|
"@ai-matrx/rich-editor": "latest",
|
|
4702
|
-
"@ai-matrx/
|
|
4702
|
+
"@ai-matrx/records-ui": "latest"
|
|
4703
4703
|
},
|
|
4704
4704
|
"devDependencies": {
|
|
4705
4705
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
@@ -4818,5 +4818,5 @@
|
|
|
4818
4818
|
"check:side-effects": "node ../scripts/check-side-effects-listed.mjs . && node ../scripts/check-side-effects-listed.mjs --self-test",
|
|
4819
4819
|
"check:public-surface": "node ./scripts/sync-public-surface.mjs --check && node ./scripts/sync-public-surface.mjs --self-test"
|
|
4820
4820
|
},
|
|
4821
|
-
"gitHead": "
|
|
4821
|
+
"gitHead": "b49e03c743ac750bc1615fd9635bdc5a4796a241"
|
|
4822
4822
|
}
|