@datalayer/agent-runtimes 1.3.11 โ 1.3.14
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/README.md +1 -2
- package/lib/AgentNode.js +3 -3
- package/lib/api/runtimes/healthz.js +4 -2
- package/lib/api/spacer/healthz.js +4 -2
- package/lib/chat/base/ChatBase.js +39 -25
- package/lib/chat/base/modelChoice.d.ts +62 -0
- package/lib/chat/base/modelChoice.js +56 -0
- package/lib/chat/messages/ChatMessageList.js +13 -5
- package/lib/chat/messages/FormSubmissionMessage.d.ts +17 -0
- package/lib/chat/messages/FormSubmissionMessage.js +49 -0
- package/lib/chat/messages/formSubmission.d.ts +41 -0
- package/lib/chat/messages/formSubmission.js +90 -0
- package/lib/chat/prompt/menus/ModelSelector.js +11 -4
- package/lib/chat/styles/streamdownStyles.js +40 -0
- package/lib/client/mixins/RuntimesMixin.js +1 -1
- package/lib/client/mixins/SpacerMixin.js +3 -3
- package/lib/components/a2ui/A2uiMarkdownProvider.d.ts +11 -0
- package/lib/components/a2ui/A2uiMarkdownProvider.js +13 -0
- package/lib/components/a2ui/index.d.ts +1 -0
- package/lib/components/a2ui/index.js +1 -0
- package/lib/examples/A2UiAgentExample.js +36 -223
- package/lib/examples/A2UiJupyterOutputExample.js +11 -1
- package/lib/examples/A2UiViewerExample.d.ts +9 -2
- package/lib/examples/A2UiViewerExample.js +33 -3
- package/lib/examples/main.js +37 -35
- package/lib/examples/utils/a2uiMarkdownProvider.d.ts +1 -7
- package/lib/examples/utils/a2uiMarkdownProvider.js +6 -8
- package/lib/loop/core/index.d.ts +33 -2
- package/lib/loop/core/index.js +29 -2
- package/lib/loop/embed/LoopEmbed.js +1 -0
- package/lib/loop/index.d.ts +1 -1
- package/lib/loop/index.js +4 -1
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +22 -0
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +64 -0
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.d.ts +38 -0
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +64 -0
- package/lib/loop/plugins/a2ui-surface/index.d.ts +34 -0
- package/lib/loop/plugins/a2ui-surface/index.js +91 -0
- package/lib/loop/plugins/a2ui-surface/toolResult.d.ts +52 -0
- package/lib/loop/plugins/a2ui-surface/toolResult.js +106 -0
- package/lib/loop/plugins/chat/ChatView.js +81 -10
- package/lib/loop/plugins/chat/index.d.ts +14 -0
- package/lib/loop/plugins/chat/index.js +1 -0
- package/lib/loop/plugins/document/index.js +4 -0
- package/lib/loop/plugins/notebook/index.js +4 -0
- package/lib/loop/plugins/shell/useEditorPreviews.js +7 -0
- package/lib/loop/presets.d.ts +5 -0
- package/lib/loop/presets.js +7 -1
- package/lib/models/environments.generated.d.ts +7 -1
- package/lib/models/environments.generated.js +1 -1
- package/lib/specs/agents/agents.d.ts +1 -0
- package/lib/specs/agents/agents.js +112 -0
- package/lib/specs/models.js +3 -3
- package/lib/tools/adapters/copilotkit/CopilotKitToolAdapter.js +1 -1
- package/package.json +12 -6
- package/patches/@primer+react+37.31.0.patch +390 -0
- package/patches/eslint-plugin-react+7.37.5.patch +65 -0
- package/scripts/apply-patches.sh +15 -1
- package/scripts/codegen/compose.py +4 -4
- package/scripts/codegen/generate_agents.py +1 -2
- package/scripts/codegen/generate_frontend_tools.py +1 -1
- package/scripts/codegen/generate_models.py +33 -8
package/README.md
CHANGED
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
# ๐ค ๐ Agent Runtimes
|
|
12
12
|
|
|
13
|
-
[](https://app.netlify.com/projects/agent-runtimes/deploys)
|
|
13
|
+
[](https://github.com/datalayer/agent-runtimes/actions/workflows/build.yaml)
|
|
15
14
|
[](https://pypi.org/project/agent-runtimes)
|
|
16
15
|
|
|
17
16
|
**Agent Runtimes** is a unified library for deploying, managing, and interacting with AI agents across multiple protocols and frameworks. It provides both a Python server for hosting agents and React components for seamless integration into web and desktop applications.
|
package/lib/AgentNode.js
CHANGED
|
@@ -819,8 +819,8 @@ export function AgentNode() {
|
|
|
819
819
|
const aiAgentsUrl = getConfigUrlFromDocument('aiAgentsUrl') ||
|
|
820
820
|
import.meta.env?.VITE_DATALAYER_AI_AGENTS_URL ||
|
|
821
821
|
'https://r1.datalayer.run';
|
|
822
|
-
const
|
|
823
|
-
import.meta.env?.
|
|
822
|
+
const datalayerMcpServerUrl = getConfigUrlFromDocument('datalayerMcpServerUrl') ||
|
|
823
|
+
import.meta.env?.VITE_DATALAYER_MCP_SERVER_URL ||
|
|
824
824
|
'https://mcp.datalayer.run/mcp';
|
|
825
825
|
// Seed all per-service URLs to match the main UI login behavior.
|
|
826
826
|
const coreApi = coreStore.getState();
|
|
@@ -832,7 +832,7 @@ export function AgentNode() {
|
|
|
832
832
|
libraryUrl: iamUrl,
|
|
833
833
|
aiAgentsUrl,
|
|
834
834
|
aiInferenceUrl: aiInferenceUrl,
|
|
835
|
-
|
|
835
|
+
datalayerMcpServerUrl,
|
|
836
836
|
otelUrl: iamUrl,
|
|
837
837
|
growthUrl: iamUrl,
|
|
838
838
|
successUrl: iamUrl,
|
|
@@ -36,9 +36,11 @@ export const ping = async (baseUrl = DEFAULT_SERVICE_URLS.RUNTIMES) => {
|
|
|
36
36
|
// Check if it's a response error with status code information
|
|
37
37
|
if (error.response) {
|
|
38
38
|
const status = error.response.status;
|
|
39
|
-
throw new Error(`Health check failed: Service unhealthy (status ${status}) - ${error.message}
|
|
39
|
+
throw new Error(`Health check failed: Service unhealthy (status ${status}) - ${error.message}`, { cause: error });
|
|
40
40
|
}
|
|
41
41
|
// Re-throw other errors (network errors, etc.)
|
|
42
|
-
throw new Error(`Health check failed: ${error.message}
|
|
42
|
+
throw new Error(`Health check failed: ${error.message}`, {
|
|
43
|
+
cause: error,
|
|
44
|
+
});
|
|
43
45
|
}
|
|
44
46
|
};
|
|
@@ -36,9 +36,11 @@ export const ping = async (baseUrl = DEFAULT_SERVICE_URLS.SPACER) => {
|
|
|
36
36
|
// Check if it's a response error with status code information
|
|
37
37
|
if (error.response) {
|
|
38
38
|
const status = error.response.status;
|
|
39
|
-
throw new Error(`Health check failed: Service unhealthy (status ${status}) - ${error.message}
|
|
39
|
+
throw new Error(`Health check failed: Service unhealthy (status ${status}) - ${error.message}`, { cause: error });
|
|
40
40
|
}
|
|
41
41
|
// Re-throw other errors (network errors, etc.)
|
|
42
|
-
throw new Error(`Health check failed: ${error.message}
|
|
42
|
+
throw new Error(`Health check failed: ${error.message}`, {
|
|
43
|
+
cause: error,
|
|
44
|
+
});
|
|
43
45
|
}
|
|
44
46
|
};
|
|
@@ -31,7 +31,7 @@ import { useChatStore } from '../../stores/chatStore';
|
|
|
31
31
|
import { useConversationStore } from '../../stores/conversationStore';
|
|
32
32
|
import { generateMessageId, createUserMessage, createAssistantMessage, } from '../../types/messages';
|
|
33
33
|
import { AgentDetails } from '../../agents/AgentDetails';
|
|
34
|
-
import { AI_MODEL_CATALOGUE } from '../../specs/models';
|
|
34
|
+
import { AI_MODEL_CATALOGUE, DEFAULT_MODEL } from '../../specs/models';
|
|
35
35
|
import { internalQueryClient, isToolCallMessage, convertHistoryToDisplayItems, createProtocolAdapter, getApiBaseFromConfig, sanitizeAssistantContent, } from '../../utils';
|
|
36
36
|
import { useConfig, useSkills, useSkillActions, useContextSnapshot, useSandbox, } from '../../hooks';
|
|
37
37
|
import { useAgentRuntimeWebSocket } from '../../hooks/useAgentRuntimes';
|
|
@@ -48,6 +48,7 @@ import { InputPrompt } from '../prompt/InputPrompt';
|
|
|
48
48
|
import { isFloatingChatViewMode } from '../viewModes';
|
|
49
49
|
import { ToolApprovalBanner, ToolApprovalDialog, } from '../tools';
|
|
50
50
|
import { EphemeralNotebook } from '../notebook/EphemeralNotebook';
|
|
51
|
+
import { initialModelId, isOffered, usableModels } from './modelChoice';
|
|
51
52
|
// EphemeralDocument statically imports `@datalayer/jupyter-lexical` (which
|
|
52
53
|
// initialises Lumino-backed nodes on load). Lazy-load it so notebook-only chats
|
|
53
54
|
// never pull lexical into the bundle or trigger its module side effects.
|
|
@@ -1378,9 +1379,10 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1378
1379
|
? (protocol?.options).model
|
|
1379
1380
|
: undefined;
|
|
1380
1381
|
const offeredModels = useMemo(() => {
|
|
1382
|
+
// Only what can actually be called โ see `modelChoice` for the rule.
|
|
1381
1383
|
const fromConfig = availableModels || configQuery.data?.models;
|
|
1382
1384
|
if (fromConfig?.length) {
|
|
1383
|
-
return fromConfig;
|
|
1385
|
+
return usableModels(fromConfig);
|
|
1384
1386
|
}
|
|
1385
1387
|
/*
|
|
1386
1388
|
* The catalogue, filtered to what is worth offering.
|
|
@@ -1402,7 +1404,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1402
1404
|
provider: model.provider,
|
|
1403
1405
|
}));
|
|
1404
1406
|
if (catalogued.length > 0) {
|
|
1405
|
-
return catalogued;
|
|
1407
|
+
return usableModels(catalogued);
|
|
1406
1408
|
}
|
|
1407
1409
|
return browserModel
|
|
1408
1410
|
? [{ id: browserModel, name: browserModel, provider: 'inference' }]
|
|
@@ -1721,7 +1723,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1721
1723
|
const timer = setTimeout(adjustTextareaHeight, 0);
|
|
1722
1724
|
return () => clearTimeout(timer);
|
|
1723
1725
|
}, [adjustTextareaHeight]);
|
|
1724
|
-
// ----
|
|
1726
|
+
// ---- The model, whenever what is offered changes ----
|
|
1725
1727
|
useEffect(() => {
|
|
1726
1728
|
/*
|
|
1727
1729
|
* `offeredModels`, not the two sources it is built from.
|
|
@@ -1731,28 +1733,40 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1731
1733
|
* sources, so that model was offered by the menu and never selected. The
|
|
1732
1734
|
* menu draws nothing without a selection, so a browser agent had a model
|
|
1733
1735
|
* list of one and no model control at all.
|
|
1736
|
+
*
|
|
1737
|
+
* Re-picked, not picked once. Before the server has answered, the offer
|
|
1738
|
+
* is the browser's catalogue, and the chat chose from it โ the first row,
|
|
1739
|
+
* Alibaba, which the catalogue marks available because the platform is
|
|
1740
|
+
* entitled to it. The server then answered with the one model this
|
|
1741
|
+
* process can call, the guess was not in it, and nothing looked again:
|
|
1742
|
+
* the trigger read "none selected" while every turn was still sent to
|
|
1743
|
+
* Alibaba and came back asking for its key. A selection stands only while
|
|
1744
|
+
* it is offered and usable; otherwise the preferred model โ the agent's
|
|
1745
|
+
* own, from the server โ or the first usable one takes its place.
|
|
1734
1746
|
*/
|
|
1735
|
-
if (offeredModels.length
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1747
|
+
if (offeredModels.length === 0 || isOffered(offeredModels, selectedModel)) {
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
// The host's word first, then the server's default, then the platform's:
|
|
1751
|
+
// the first row of a catalogue is an order of listing, not a choice, and
|
|
1752
|
+
// opening on it sent the first message to Alibaba while the footer โ
|
|
1753
|
+
// drawn from the host's own model โ said Claude.
|
|
1754
|
+
const opening = initialModelId(offeredModels, initialModel || configQuery.data?.defaultModel || DEFAULT_MODEL);
|
|
1755
|
+
if (opening && opening !== selectedModel) {
|
|
1756
|
+
setSelectedModel(opening);
|
|
1757
|
+
}
|
|
1758
|
+
}, [
|
|
1759
|
+
offeredModels,
|
|
1760
|
+
selectedModel,
|
|
1761
|
+
initialModel,
|
|
1762
|
+
configQuery.data?.defaultModel,
|
|
1763
|
+
]);
|
|
1764
|
+
// ---- Initialize tools when config is available ----
|
|
1765
|
+
const toolsInitializedRef = useRef(false);
|
|
1766
|
+
useEffect(() => {
|
|
1767
|
+
// Once: a later change to the offer must not undo what the reader toggled.
|
|
1768
|
+
if (offeredModels.length > 0 && !toolsInitializedRef.current) {
|
|
1769
|
+
toolsInitializedRef.current = true;
|
|
1756
1770
|
const allToolIds = configQuery.data?.builtinTools?.map(tool => tool.id) || [];
|
|
1757
1771
|
setEnabledTools(allToolIds);
|
|
1758
1772
|
if (configQuery.data?.mcpServers) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which models a chat offers, and which one it opens on.
|
|
3
|
+
*
|
|
4
|
+
* Two rules, kept out of `ChatBase` so they can be read and tested without
|
|
5
|
+
* rendering a chat:
|
|
6
|
+
*
|
|
7
|
+
* - **Only what can be called is offered.** A server answers with its whole
|
|
8
|
+
* catalogue and a flag per model, and the menu used to draw all of it, most
|
|
9
|
+
* rows greyed out with a reason. Thirty models to choose two from reads as
|
|
10
|
+
* a broken menu, and the one thing it guarantees is that a reader's first
|
|
11
|
+
* click lands on something that cannot answer.
|
|
12
|
+
* - **The default has to be one of them.** A default naming a model this
|
|
13
|
+
* deployment cannot use โ a spec claiming the default while marked
|
|
14
|
+
* unavailable โ opened every chat on it, and the first message came back
|
|
15
|
+
* as the provider's complaint about a missing key.
|
|
16
|
+
*
|
|
17
|
+
* @module chat/base/modelChoice
|
|
18
|
+
*/
|
|
19
|
+
import type { ModelConfig } from '../../types/chat';
|
|
20
|
+
/** Whether a model can be picked: the flag says no, or says nothing. */
|
|
21
|
+
export declare const isUsable: (model: ModelConfig) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The models to offer: the usable ones.
|
|
24
|
+
*
|
|
25
|
+
* When none is usable the list is kept whole. An empty menu says nothing,
|
|
26
|
+
* while the rows and their reasons say which key is missing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function usableModels(models: ModelConfig[]): ModelConfig[];
|
|
29
|
+
/** Whether `id` is on offer and usable โ the selection may stand. */
|
|
30
|
+
export declare const isOffered: (offered: ModelConfig[], id?: string) => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The model a chat opens on.
|
|
33
|
+
*
|
|
34
|
+
* The preferred one when it is offered *and* usable; otherwise the first
|
|
35
|
+
* usable one; otherwise the first there is. `undefined` when there is none.
|
|
36
|
+
*/
|
|
37
|
+
export declare function initialModelId(offered: ModelConfig[], preferred?: string | null): string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* One row of `/api/v1/configure/models`, as the server writes it.
|
|
40
|
+
*
|
|
41
|
+
* Snake case and its own flag names: the route is read by more than this
|
|
42
|
+
* chat, and its shape is the server's to keep.
|
|
43
|
+
*/
|
|
44
|
+
export type ServerCatalogueModel = {
|
|
45
|
+
id: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
available?: boolean;
|
|
48
|
+
missing_env_vars?: string[];
|
|
49
|
+
reason?: string;
|
|
50
|
+
warning?: string | null;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The server's catalogue in the chat's shape.
|
|
54
|
+
*
|
|
55
|
+
* The route says `available`; the chat, the menu and the rules above read
|
|
56
|
+
* `isAvailable`. Handed over unmapped, every row looked usable, the menu
|
|
57
|
+
* offered all thirty-one, and the first message of a chat that opened before
|
|
58
|
+
* its spec was known went to the first row โ Alibaba, and its missing key.
|
|
59
|
+
*/
|
|
60
|
+
export declare function readServerCatalogue(payload: {
|
|
61
|
+
models?: ServerCatalogueModel[];
|
|
62
|
+
} | null | undefined): ModelConfig[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** Whether a model can be picked: the flag says no, or says nothing. */
|
|
6
|
+
export const isUsable = (model) => model.isAvailable !== false;
|
|
7
|
+
/**
|
|
8
|
+
* The models to offer: the usable ones.
|
|
9
|
+
*
|
|
10
|
+
* When none is usable the list is kept whole. An empty menu says nothing,
|
|
11
|
+
* while the rows and their reasons say which key is missing.
|
|
12
|
+
*/
|
|
13
|
+
export function usableModels(models) {
|
|
14
|
+
const usable = models.filter(isUsable);
|
|
15
|
+
return usable.length > 0 ? usable : models;
|
|
16
|
+
}
|
|
17
|
+
/** Whether `id` is on offer and usable โ the selection may stand. */
|
|
18
|
+
export const isOffered = (offered, id) => Boolean(id) && offered.some(m => m.id === id && isUsable(m));
|
|
19
|
+
/**
|
|
20
|
+
* The model a chat opens on.
|
|
21
|
+
*
|
|
22
|
+
* The preferred one when it is offered *and* usable; otherwise the first
|
|
23
|
+
* usable one; otherwise the first there is. `undefined` when there is none.
|
|
24
|
+
*/
|
|
25
|
+
export function initialModelId(offered, preferred) {
|
|
26
|
+
if (preferred && offered.some(m => m.id === preferred && isUsable(m))) {
|
|
27
|
+
return preferred;
|
|
28
|
+
}
|
|
29
|
+
const first = offered.find(isUsable) ?? offered[0];
|
|
30
|
+
return first?.id;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The server's catalogue in the chat's shape.
|
|
34
|
+
*
|
|
35
|
+
* The route says `available`; the chat, the menu and the rules above read
|
|
36
|
+
* `isAvailable`. Handed over unmapped, every row looked usable, the menu
|
|
37
|
+
* offered all thirty-one, and the first message of a chat that opened before
|
|
38
|
+
* its spec was known went to the first row โ Alibaba, and its missing key.
|
|
39
|
+
*/
|
|
40
|
+
export function readServerCatalogue(payload) {
|
|
41
|
+
return (payload?.models ?? []).map(model => {
|
|
42
|
+
const missing = model.missing_env_vars ?? [];
|
|
43
|
+
const reason = model.reason ??
|
|
44
|
+
(missing.length > 0 ? `Set ${missing.join(', ')}` : undefined) ??
|
|
45
|
+
model.warning ??
|
|
46
|
+
undefined;
|
|
47
|
+
return {
|
|
48
|
+
id: model.id,
|
|
49
|
+
name: model.name ?? model.id,
|
|
50
|
+
isAvailable: model.available !== false,
|
|
51
|
+
...(reason && model.available === false
|
|
52
|
+
? { unavailableReason: reason }
|
|
53
|
+
: {}),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -18,6 +18,8 @@ import { streamdownMarkdownStyles, streamdownCodeBlockStyles, } from '../styles/
|
|
|
18
18
|
import { ToolCallDisplay } from '../tools/ToolCallDisplay';
|
|
19
19
|
import { TurnFooter } from './TurnFooter';
|
|
20
20
|
import { normalizeAssistantMarkdown } from './assistantMarkdown';
|
|
21
|
+
import { parseFormSubmission } from './formSubmission';
|
|
22
|
+
import { FormSubmissionMessage } from './FormSubmissionMessage';
|
|
21
23
|
import { isToolCallMessage, getMessageText } from '../../utils';
|
|
22
24
|
import { A2AAgentDialog, a2aAgentDetails } from '../tools/A2AAgentDialog';
|
|
23
25
|
import { SUBAGENT_STOPPED } from '../../types/stream';
|
|
@@ -697,11 +699,17 @@ export function ChatMessageList({ displayItems, isLoading, isStreaming, showLoad
|
|
|
697
699
|
display: 'inline-flex',
|
|
698
700
|
gap: 1,
|
|
699
701
|
alignItems: 'center',
|
|
700
|
-
}, children: [_jsx(ToolsIcon, { size: 10 }), " ", label] }) }, `${label}-${chipIndex}`))) })) : null, isUser ? (
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
702
|
+
}, children: [_jsx(ToolsIcon, { size: 10 }), " ", label] }) }, `${label}-${chipIndex}`))) })) : null, isUser ? ((() => {
|
|
703
|
+
// A submitted form is drawn as the form, not as the JSON
|
|
704
|
+
// the agent reads โ see `formSubmission`.
|
|
705
|
+
const text = getMessageText(message);
|
|
706
|
+
const submission = parseFormSubmission(text);
|
|
707
|
+
return submission ? (_jsx(FormSubmissionMessage, { submission: submission })) : (_jsx(Text, { sx: {
|
|
708
|
+
fontSize: 1,
|
|
709
|
+
whiteSpace: 'pre-wrap',
|
|
710
|
+
wordBreak: 'break-word',
|
|
711
|
+
}, children: text }));
|
|
712
|
+
})()) : message.live && !getMessageText(message) ? (_jsx(TypingDots, { size: 6 })) : (_jsx(Box, { sx: streamdownMarkdownStyles, children: _jsx(Streamdown, { children: normalizeAssistantMarkdown(getMessageText(message) || (isStreaming ? '...' : '')) }) }))] }), renderMessageFooter ? (_jsx(Box, { sx: {
|
|
705
713
|
display: 'flex',
|
|
706
714
|
gap: 2,
|
|
707
715
|
alignItems: 'center',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A submitted form, as a card in the transcript.
|
|
3
|
+
*
|
|
4
|
+
* Drawn where the reader's turn would be, in the shape a tool call has: a
|
|
5
|
+
* header that names the form and says how many fields it carried, a chevron
|
|
6
|
+
* that opens it, and the fields as a two-column table underneath. The JSON
|
|
7
|
+
* the agent reads stays in the turn's text; the reader sees the form they
|
|
8
|
+
* filled in.
|
|
9
|
+
*
|
|
10
|
+
* @module chat/messages/FormSubmissionMessage
|
|
11
|
+
*/
|
|
12
|
+
import type { JSX } from 'react';
|
|
13
|
+
import { type FormSubmission } from './formSubmission';
|
|
14
|
+
export declare function FormSubmissionMessage({ submission, }: {
|
|
15
|
+
submission: FormSubmission;
|
|
16
|
+
}): JSX.Element;
|
|
17
|
+
export default FormSubmissionMessage;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Text } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { ChecklistIcon, ChevronDownIcon, ChevronRightIcon, } from '@primer/octicons-react';
|
|
6
|
+
import { displayValue, labelOfFieldId, } from './formSubmission';
|
|
7
|
+
export function FormSubmissionMessage({ submission, }) {
|
|
8
|
+
const [expanded, setExpanded] = useState(true);
|
|
9
|
+
const entries = Object.entries(submission.values);
|
|
10
|
+
const count = entries.length;
|
|
11
|
+
return (_jsxs(Box, { sx: {
|
|
12
|
+
width: '100%',
|
|
13
|
+
minWidth: 280,
|
|
14
|
+
border: '1px solid',
|
|
15
|
+
borderColor: 'border.default',
|
|
16
|
+
borderRadius: '12px',
|
|
17
|
+
overflow: 'hidden',
|
|
18
|
+
backgroundColor: 'canvas.default',
|
|
19
|
+
color: 'fg.default',
|
|
20
|
+
}, children: [_jsxs(Box, { as: "button", type: "button", "aria-expanded": expanded, onClick: () => setExpanded(value => !value), sx: {
|
|
21
|
+
width: '100%',
|
|
22
|
+
display: 'flex',
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
gap: 2,
|
|
25
|
+
padding: 2,
|
|
26
|
+
backgroundColor: 'canvas.subtle',
|
|
27
|
+
border: 'none',
|
|
28
|
+
borderBottom: expanded ? '1px solid' : 'none',
|
|
29
|
+
borderColor: 'border.default',
|
|
30
|
+
cursor: 'pointer',
|
|
31
|
+
textAlign: 'left',
|
|
32
|
+
color: 'fg.default',
|
|
33
|
+
'&:hover': { backgroundColor: 'neutral.muted' },
|
|
34
|
+
}, children: [_jsx(Box, { sx: { color: 'fg.muted', flexShrink: 0 }, children: expanded ? (_jsx(ChevronDownIcon, { size: 16 })) : (_jsx(ChevronRightIcon, { size: 16 })) }), _jsx(Box, { sx: { color: 'fg.muted', flexShrink: 0 }, children: _jsx(ChecklistIcon, { size: 16 }) }), _jsxs(Text, { sx: { fontWeight: 'semibold', fontSize: 1 }, children: ["Submitted ", submission.title] }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted', ml: 'auto' }, children: [count, " field", count === 1 ? '' : 's'] })] }), expanded ? (_jsx(Box, { as: "table", sx: { width: '100%', borderCollapse: 'collapse' }, children: _jsx(Box, { as: "tbody", children: entries.map(([id, value]) => (_jsxs(Box, { as: "tr", sx: {
|
|
35
|
+
'& + tr td': {
|
|
36
|
+
borderTop: '1px solid',
|
|
37
|
+
borderColor: 'border.muted',
|
|
38
|
+
},
|
|
39
|
+
}, children: [_jsx(Box, { as: "td", sx: {
|
|
40
|
+
px: 3,
|
|
41
|
+
py: 1,
|
|
42
|
+
fontSize: 0,
|
|
43
|
+
color: 'fg.muted',
|
|
44
|
+
whiteSpace: 'nowrap',
|
|
45
|
+
verticalAlign: 'top',
|
|
46
|
+
width: '40%',
|
|
47
|
+
}, children: labelOfFieldId(id) }), _jsx(Box, { as: "td", sx: { px: 3, py: 1, fontSize: 1, wordBreak: 'break-word' }, children: displayValue(value) })] }, id))) }) })) : null] }));
|
|
48
|
+
}
|
|
49
|
+
export default FormSubmissionMessage;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A form submission as a chat turn: written for the agent, drawn for the
|
|
3
|
+
* reader.
|
|
4
|
+
*
|
|
5
|
+
* When a reader submits a surface the agent drew, the values go back to the
|
|
6
|
+
* agent as the reader's next turn โ that is how the confirmation lands in
|
|
7
|
+
* the conversation. But a turn is text, and the reader was shown that text:
|
|
8
|
+
* a JSON blob under "here are the values", which is the agent's reading and
|
|
9
|
+
* nobody else's. So the turn carries the submission in a fenced block of its
|
|
10
|
+
* own language, `a2ui-submission`. The agent reads the JSON inside it as
|
|
11
|
+
* before; the transcript recognises the fence and draws a card instead โ
|
|
12
|
+
* the form's name and its fields, the way a tool call is drawn.
|
|
13
|
+
*
|
|
14
|
+
* Pure, so the two halves โ the sender's writing, the transcript's reading โ
|
|
15
|
+
* are tested together without a chat.
|
|
16
|
+
*
|
|
17
|
+
* @module chat/messages/formSubmission
|
|
18
|
+
*/
|
|
19
|
+
/** The fence's language: what marks a turn as a submission. */
|
|
20
|
+
export declare const FORM_SUBMISSION_LANGUAGE = "a2ui-submission";
|
|
21
|
+
export type FormSubmission = {
|
|
22
|
+
/** The form's title, as the surface named it. */
|
|
23
|
+
title: string;
|
|
24
|
+
/** The surface the values came from, when known. */
|
|
25
|
+
surfaceId?: string;
|
|
26
|
+
/** The values, by field id. */
|
|
27
|
+
values: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
/** The turn to send: a sentence, the fenced submission, the ask. */
|
|
30
|
+
export declare function encodeFormSubmission(submission: FormSubmission): string;
|
|
31
|
+
/** The submission a turn carries, or `null` for any other turn. */
|
|
32
|
+
export declare function parseFormSubmission(text: string): FormSubmission | null;
|
|
33
|
+
/**
|
|
34
|
+
* A field id as a label: `party-size` โ `Party size`.
|
|
35
|
+
*
|
|
36
|
+
* The turn carries ids, not labels โ the values are keyed by them โ and an
|
|
37
|
+
* id is a readable enough label once its dashes and underscores are spaces.
|
|
38
|
+
*/
|
|
39
|
+
export declare function labelOfFieldId(id: string): string;
|
|
40
|
+
/** A value as a reader would write it; `โ` for nothing. */
|
|
41
|
+
export declare function displayValue(value: unknown): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* A form submission as a chat turn: written for the agent, drawn for the
|
|
7
|
+
* reader.
|
|
8
|
+
*
|
|
9
|
+
* When a reader submits a surface the agent drew, the values go back to the
|
|
10
|
+
* agent as the reader's next turn โ that is how the confirmation lands in
|
|
11
|
+
* the conversation. But a turn is text, and the reader was shown that text:
|
|
12
|
+
* a JSON blob under "here are the values", which is the agent's reading and
|
|
13
|
+
* nobody else's. So the turn carries the submission in a fenced block of its
|
|
14
|
+
* own language, `a2ui-submission`. The agent reads the JSON inside it as
|
|
15
|
+
* before; the transcript recognises the fence and draws a card instead โ
|
|
16
|
+
* the form's name and its fields, the way a tool call is drawn.
|
|
17
|
+
*
|
|
18
|
+
* Pure, so the two halves โ the sender's writing, the transcript's reading โ
|
|
19
|
+
* are tested together without a chat.
|
|
20
|
+
*
|
|
21
|
+
* @module chat/messages/formSubmission
|
|
22
|
+
*/
|
|
23
|
+
/** The fence's language: what marks a turn as a submission. */
|
|
24
|
+
export const FORM_SUBMISSION_LANGUAGE = 'a2ui-submission';
|
|
25
|
+
const FENCE = new RegExp('```' + FORM_SUBMISSION_LANGUAGE + '\\s*\\n([\\s\\S]*?)\\n```');
|
|
26
|
+
/** The turn to send: a sentence, the fenced submission, the ask. */
|
|
27
|
+
export function encodeFormSubmission(submission) {
|
|
28
|
+
const json = JSON.stringify(submission, null, 2);
|
|
29
|
+
return (`I just submitted "${submission.title}".\n\n` +
|
|
30
|
+
'```' +
|
|
31
|
+
FORM_SUBMISSION_LANGUAGE +
|
|
32
|
+
'\n' +
|
|
33
|
+
json +
|
|
34
|
+
'\n```\n\n' +
|
|
35
|
+
'Please confirm you received it and briefly say what happens next.');
|
|
36
|
+
}
|
|
37
|
+
/** The submission a turn carries, or `null` for any other turn. */
|
|
38
|
+
export function parseFormSubmission(text) {
|
|
39
|
+
const match = FENCE.exec(text);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(match[1]);
|
|
45
|
+
if (!parsed ||
|
|
46
|
+
typeof parsed !== 'object' ||
|
|
47
|
+
typeof parsed.title !== 'string' ||
|
|
48
|
+
!parsed.values ||
|
|
49
|
+
typeof parsed.values !== 'object') {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
title: parsed.title,
|
|
54
|
+
...(parsed.surfaceId ? { surfaceId: String(parsed.surfaceId) } : {}),
|
|
55
|
+
values: parsed.values,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A field id as a label: `party-size` โ `Party size`.
|
|
64
|
+
*
|
|
65
|
+
* The turn carries ids, not labels โ the values are keyed by them โ and an
|
|
66
|
+
* id is a readable enough label once its dashes and underscores are spaces.
|
|
67
|
+
*/
|
|
68
|
+
export function labelOfFieldId(id) {
|
|
69
|
+
const words = id.replace(/[-_]+/g, ' ').trim();
|
|
70
|
+
return words ? words[0].toUpperCase() + words.slice(1) : id;
|
|
71
|
+
}
|
|
72
|
+
/** A value as a reader would write it; `โ` for nothing. */
|
|
73
|
+
export function displayValue(value) {
|
|
74
|
+
if (value === null || value === undefined) {
|
|
75
|
+
return 'โ';
|
|
76
|
+
}
|
|
77
|
+
if (typeof value === 'string') {
|
|
78
|
+
return value.trim() === '' ? 'โ' : value;
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
return value.length === 0 ? 'โ' : value.map(displayValue).join(', ');
|
|
82
|
+
}
|
|
83
|
+
if (typeof value === 'boolean') {
|
|
84
|
+
return value ? 'Yes' : 'No';
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === 'number') {
|
|
87
|
+
return String(value);
|
|
88
|
+
}
|
|
89
|
+
return JSON.stringify(value);
|
|
90
|
+
}
|
|
@@ -13,15 +13,22 @@ import { Box } from '@datalayer/primer-addons';
|
|
|
13
13
|
import { AiModelIcon } from '@primer/octicons-react';
|
|
14
14
|
export function ModelSelector({ models, selectedModel, onModelSelect, isA2AProtocol, }) {
|
|
15
15
|
const active = models.find(model => model.id === selectedModel);
|
|
16
|
+
/*
|
|
17
|
+
* A count, not a name โ the same trigger as tools, skills, agents and
|
|
18
|
+
* suggestions beside it. The name is the longest label on the row and it
|
|
19
|
+
* changes width as the model changes, which pushed everything beside it
|
|
20
|
+
* about; the tooltip says which one, and how many there are to choose from.
|
|
21
|
+
*/
|
|
22
|
+
const summary = isA2AProtocol
|
|
23
|
+
? 'set by the agent config'
|
|
24
|
+
: `${active?.name ?? 'none selected'} ยท ${models.length} to choose from`;
|
|
16
25
|
return (_jsxs(Box, { sx: {
|
|
17
26
|
display: 'flex',
|
|
18
27
|
flexDirection: 'column',
|
|
19
28
|
alignItems: 'flex-end',
|
|
20
|
-
}, children: [_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(Tooltip, { text: isA2AProtocol
|
|
21
|
-
? 'This protocol does not take a model override'
|
|
22
|
-
: `Model โ ${active?.name ?? 'none selected'}`, direction: "n", children: _jsx(Button, { type: "button", variant: "invisible", size: "small", "aria-label": `Model โ ${active?.name ?? 'none selected'}`, leadingVisual: AiModelIcon, disabled: isA2AProtocol, sx: isA2AProtocol
|
|
29
|
+
}, children: [_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(Tooltip, { text: `Model โ ${summary}`, direction: "n", children: _jsx(Button, { type: "button", variant: "invisible", size: "small", "aria-label": `Model โ ${summary}`, leadingVisual: AiModelIcon, disabled: isA2AProtocol, sx: isA2AProtocol
|
|
23
30
|
? { opacity: 0.5, cursor: 'not-allowed' }
|
|
24
|
-
: undefined }) }) }), _jsx(ActionMenu.Overlay, { side: "outside-top", align: "end", children: _jsx(ActionList, { selectionVariant: "single", children: models.map(modelItem => (_jsxs(ActionList.Item, { selected: selectedModel === modelItem.id, onSelect: () => onModelSelect(modelItem.id), disabled: modelItem.isAvailable === false || isA2AProtocol, sx: modelItem.isAvailable === false
|
|
31
|
+
: undefined, children: _jsx(Text, { sx: { fontSize: 0 }, children: models.length }) }) }) }), _jsx(ActionMenu.Overlay, { side: "outside-top", align: "end", children: _jsx(ActionList, { selectionVariant: "single", children: models.map(modelItem => (_jsxs(ActionList.Item, { selected: selectedModel === modelItem.id, onSelect: () => onModelSelect(modelItem.id), disabled: modelItem.isAvailable === false || isA2AProtocol, sx: modelItem.isAvailable === false
|
|
25
32
|
? { color: 'fg.muted' }
|
|
26
33
|
: undefined, children: [modelItem.name, modelItem.isAvailable === false && (_jsx(ActionList.Description, { variant: "block", children: modelItem.unavailableReason ?? 'Missing API key' }))] }, modelItem.id))) }) })] }), isA2AProtocol && (_jsx(Text, { sx: { fontSize: 0, color: 'attention.fg', mt: 1 }, children: "A2A: Model set by agent config" }))] }));
|
|
27
34
|
}
|
|
@@ -250,6 +250,46 @@ export const streamdownMarkdownStyles = {
|
|
|
250
250
|
marginTop: '1em',
|
|
251
251
|
marginBottom: '1em',
|
|
252
252
|
},
|
|
253
|
+
/*
|
|
254
|
+
* The controls Streamdown draws over a table, an image and a diagram: copy
|
|
255
|
+
* and download, and the download's own menu.
|
|
256
|
+
*
|
|
257
|
+
* They are plain `<button>`s with Tailwind classes, and Tailwind's preflight
|
|
258
|
+
* โ which we do not load โ is what strips a button of its default chrome.
|
|
259
|
+
* Without it the browser drew them as it draws any button: a grey face and
|
|
260
|
+
* a bevelled border, in a message whose ground is the theme's. The reset
|
|
261
|
+
* here is the one the code block's header already has, and the colours are
|
|
262
|
+
* Primer's, so the row reads as part of the message in either colour mode.
|
|
263
|
+
*/
|
|
264
|
+
'& [data-streamdown="table-wrapper"] button, & [data-streamdown="image-wrapper"] button, & [data-streamdown="mermaid-block"] button': {
|
|
265
|
+
background: 'transparent',
|
|
266
|
+
border: 'none',
|
|
267
|
+
font: 'inherit',
|
|
268
|
+
cursor: 'pointer',
|
|
269
|
+
color: 'fg.muted',
|
|
270
|
+
borderRadius: '4px',
|
|
271
|
+
'&:hover': {
|
|
272
|
+
backgroundColor: 'neutral.muted',
|
|
273
|
+
color: 'fg.default',
|
|
274
|
+
},
|
|
275
|
+
'&:disabled': { cursor: 'not-allowed', opacity: 0.5 },
|
|
276
|
+
},
|
|
277
|
+
'& [data-streamdown="table-wrapper"] > .flex': {
|
|
278
|
+
marginBottom: '4px',
|
|
279
|
+
},
|
|
280
|
+
// The download menu, and its rows: the popover's own ground and rule, not
|
|
281
|
+
// the button reset above.
|
|
282
|
+
'& [data-streamdown="table-wrapper"] .bg-background, & [data-streamdown="mermaid-block"] .bg-background': {
|
|
283
|
+
backgroundColor: 'canvas.overlay',
|
|
284
|
+
borderColor: 'border.default',
|
|
285
|
+
color: 'fg.default',
|
|
286
|
+
boxShadow: 'shadow.medium',
|
|
287
|
+
},
|
|
288
|
+
'& [data-streamdown="table-wrapper"] .bg-background button, & [data-streamdown="mermaid-block"] .bg-background button': {
|
|
289
|
+
color: 'fg.default',
|
|
290
|
+
borderRadius: 0,
|
|
291
|
+
'&:hover': { backgroundColor: 'neutral.muted' },
|
|
292
|
+
},
|
|
253
293
|
// Include Tailwind utilities for Streamdown output
|
|
254
294
|
...tailwindUtilities,
|
|
255
295
|
};
|
|
@@ -246,7 +246,7 @@ export function RuntimesMixin(Base) {
|
|
|
246
246
|
async checkRuntimesHealth() {
|
|
247
247
|
const startTime = Date.now();
|
|
248
248
|
const errors = [];
|
|
249
|
-
let status
|
|
249
|
+
let status;
|
|
250
250
|
let healthy = false;
|
|
251
251
|
try {
|
|
252
252
|
// Test basic connectivity by listing environments (lightweight operation)
|
|
@@ -244,9 +244,9 @@ export function SpacerMixin(Base) {
|
|
|
244
244
|
// If getItem throws (e.g., 404), wrap in descriptive error
|
|
245
245
|
if (error.message?.includes('404') ||
|
|
246
246
|
error.message?.includes('not found')) {
|
|
247
|
-
throw new Error(`Failed to delete space item '${itemId}': Item not found
|
|
247
|
+
throw new Error(`Failed to delete space item '${itemId}': Item not found`, { cause: error });
|
|
248
248
|
}
|
|
249
|
-
throw new Error(`Failed to delete space item '${itemId}': ${error.message}
|
|
249
|
+
throw new Error(`Failed to delete space item '${itemId}': ${error.message}`, { cause: error });
|
|
250
250
|
}
|
|
251
251
|
// Item exists, proceed with deletion
|
|
252
252
|
const response = await items.deleteItem(token, itemId, spacerUrl);
|
|
@@ -291,7 +291,7 @@ export function SpacerMixin(Base) {
|
|
|
291
291
|
async checkSpacerHealth() {
|
|
292
292
|
const startTime = Date.now();
|
|
293
293
|
const errors = [];
|
|
294
|
-
let status
|
|
294
|
+
let status;
|
|
295
295
|
let healthy = false;
|
|
296
296
|
try {
|
|
297
297
|
// Test basic connectivity by getting user spaces (lightweight operation)
|