@ai-matrx/chat 0.7.6 → 0.7.8
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 +29 -0
- package/dist/agents/api/fetch-pending-calls.js +4 -3
- package/dist/agents/api/submit-tool-results.js +3 -6
- package/dist/agents/components/chat/begin-fresh-chat.js +2 -2
- package/dist/agents/components/inputs/smart-input/InputActionButtons.d.ts +2 -2
- package/dist/agents/components/inputs/smart-input/InputActionButtons.js +3 -1
- package/dist/agents/components/inputs/smart-input/SmartAgentInputStacked.js +8 -3
- package/dist/agents/components/inputs/smart-input/composer/ComposerSplash.js +2 -3
- package/dist/agents/components/inputs/smart-input/composer/useComposerMode.js +4 -2
- package/dist/agents/components/live-run/LiveRunDisplay.d.ts +7 -1
- package/dist/agents/components/live-run/LiveRunDisplay.js +3 -1
- package/dist/agents/components/messages-display/assistant/AssistantMessageFooter.js +4 -2
- package/dist/agents/components/shared/transcript-layout.d.ts +9 -8
- package/dist/agents/components/shared/transcript-layout.js +2 -2
- package/dist/agents/redux/agent-definition/agent-cache-bust-request.js +7 -7
- package/dist/agents/redux/execution-system/context-rules/receipt-check.js +2 -2
- package/dist/agents/redux/execution-system/inbox/inbox.thunks.d.ts +2 -2
- package/dist/agents/redux/execution-system/inbox/inbox.thunks.js +7 -15
- package/dist/agents/redux/execution-system/observational-memory/fetch-memory-cost.thunk.d.ts +1 -1
- package/dist/agents/redux/execution-system/observational-memory/fetch-memory-cost.thunk.js +7 -4
- package/dist/agents/redux/execution-system/selectors/aggregate.selectors.d.ts +1 -114
- package/dist/agents/redux/execution-system/selectors/aggregate.selectors.js +28 -43
- package/dist/agents/redux/execution-system/thunks/call-conversation-api.d.ts +44 -0
- package/dist/agents/redux/execution-system/thunks/call-conversation-api.js +78 -0
- package/dist/agents/redux/execution-system/thunks/continue-agent-json.d.ts +41 -0
- package/dist/agents/redux/execution-system/thunks/continue-agent-json.js +66 -0
- package/dist/agents/redux/execution-system/thunks/execute-instance.thunk.d.ts +20 -0
- package/dist/agents/redux/execution-system/thunks/execute-instance.thunk.js +9 -2
- package/dist/agents/redux/execution-system/thunks/launch-agent-execution.thunk.js +4 -6
- package/dist/agents/redux/execution-system/thunks/notify-and-play-dispatcher.d.ts +1 -5
- package/dist/agents/redux/execution-system/thunks/notify-and-play-dispatcher.js +2 -1
- package/dist/agents/redux/execution-system/thunks/resolve-base-url.js +7 -17
- package/dist/agents/redux/execution-system/thunks/settle-after-stop.thunk.js +2 -2
- package/dist/agents/redux/execution-system/thunks/smart-execute.thunk.js +3 -3
- package/dist/agents/redux/execution-system/utils/continuation-body.d.ts +8 -2
- package/dist/agents/redux/execution-system/utils/continuation-body.js +3 -3
- package/dist/agents/redux/execution-system/utils/required-organization.d.ts +18 -0
- package/dist/agents/redux/execution-system/utils/required-organization.js +14 -10
- package/dist/agents/services/mcp-connections.service.js +5 -8
- package/dist/agents/ui-first-tools/dispatcher/dispatch-ui-first-tool.thunk.js +2 -1
- package/dist/host/defaults/files-host.js +11 -1
- package/dist/host/defaults/server-api.d.ts +1 -15
- package/dist/host/defaults/server-api.js +0 -21
- package/dist/host/defaults/server.d.ts +2 -1
- package/dist/host/defaults/server.js +8 -6
- package/dist/host/request-headers.d.ts +26 -0
- package/dist/host/request-headers.js +17 -0
- package/dist/host/server/call-api.d.ts +0 -13
- package/dist/host/server/call-api.js +0 -2
- package/dist/host/server/matrx-transport.d.ts +0 -1
- package/dist/host/server/matrx-transport.js +0 -2
- package/dist/store/root-state.d.ts +4 -21
- package/dist/surfaces/hooks/useSurfaceConfig.js +2 -1
- package/dist/surfaces/manifests/agent-builder.manifest.d.ts +8 -0
- package/dist/surfaces/manifests/agent-builder.manifest.js +62 -2
- package/dist/surfaces/runtime/surface-feedback.js +2 -2
- package/dist/ui/mandates-storage/mandateStorage.d.ts +36 -0
- package/dist/ui/mandates-storage/shortcutStorage.d.ts +9 -0
- package/dist/ui/markdown-stream/EnhancedChatMarkdown.js +22 -0
- package/dist/ui/markdown-stream/MarkdownStreamImpl.js +3 -3
- package/dist/ui/markdown-stream/StreamTextFallback.d.ts +6 -0
- package/dist/ui/markdown-stream/StreamTextFallback.js +23 -0
- package/dist/ui/markdown-stream/merge-thinking-across-hidden.d.ts +19 -0
- package/dist/ui/markdown-stream/merge-thinking-across-hidden.js +21 -0
- package/package.json +16 -11
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
selectShowAutoClearToggle
|
|
12
12
|
} from "../instance-ui-state/instance-ui-state.selectors.js";
|
|
13
13
|
import { assembleRequest } from "../thunks/execute-instance.thunk.js";
|
|
14
|
+
import { selectAppContext } from "../../../../context/sources/scopes.js";
|
|
14
15
|
import {
|
|
15
16
|
contextItemBindingOf,
|
|
16
17
|
isCustomDataBinding
|
|
@@ -170,49 +171,33 @@ const selectIsInstanceReady = (conversationId) => {
|
|
|
170
171
|
return selector;
|
|
171
172
|
};
|
|
172
173
|
function makeSelectAssembledRequest(conversationId) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
instanceResources,
|
|
201
|
-
instanceVariableValues,
|
|
202
|
-
instanceModelOverrides,
|
|
203
|
-
instanceContext,
|
|
204
|
-
instanceClientTools,
|
|
205
|
-
appContext,
|
|
206
|
-
chatHost,
|
|
207
|
-
agentDefinition,
|
|
208
|
-
surfaceUserState,
|
|
209
|
-
messages,
|
|
210
|
-
userAuth,
|
|
211
|
-
userProfile
|
|
212
|
-
};
|
|
213
|
-
return assembleRequest(state, conversationId);
|
|
214
|
-
}
|
|
215
|
-
);
|
|
174
|
+
const inputs = [
|
|
175
|
+
(s) => s.conversations,
|
|
176
|
+
(s) => s.instanceUIState,
|
|
177
|
+
(s) => s.instanceUserInput,
|
|
178
|
+
(s) => s.instanceResources,
|
|
179
|
+
(s) => s.instanceVariableValues,
|
|
180
|
+
(s) => s.instanceModelOverrides,
|
|
181
|
+
(s) => s.instanceContext,
|
|
182
|
+
(s) => s.instanceClientTools,
|
|
183
|
+
(s) => selectAppContext(s),
|
|
184
|
+
(s) => s.chatHost,
|
|
185
|
+
// The context door (buildRequestContext) reads these too: the agent's
|
|
186
|
+
// policies + kill switch, the person's saved context rules, whether this
|
|
187
|
+
// is the first turn.
|
|
188
|
+
(s) => s.agentDefinition,
|
|
189
|
+
(s) => s.surfaceUserState,
|
|
190
|
+
(s) => s.messages
|
|
191
|
+
];
|
|
192
|
+
let lastInputs = null;
|
|
193
|
+
let lastResult = null;
|
|
194
|
+
return (state) => {
|
|
195
|
+
const current = inputs.map((read) => read(state));
|
|
196
|
+
if (lastInputs && current.every((value, i) => value === lastInputs[i])) return lastResult;
|
|
197
|
+
lastInputs = current;
|
|
198
|
+
lastResult = assembleRequest(state, conversationId);
|
|
199
|
+
return lastResult;
|
|
200
|
+
};
|
|
216
201
|
}
|
|
217
202
|
const selectAssembledRequest = makeSelectAssembledRequest;
|
|
218
203
|
const selectInstanceSummary = (conversationId) => createSelector(
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CONVERSATION DOOR for every JSON request that names a conversation (Audit B family 8).
|
|
3
|
+
*
|
|
4
|
+
* A turn resolves where it goes and what it carries in `resolveBackendForConversation`: the
|
|
5
|
+
* conversation's channel (global / sandbox override / local engine / EC2-dedicated, under the
|
|
6
|
+
* confidential-routing rule), THE ONE header builder and THE ORG RULE. Until 2026-10-08 the calls
|
|
7
|
+
* that ride beside a turn on the same conversation — the inbox, tool results, pending calls,
|
|
8
|
+
* memory cost, cancel — went through the host's general `callApi` instead: another base URL (no
|
|
9
|
+
* sandbox / local override), another organization precedence (the tab's selection, not the
|
|
10
|
+
* conversation's), and in one host no session at all (the inbox sent without auth). This door
|
|
11
|
+
* makes them the turn's twin: same resolver, so the same server, credential and organization.
|
|
12
|
+
*
|
|
13
|
+
* Result shape and error classification are the shared pipeline's (`executeMatrxCall`,
|
|
14
|
+
* `normalizeMatrxError` in `@ai-matrx/agents/matrx`) — the same `{ data } | { error }` the call
|
|
15
|
+
* sites already read from `callApi`. Guard: `__tests__/conversation-calls-name-the-turns-org.test.ts`.
|
|
16
|
+
*/
|
|
17
|
+
import { type MatrxCancelResponse, type MatrxCallResult, type MatrxQueryParams } from "@ai-matrx/agents/matrx";
|
|
18
|
+
import type { ChatRootState } from "../../../../store/root-state.js";
|
|
19
|
+
export type ConversationCallMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
20
|
+
export interface ConversationCallConfig {
|
|
21
|
+
/** Path template; `{conversation_id}` is filled from the door's conversation. */
|
|
22
|
+
path: string;
|
|
23
|
+
method: ConversationCallMethod;
|
|
24
|
+
/** Other path parameters (never `conversation_id`). */
|
|
25
|
+
pathParams?: Record<string, string>;
|
|
26
|
+
queryParams?: MatrxQueryParams;
|
|
27
|
+
body?: unknown;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
/** HTTP statuses this call site handles as expected outcomes (not logged as failures). */
|
|
30
|
+
expectedErrorStatuses?: readonly number[];
|
|
31
|
+
connectTimeoutMs?: number;
|
|
32
|
+
totalTimeoutMs?: number | null;
|
|
33
|
+
}
|
|
34
|
+
type AnyDispatch = (action: never) => unknown;
|
|
35
|
+
/** Send one JSON request on `conversationId`'s own channel, credential and organization. */
|
|
36
|
+
export declare function callConversationApi<T = unknown>(conversationId: string, config: ConversationCallConfig): (dispatch: AnyDispatch, getState: () => ChatRootState) => Promise<MatrxCallResult<T>>;
|
|
37
|
+
/**
|
|
38
|
+
* Stop (or interrupt) one server run of `conversationId` on the conversation's own channel,
|
|
39
|
+
* credential and organization — never the tab's. `seenSeq` is the last wire `stream_seq` the page
|
|
40
|
+
* APPLIED before Stop; the server cuts the stopped answer there, so the saved text never outgrows
|
|
41
|
+
* the screen.
|
|
42
|
+
*/
|
|
43
|
+
export declare function cancelConversationRun(conversationId: string, requestId: string, mode?: "cancel" | "interrupt", seenSeq?: number): (dispatch: AnyDispatch, getState: () => ChatRootState) => Promise<MatrxCallResult<MatrxCancelResponse>>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cancelAgentRun,
|
|
3
|
+
buildMatrxRequestBody,
|
|
4
|
+
buildMatrxRequestUrl,
|
|
5
|
+
executeMatrxCall,
|
|
6
|
+
normalizeMatrxError,
|
|
7
|
+
resolveEndpointPath
|
|
8
|
+
} from "@ai-matrx/agents/matrx";
|
|
9
|
+
import { selectEndpointOverrideConfig } from "../../../../host/server/api-config.js";
|
|
10
|
+
import { conversationRequestOrganizationId } from "../utils/required-organization.js";
|
|
11
|
+
import { resolveBackendForConversation } from "./resolve-base-url.js";
|
|
12
|
+
import { createMatrxTransportForConversation } from "./matrx-transport-for-conversation.js";
|
|
13
|
+
function callConversationApi(conversationId, config) {
|
|
14
|
+
return async (_dispatch, getState) => {
|
|
15
|
+
try {
|
|
16
|
+
const state = getState();
|
|
17
|
+
const backend = resolveBackendForConversation(state, conversationId);
|
|
18
|
+
if (!backend) {
|
|
19
|
+
return {
|
|
20
|
+
error: {
|
|
21
|
+
type: "network_error",
|
|
22
|
+
message: "No AI Matrx server is configured for this conversation. The request was not sent."
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const organizationId = conversationRequestOrganizationId(state, conversationId);
|
|
27
|
+
const path = resolveEndpointPath(config.path, selectEndpointOverrideConfig(state));
|
|
28
|
+
const result = await executeMatrxCall({
|
|
29
|
+
url: buildMatrxRequestUrl(
|
|
30
|
+
backend.baseUrl,
|
|
31
|
+
path,
|
|
32
|
+
{ ...config.pathParams ?? {}, conversation_id: conversationId },
|
|
33
|
+
config.queryParams
|
|
34
|
+
),
|
|
35
|
+
method: config.method,
|
|
36
|
+
headers: { ...backend.headers, Accept: "application/json" },
|
|
37
|
+
body: buildMatrxRequestBody(
|
|
38
|
+
config.body,
|
|
39
|
+
organizationId ? { organization_id: organizationId } : {}
|
|
40
|
+
),
|
|
41
|
+
...config.signal ? { signal: config.signal } : {},
|
|
42
|
+
...config.connectTimeoutMs !== void 0 ? { connectTimeoutMs: config.connectTimeoutMs } : {},
|
|
43
|
+
...config.totalTimeoutMs !== void 0 ? { totalTimeoutMs: config.totalTimeoutMs } : {}
|
|
44
|
+
});
|
|
45
|
+
const status = result.error?.status;
|
|
46
|
+
if (result.error && !(status !== void 0 && config.expectedErrorStatuses?.includes(status))) {
|
|
47
|
+
console.warn(`[conversation-call] ${config.method} ${config.path} failed`, {
|
|
48
|
+
conversationId,
|
|
49
|
+
channel: backend.channel,
|
|
50
|
+
error: result.error
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return { error: normalizeMatrxError(error) };
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function cancelConversationRun(conversationId, requestId, mode = "cancel", seenSeq) {
|
|
60
|
+
return async (_dispatch, getState) => {
|
|
61
|
+
try {
|
|
62
|
+
const base = createMatrxTransportForConversation(getState, conversationId, {
|
|
63
|
+
source: "matrxTransport:cancel"
|
|
64
|
+
});
|
|
65
|
+
const transport = seenSeq === void 0 ? base : {
|
|
66
|
+
fetch: (path, init) => base.fetch(`${path}${path.includes("?") ? "&" : "?"}seen_seq=${seenSeq}`, init)
|
|
67
|
+
};
|
|
68
|
+
const data = await cancelAgentRun(transport, requestId, { mode });
|
|
69
|
+
return { data, requestId: data.request_id };
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return { error: normalizeMatrxError(error) };
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
callConversationApi,
|
|
77
|
+
cancelConversationRun
|
|
78
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* continueAgentJson — the NEXT turn of a conversation whose first turn ran headlessly
|
|
3
|
+
* (`runHeadlessAgentJson` with `keepInstance`), waited on exactly like the first.
|
|
4
|
+
*
|
|
5
|
+
* ONE CONVERSATION, MANY ROUNDS. A surface that builds something with an agent and then keeps
|
|
6
|
+
* talking about it (the Applet builder: build → automatic fix round → "Change it" → the person's
|
|
7
|
+
* replies) continues THAT conversation instead of starting a new one per round, so the person can
|
|
8
|
+
* read and answer it in one place and the agent sees every earlier round.
|
|
9
|
+
*
|
|
10
|
+
* A turn is either the person's words (`userInput` — only what a human typed, THE USER-INPUT LAW)
|
|
11
|
+
* or a turn the host writes (`hostTurn` — an activation line + which feature wrote it; never quoted
|
|
12
|
+
* as the person). Structured content for the turn rides `context` (named entries on the instance —
|
|
13
|
+
* a `null` value removes the key), never the message.
|
|
14
|
+
*
|
|
15
|
+
* It reuses THE machinery: `executeInstance` sends the continuation (`POST /ai/conversations/{id}`),
|
|
16
|
+
* `adoptHeadlessAgentJson` waits on it (polling, settle window, one result rule, the persistence
|
|
17
|
+
* seam). A conversation not in this tab yet (a refresh) is read first through `loadConversation`.
|
|
18
|
+
* Never throws: resolves a structured result, exactly like `runHeadlessAgentJson`.
|
|
19
|
+
*/
|
|
20
|
+
import type { ChatDispatch, ChatRootState } from "../../../../store/root-state.js";
|
|
21
|
+
import type { RequestInitiation } from "../../../types/instance.types.js";
|
|
22
|
+
import { type HostTurnInput } from "./execute-instance.thunk.js";
|
|
23
|
+
import { type AdoptedAgentJsonOptions, type HeadlessAgentJsonResult } from "./run-headless-agent-json.js";
|
|
24
|
+
export interface ContinueAgentJsonOptions extends Pick<AdoptedAgentJsonOptions, "surfaceKey" | "timeoutMs" | "pollIntervalMs" | "settleMs" | "expect" | "onResult" | "signal" | "failureMessages"> {
|
|
25
|
+
conversationId: string;
|
|
26
|
+
/** What the person typed for this turn. Exclusive with `hostTurn`. */
|
|
27
|
+
userInput?: string;
|
|
28
|
+
/** A turn the host writes (the person said nothing). Exclusive with `userInput`. */
|
|
29
|
+
hostTurn?: HostTurnInput;
|
|
30
|
+
/** Per-turn named context: each key is written on the instance; `null` removes it. */
|
|
31
|
+
context?: Record<string, unknown>;
|
|
32
|
+
/** "user" when the person pressed send; "auto" for a host-started round. Default by turn kind. */
|
|
33
|
+
initiation?: RequestInitiation;
|
|
34
|
+
/** Fires the instant the turn's request exists (a live view binds to it). */
|
|
35
|
+
onRequestId?: (requestId: string) => void;
|
|
36
|
+
/** Keep the instance after the turn (the live view owns teardown). Default true. */
|
|
37
|
+
keepInstance?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** Which of the two turn kinds this is — exactly one, or a sentence why not. */
|
|
40
|
+
export declare function continuationTurnProblem(opts: Pick<ContinueAgentJsonOptions, "userInput" | "hostTurn">): string | null;
|
|
41
|
+
export declare function continueAgentJson(dispatch: ChatDispatch, getState: () => ChatRootState, opts: ContinueAgentJsonOptions): Promise<HeadlessAgentJsonResult>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { removeContextEntry, setContextEntries } from "../instance-context/instance-context.slice.js";
|
|
2
|
+
import { setUserInputText } from "../instance-user-input/instance-user-input.slice.js";
|
|
3
|
+
import { executeInstance } from "./execute-instance.thunk.js";
|
|
4
|
+
import { loadConversation } from "./load-conversation.thunk.js";
|
|
5
|
+
import {
|
|
6
|
+
adoptHeadlessAgentJson
|
|
7
|
+
} from "./run-headless-agent-json.js";
|
|
8
|
+
function continuationTurnProblem(opts) {
|
|
9
|
+
const typed = typeof opts.userInput === "string" && opts.userInput.trim().length > 0;
|
|
10
|
+
if (typed && opts.hostTurn) return "A turn is the person's words or a host turn, never both.";
|
|
11
|
+
if (!typed && !opts.hostTurn) return "Nothing to send: the turn has neither the person's words nor a host turn.";
|
|
12
|
+
if (opts.hostTurn && (!opts.hostTurn.text.trim() || !opts.hostTurn.reason.trim())) return "A host turn needs its text and its reason.";
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
async function continueAgentJson(dispatch, getState, opts) {
|
|
16
|
+
const { conversationId } = opts;
|
|
17
|
+
const problem = continuationTurnProblem(opts);
|
|
18
|
+
if (problem) return { success: false, data: null, fullResponse: "", error: problem, conversationId };
|
|
19
|
+
try {
|
|
20
|
+
if (!getState().conversations.byConversationId[conversationId]) {
|
|
21
|
+
await dispatch(loadConversation({ conversationId })).unwrap();
|
|
22
|
+
}
|
|
23
|
+
const context = Object.entries(opts.context ?? {});
|
|
24
|
+
const writes = context.filter(([, value]) => value !== null && value !== void 0);
|
|
25
|
+
for (const [key] of context.filter(([, value]) => value === null)) dispatch(removeContextEntry({ conversationId, key }));
|
|
26
|
+
if (writes.length) dispatch(setContextEntries({ conversationId, entries: writes.map(([key, value]) => ({ key, value })) }));
|
|
27
|
+
if (opts.userInput) dispatch(setUserInputText({ conversationId, text: opts.userInput }));
|
|
28
|
+
let requestId = null;
|
|
29
|
+
const sent = await dispatch(
|
|
30
|
+
executeInstance({
|
|
31
|
+
conversationId,
|
|
32
|
+
initiation: opts.initiation ?? (opts.hostTurn ? "auto" : "user"),
|
|
33
|
+
...opts.hostTurn ? { hostTurn: opts.hostTurn } : {},
|
|
34
|
+
onRequestId: (id) => {
|
|
35
|
+
requestId = id;
|
|
36
|
+
opts.onRequestId?.(id);
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
if (executeInstance.rejected.match(sent)) {
|
|
41
|
+
const reason = typeof sent.payload === "string" ? sent.payload : sent.error.message ?? "The turn could not be sent.";
|
|
42
|
+
return { success: false, data: null, fullResponse: "", error: reason, conversationId, ...requestId ? { requestId } : {} };
|
|
43
|
+
}
|
|
44
|
+
return await adoptHeadlessAgentJson(dispatch, getState, {
|
|
45
|
+
requestId: sent.payload.requestId,
|
|
46
|
+
conversationId,
|
|
47
|
+
surfaceKey: opts.surfaceKey,
|
|
48
|
+
keepInstance: opts.keepInstance ?? true,
|
|
49
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
|
|
50
|
+
...opts.pollIntervalMs !== void 0 ? { pollIntervalMs: opts.pollIntervalMs } : {},
|
|
51
|
+
...opts.settleMs !== void 0 ? { settleMs: opts.settleMs } : {},
|
|
52
|
+
...opts.expect ? { expect: opts.expect } : {},
|
|
53
|
+
...opts.onResult ? { onResult: opts.onResult } : {},
|
|
54
|
+
...opts.signal ? { signal: opts.signal } : {},
|
|
55
|
+
...opts.failureMessages ? { failureMessages: opts.failureMessages } : {}
|
|
56
|
+
});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
59
|
+
console.error("[continueAgentJson] the turn failed before it ran", { conversationId, err });
|
|
60
|
+
return { success: false, data: null, fullResponse: "", error: message, conversationId };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
continuationTurnProblem,
|
|
65
|
+
continueAgentJson
|
|
66
|
+
};
|
|
@@ -36,6 +36,8 @@ export declare function isEmptyUserInput(userInput: unknown): boolean;
|
|
|
36
36
|
*/
|
|
37
37
|
export declare function refusesEmptyTurn(args: {
|
|
38
38
|
retry: boolean;
|
|
39
|
+
/** A host-written turn is a message even with no `user_input`. */
|
|
40
|
+
hostTurn?: boolean;
|
|
39
41
|
hasPriorTurns: boolean;
|
|
40
42
|
cacheOnly: boolean;
|
|
41
43
|
isEphemeral: boolean;
|
|
@@ -123,6 +125,24 @@ interface ExecuteInstanceArgs {
|
|
|
123
125
|
* composer: the person may already be typing the next message in it.
|
|
124
126
|
*/
|
|
125
127
|
submission?: FrozenSubmission;
|
|
128
|
+
/**
|
|
129
|
+
* A turn the HOST writes on a persisted conversation — never something the
|
|
130
|
+
* person typed (THE USER-INPUT LAW). Sent as `host_turn` instead of
|
|
131
|
+
* `user_input`: a short activation line the model reads, plus which host
|
|
132
|
+
* feature wrote it; the turn's content rides `context`. Only a continuation
|
|
133
|
+
* (the conversation already has turns). The Applet builder's automatic fix
|
|
134
|
+
* round is one (lane F6b).
|
|
135
|
+
*/
|
|
136
|
+
hostTurn?: HostTurnInput;
|
|
137
|
+
}
|
|
138
|
+
/** `ConversationContinueRequest.host_turn` — what a host-written turn carries. */
|
|
139
|
+
export interface HostTurnInput {
|
|
140
|
+
/** The activation line the model reads. */
|
|
141
|
+
text: string;
|
|
142
|
+
/** Which host feature wrote it (≤80 chars). */
|
|
143
|
+
reason: string;
|
|
144
|
+
/** Extra stamps for the stored message. */
|
|
145
|
+
metadata?: Record<string, unknown>;
|
|
126
146
|
}
|
|
127
147
|
interface ExecuteInstanceResult {
|
|
128
148
|
requestId: string;
|
|
@@ -155,7 +155,7 @@ function isEmptyUserInput(userInput) {
|
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
157
|
function refusesEmptyTurn(args) {
|
|
158
|
-
if (args.retry || !args.hasPriorTurns) return false;
|
|
158
|
+
if (args.retry || args.hostTurn || !args.hasPriorTurns) return false;
|
|
159
159
|
if (!shouldContinuePersistedConversation(
|
|
160
160
|
args.cacheOnly,
|
|
161
161
|
args.hasPriorTurns,
|
|
@@ -254,7 +254,8 @@ const executeInstance = createAsyncThunk(
|
|
|
254
254
|
initiation,
|
|
255
255
|
onRequestId,
|
|
256
256
|
surfaceRefreshed = false,
|
|
257
|
-
submission
|
|
257
|
+
submission,
|
|
258
|
+
hostTurn
|
|
258
259
|
}, { getState, dispatch, rejectWithValue: reject }) => {
|
|
259
260
|
const requestId = generateRequestId();
|
|
260
261
|
const rejectWithValue = (value, originalErrorName) => reject(
|
|
@@ -340,6 +341,7 @@ const executeInstance = createAsyncThunk(
|
|
|
340
341
|
}
|
|
341
342
|
if (refusesEmptyTurn({
|
|
342
343
|
retry,
|
|
344
|
+
hostTurn: Boolean(hostTurn),
|
|
343
345
|
hasPriorTurns: selectMessageCount(conversationId)(state) > 0,
|
|
344
346
|
cacheOnly: instance.cacheOnly,
|
|
345
347
|
isEphemeral: instance.isEphemeral === true,
|
|
@@ -535,11 +537,16 @@ const executeInstance = createAsyncThunk(
|
|
|
535
537
|
).replace("{conversation_id}", encodeURIComponent(conversationId));
|
|
536
538
|
url = `${baseUrl}${convPath}`;
|
|
537
539
|
routedPayload = buildContinuationBody(payload, {
|
|
540
|
+
hostTurn,
|
|
538
541
|
retry: Boolean(retry),
|
|
539
542
|
debug: Boolean(debug),
|
|
540
543
|
cacheBypass: pendingBypass
|
|
541
544
|
});
|
|
542
545
|
} else {
|
|
546
|
+
if (hostTurn) {
|
|
547
|
+
console.error(`[execute-instance] a host turn ("${hostTurn.reason}") was sent on conversation "${conversationId}", which has no persisted turns \u2014 nothing was sent.`);
|
|
548
|
+
return rejectWithValue("A host turn continues a conversation; this one has no turns yet");
|
|
549
|
+
}
|
|
543
550
|
const start = resolveStartPath({
|
|
544
551
|
agentId: instance.agentId,
|
|
545
552
|
pinnedVersionId: instance.initialAgentVersionId ?? null,
|
|
@@ -26,10 +26,7 @@ import { alwaysOnSurfaceKeys } from "../../../../surfaces/utils/always-on-contex
|
|
|
26
26
|
import {
|
|
27
27
|
fetchAgentRunTier
|
|
28
28
|
} from "../../agent-definition/thunks.js";
|
|
29
|
-
import {
|
|
30
|
-
isSignedOutVisitor,
|
|
31
|
-
NotAuthenticatedError
|
|
32
|
-
} from "../../../../host/identity.js";
|
|
29
|
+
import { isSignedOutVisitor, NotAuthenticatedError, selectAccessToken } from "../../../../host/identity.js";
|
|
33
30
|
import { applyLaunchModelOverrides } from "../instance-model-overrides/launch-model-overrides.js";
|
|
34
31
|
import { selectAgentRunTier, selectAgentById } from "../../agent-definition/selectors.js";
|
|
35
32
|
import { getShortcutRecordFromState } from "../../agent-shortcuts/selectors.js";
|
|
@@ -84,8 +81,9 @@ import {
|
|
|
84
81
|
resolvePerLaunchMappings
|
|
85
82
|
} from "./surface-scope-mapping.js";
|
|
86
83
|
import { ensureAgentCatalog } from "../../../identity/agent-identity.js";
|
|
84
|
+
import { selectOrganizationId } from "../../../../host/org.js";
|
|
87
85
|
function hasSignedInSession(state) {
|
|
88
|
-
return Boolean(state
|
|
86
|
+
return Boolean(selectAccessToken(state));
|
|
89
87
|
}
|
|
90
88
|
const INTERACTIVE_MODES = /* @__PURE__ */ new Set([
|
|
91
89
|
"modal-full",
|
|
@@ -144,7 +142,7 @@ const launchAgentExecution = createAsyncThunk("instances/launch", async (options
|
|
|
144
142
|
);
|
|
145
143
|
}
|
|
146
144
|
if (!agentId) {
|
|
147
|
-
const selectedOrganizationId = adminLaneOrganizationId() ?? getState()
|
|
145
|
+
const selectedOrganizationId = adminLaneOrganizationId() ?? selectOrganizationId(getState());
|
|
148
146
|
const questionOrganizationId = organizationId ?? selectedOrganizationId ?? await (await import("../../../../host/org.js")).ensureOrgId(null);
|
|
149
147
|
const displayKey = mandateKey;
|
|
150
148
|
const resolved = await resolveMandate(displayKey, { organizationId: questionOrganizationId });
|
|
@@ -34,11 +34,7 @@ export interface NotifyAndPlayEntry<S> {
|
|
|
34
34
|
run: (args: never, ctx: NotifyAndPlayContext<S>) => Promise<unknown>;
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
export declare function createNotifyAndPlayDispatcher<S
|
|
38
|
-
userAuth?: {
|
|
39
|
-
id?: string | null;
|
|
40
|
-
} | null;
|
|
41
|
-
}>(options: {
|
|
37
|
+
export declare function createNotifyAndPlayDispatcher<S>(options: {
|
|
42
38
|
/** Redux action type prefix, e.g. "scribeTools/dispatch". */
|
|
43
39
|
typePrefix: string;
|
|
44
40
|
/** Human label used in the unknown-tool message, e.g. "scribe". */
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { createAsyncThunk } from "@reduxjs/toolkit";
|
|
2
2
|
import { extractErrorMessage } from "@ai-matrx/data/net";
|
|
3
3
|
import { postDelegatedToolOutcome } from "./post-delegated-tool-outcome.js";
|
|
4
|
+
import { selectUserId } from "../../../../host/identity.js";
|
|
4
5
|
function createNotifyAndPlayDispatcher(options) {
|
|
5
6
|
const { typePrefix, family, isToolName, getEntry } = options;
|
|
6
7
|
return createAsyncThunk(
|
|
7
8
|
typePrefix,
|
|
8
9
|
async ({ conversationId, requestId, callId, toolName, args }, { dispatch, getState }) => {
|
|
9
|
-
const userId = getState()
|
|
10
|
+
const userId = selectUserId(getState());
|
|
10
11
|
const base = { conversationId, requestId, callId, toolName };
|
|
11
12
|
const fail = (errorType, message, durationMs) => postDelegatedToolOutcome(dispatch, {
|
|
12
13
|
...base,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { chatRequestHeaders } from "../../../../host/request-headers.js";
|
|
2
|
+
import { conversationRequestOrganizationId } from "../utils/required-organization.js";
|
|
3
3
|
import {
|
|
4
4
|
selectResolvedBaseUrl,
|
|
5
5
|
selectActiveServer
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
getCachedLocalEngine,
|
|
15
15
|
supportsLocalAgentExecution
|
|
16
16
|
} from "../../../../host/server/local-engine.js";
|
|
17
|
-
import { selectOrganizationId } from "../../../../host/org.js";
|
|
18
17
|
import { getChatHost, isChatHostConfigured } from "../../../../host/configure.js";
|
|
19
18
|
import {
|
|
20
19
|
ConfidentialRoutingRefusedError,
|
|
@@ -86,21 +85,12 @@ function resolveBackendUnchecked(state, conversationId) {
|
|
|
86
85
|
const entry = state.instanceUIState?.byConversationId?.[conversationId] ?? null;
|
|
87
86
|
const overrideUrl = entry?.serverOverrideUrl ?? null;
|
|
88
87
|
const headers = {
|
|
89
|
-
"Content-Type": "application/json"
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
...chatRequestHeaders(
|
|
90
|
+
{ accessToken: selectAccessToken(state), fingerprintId: selectFingerprintId(state) },
|
|
91
|
+
conversationRequestOrganizationId(state, conversationId)
|
|
92
|
+
)
|
|
90
93
|
};
|
|
91
|
-
const accessToken = selectAccessToken(state);
|
|
92
|
-
const fingerprintId = selectFingerprintId(state);
|
|
93
|
-
if (accessToken) {
|
|
94
|
-
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
95
|
-
} else if (fingerprintId) {
|
|
96
|
-
headers["X-Fingerprint-ID"] = fingerprintId;
|
|
97
|
-
}
|
|
98
|
-
const conversationOrganizationId = state.conversations?.byConversationId?.[conversationId]?.organizationId ?? null;
|
|
99
|
-
const organizationId = adminSeatReplyOrganizationId(state, conversationId) ?? conversationOrganizationId ?? adminLaneOrganizationId() ?? selectOrganizationId(state);
|
|
100
|
-
if (organizationId) {
|
|
101
|
-
headers["X-Organization-Id"] = organizationId;
|
|
102
|
-
Object.assign(headers, adminLaneHeadersFor(organizationId));
|
|
103
|
-
}
|
|
104
94
|
if (overrideUrl) {
|
|
105
95
|
const ref = resolveAgentSandboxRef(state, conversationId);
|
|
106
96
|
if (ref && ref.kind !== "local-pc") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createAsyncThunk } from "@reduxjs/toolkit";
|
|
2
|
-
import {
|
|
2
|
+
import { cancelConversationRun } from "./call-conversation-api.js";
|
|
3
3
|
import { fetchOperationsByLink } from "../../../runtime-reconnect/api.js";
|
|
4
4
|
import { resolveBackendForConversation } from "./resolve-base-url.js";
|
|
5
5
|
import { loadConversation } from "./load-conversation.thunk.js";
|
|
@@ -82,7 +82,7 @@ const settleAfterStop = createAsyncThunk(
|
|
|
82
82
|
}
|
|
83
83
|
if (op && !op.is_terminal && !cancelSentFor && op.request_id) {
|
|
84
84
|
cancelSentFor = op.request_id;
|
|
85
|
-
const result = await dispatch(
|
|
85
|
+
const result = await dispatch(cancelConversationRun(conversationId, op.request_id));
|
|
86
86
|
if (result.error) {
|
|
87
87
|
console.warn("[settle-after-stop] late server cancel failed", {
|
|
88
88
|
conversationId,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
selectInFlightRequestIds,
|
|
38
38
|
settleAfterStop
|
|
39
39
|
} from "./settle-after-stop.thunk.js";
|
|
40
|
-
import {
|
|
40
|
+
import { cancelConversationRun } from "./call-conversation-api.js";
|
|
41
41
|
import { toast } from "../../../../host/notify.js";
|
|
42
42
|
import { selectHostConversation } from "../host-conversations/host-conversations.selectors.js";
|
|
43
43
|
import { sendToHost, stopHostTurn } from "../host-conversations/host-conversation.thunks.js";
|
|
@@ -508,7 +508,7 @@ const cancelExecution = createAsyncThunk(
|
|
|
508
508
|
serverRequestId
|
|
509
509
|
);
|
|
510
510
|
void dispatch(
|
|
511
|
-
|
|
511
|
+
cancelConversationRun(conversationId, serverRequestId, "cancel", seenSeq)
|
|
512
512
|
).then((result) => {
|
|
513
513
|
if (result.error) {
|
|
514
514
|
console.warn(
|
|
@@ -564,7 +564,7 @@ const interruptAndSend = createAsyncThunk(
|
|
|
564
564
|
serverRequestId
|
|
565
565
|
);
|
|
566
566
|
void dispatch(
|
|
567
|
-
|
|
567
|
+
cancelConversationRun(conversationId, serverRequestId, "interrupt", seenSeq)
|
|
568
568
|
).then(
|
|
569
569
|
(result) => {
|
|
570
570
|
if (result.error) {
|
|
@@ -56,11 +56,17 @@ export declare const CONTINUATION_FIELD_ROUTING: {
|
|
|
56
56
|
readonly max_iterations: "not assembled by this thunk";
|
|
57
57
|
readonly max_retries_per_iteration: "not assembled by this thunk";
|
|
58
58
|
readonly responder_agent_id: "coding-mirror replies only; not assembled by this thunk";
|
|
59
|
-
readonly host_turn: "
|
|
59
|
+
readonly host_turn: "set by the builder from the hostTurn option (never copied from the payload)";
|
|
60
60
|
};
|
|
61
61
|
export interface ContinuationBodyOptions {
|
|
62
62
|
retry: boolean;
|
|
63
|
+
/** A host-written turn: sent as `host_turn`, and `user_input` is omitted (the person said nothing). */
|
|
64
|
+
hostTurn?: {
|
|
65
|
+
text: string;
|
|
66
|
+
reason: string;
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
};
|
|
63
69
|
debug: boolean;
|
|
64
70
|
cacheBypass: unknown;
|
|
65
71
|
}
|
|
66
|
-
export declare function buildContinuationBody(payload: AssembledAgentStartRequest, { retry, debug, cacheBypass }: ContinuationBodyOptions): Record<string, unknown>;
|
|
72
|
+
export declare function buildContinuationBody(payload: AssembledAgentStartRequest, { retry, debug, cacheBypass, hostTurn }: ContinuationBodyOptions): Record<string, unknown>;
|
|
@@ -50,12 +50,12 @@ const CONTINUATION_FIELD_ROUTING = {
|
|
|
50
50
|
max_iterations: "not assembled by this thunk",
|
|
51
51
|
max_retries_per_iteration: "not assembled by this thunk",
|
|
52
52
|
responder_agent_id: "coding-mirror replies only; not assembled by this thunk",
|
|
53
|
-
host_turn: "
|
|
53
|
+
host_turn: "set by the builder from the hostTurn option (never copied from the payload)"
|
|
54
54
|
};
|
|
55
55
|
const EMPTY_IS_MEANINGFUL = /* @__PURE__ */ new Set(["tools", "output_kinds"]);
|
|
56
56
|
const FALSE_IS_MEANINGFUL = /* @__PURE__ */ new Set(["memory", "tools_replace", "context_withheld"]);
|
|
57
|
-
function buildContinuationBody(payload, { retry, debug, cacheBypass }) {
|
|
58
|
-
const body = retry ? { retry: true } : { user_input: payload.user_input };
|
|
57
|
+
function buildContinuationBody(payload, { retry, debug, cacheBypass, hostTurn }) {
|
|
58
|
+
const body = retry ? { retry: true } : hostTurn ? { host_turn: hostTurn } : { user_input: payload.user_input };
|
|
59
59
|
body.stream = true;
|
|
60
60
|
const source = payload;
|
|
61
61
|
for (const [key, route] of Object.entries(CONTINUATION_FIELD_ROUTING)) {
|
|
@@ -80,4 +80,22 @@ export declare function launchAsksForOrganizationBeforeOpen(launch: {
|
|
|
80
80
|
opensWindow: boolean;
|
|
81
81
|
}): boolean;
|
|
82
82
|
export declare function ensureExecutionOrganization(state: ChatRootState, conversationId: string): Promise<string | null>;
|
|
83
|
+
/**
|
|
84
|
+
* THE ORG RULE (Audit B family 26): which organization a request on this conversation names.
|
|
85
|
+
* ONE function, read by the turn (`requireExecutionOrganizationId`) and by the conversation door
|
|
86
|
+
* every conversation-bound call rides (`resolveBackendForConversation`), so a turn and an inbox /
|
|
87
|
+
* tool-result / pending-calls / memory-cost / cancel call on the same conversation can never name
|
|
88
|
+
* different organizations. Guard: `__tests__/one-org-rule-per-conversation.test.ts`.
|
|
89
|
+
*
|
|
90
|
+
* - Not yet persisted (`cacheOnly !== false`, or not in the store): the organization a launcher
|
|
91
|
+
* declared, then the admin seat's platform tenant (only in a host with a database), then the
|
|
92
|
+
* app-selected organization.
|
|
93
|
+
* - Persisted: the row owns its organization forever. The one exception is a platform admin
|
|
94
|
+
* replying from the admin seat to a conversation they reach only as an admin: the request names
|
|
95
|
+
* the platform tenant (the admin lane admits nothing else); the row's own organization is
|
|
96
|
+
* restored server-side and never changes.
|
|
97
|
+
*
|
|
98
|
+
* Returns null when nothing names one (the caller decides whether that refuses).
|
|
99
|
+
*/
|
|
100
|
+
export declare function conversationRequestOrganizationId(state: ChatRootState, conversationId: string): string | null;
|
|
83
101
|
export declare function requireExecutionOrganizationId(state: ChatRootState, conversationId: string): string;
|