@datalayer/agent-runtimes 1.3.30 → 1.3.32

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.
Files changed (79) hide show
  1. package/lib/chat/ChatFloating.js +40 -8
  2. package/lib/chat/assistant/AssistantStage.d.ts +26 -7
  3. package/lib/chat/assistant/AssistantStage.js +115 -8
  4. package/lib/chat/assistant/SpeechBalloon.js +7 -3
  5. package/lib/chat/assistant/characters.d.ts +32 -1
  6. package/lib/chat/assistant/characters.js +180 -15
  7. package/lib/chat/assistant/state.d.ts +28 -0
  8. package/lib/chat/assistant/state.js +31 -0
  9. package/lib/chat/base/ChatBase.js +34 -19
  10. package/lib/chat/base/modelChoice.d.ts +27 -1
  11. package/lib/chat/base/modelChoice.js +29 -15
  12. package/lib/chat/prompt/InputPrompt.d.ts +7 -1
  13. package/lib/chat/prompt/InputPrompt.js +2 -2
  14. package/lib/chat/prompt/InputPromptBase.js +1 -1
  15. package/lib/chat/prompt/menus/ModelSelector.d.ts +13 -1
  16. package/lib/chat/prompt/menus/ModelSelector.js +17 -5
  17. package/lib/chat/useViewportDrag.d.ts +9 -1
  18. package/lib/chat/useViewportDrag.js +4 -4
  19. package/lib/components/a2ui/index.d.ts +2 -0
  20. package/lib/components/a2ui/index.js +2 -0
  21. package/lib/components/a2ui/visibility.d.ts +15 -0
  22. package/lib/components/a2ui/visibility.js +24 -0
  23. package/lib/components/a2ui/visibleWhen.d.ts +17 -0
  24. package/lib/components/a2ui/visibleWhen.js +54 -0
  25. package/lib/examples/ChatAssistantExample.d.ts +4 -0
  26. package/lib/examples/ChatAssistantExample.js +26 -5
  27. package/lib/examples/utils/owlCharacterPlugin.d.ts +7 -0
  28. package/lib/examples/utils/owlCharacterPlugin.js +44 -0
  29. package/lib/loop/apps/AppFeedback.d.ts +27 -0
  30. package/lib/loop/apps/AppFeedback.js +78 -0
  31. package/lib/loop/apps/AppRenderer.js +17 -2
  32. package/lib/loop/apps/appspec.js +6 -3
  33. package/lib/loop/apps/checks.d.ts +10 -2
  34. package/lib/loop/apps/checks.js +21 -10
  35. package/lib/loop/apps/feedback.d.ts +51 -0
  36. package/lib/loop/apps/feedback.js +61 -0
  37. package/lib/loop/apps/index.d.ts +2 -0
  38. package/lib/loop/apps/index.js +2 -0
  39. package/lib/loop/apps/rules.d.ts +14 -0
  40. package/lib/loop/apps/rules.js +24 -0
  41. package/lib/loop/core/canvasBlocks.d.ts +16 -0
  42. package/lib/loop/core/canvasBlocks.js +13 -0
  43. package/lib/loop/core/index.d.ts +7 -0
  44. package/lib/loop/core/index.js +3 -0
  45. package/lib/loop/index.d.ts +1 -1
  46. package/lib/loop/plugins/a2ui/A2uiView.js +2 -3
  47. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +4 -2
  48. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +3 -4
  49. package/lib/loop/plugins/app-page/AppPage.js +1 -1
  50. package/lib/loop/plugins/app-page/{appPage.d.ts → appPageModel.d.ts} +1 -1
  51. package/lib/loop/plugins/app-page/index.d.ts +1 -1
  52. package/lib/loop/plugins/app-page/index.js +2 -2
  53. package/lib/loop/plugins/canvas-blocks/index.d.ts +63 -0
  54. package/lib/loop/plugins/canvas-blocks/index.js +118 -0
  55. package/lib/loop/plugins/chat/ChatView.js +7 -3
  56. package/lib/loop/plugins/chat/turnState.d.ts +5 -2
  57. package/lib/loop/plugins/chat/turnState.js +2 -1
  58. package/lib/specs/actions.js +1 -0
  59. package/lib/specs/agents/agents.js +16 -4
  60. package/lib/specs/apps.js +21 -10
  61. package/lib/specs/appspecSchema.js +13 -7
  62. package/lib/specs/models.d.ts +1 -1
  63. package/lib/specs/models.js +8 -8
  64. package/lib/specs/tools.d.ts +1 -0
  65. package/lib/specs/tools.js +19 -0
  66. package/lib/stories/loop/LoopReference.stories.d.ts +21 -0
  67. package/lib/stories/loop/LoopReference.stories.js +31 -0
  68. package/lib/stories/loop/ReferenceScreens.d.ts +53 -0
  69. package/lib/stories/loop/ReferenceScreens.js +256 -0
  70. package/lib/stories/loop/pictures-main.d.ts +1 -0
  71. package/lib/stories/loop/pictures-main.js +64 -0
  72. package/lib/types/agentspecs.d.ts +6 -1
  73. package/lib/types/chat.d.ts +5 -0
  74. package/lib/types/config.d.ts +8 -0
  75. package/package.json +9 -6
  76. package/scripts/check-cloudflare-models.py +2 -2
  77. package/scripts/codegen/generate_apps.py +2 -1
  78. package/scripts/codegen/generate_models.py +4 -4
  79. /package/lib/loop/plugins/app-page/{appPage.js → appPageModel.js} +0 -0
@@ -7,14 +7,31 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  * The floating assistant (LOOP T-21 to T-27): the chat as a character on the
8
8
  * page, after the Office Assistant — Datalayer's own characters, chosen
9
9
  * here, acting out what the agent does and speaking in a balloon.
10
+ *
11
+ * The characters offered are what the enabled plugins contribute to
12
+ * `loop.assistant.character` (T-24): Datalayer's four, and an owl from a
13
+ * small example plugin (`utils/owlCharacterPlugin`).
10
14
  */
11
15
  import { useState } from 'react';
16
+ import { buildReactorFromPlugins } from '@datalayer/reactor';
12
17
  import { Button, Heading, Text } from '@primer/react';
13
18
  import { Box } from '@datalayer/primer-addons';
14
19
  import { ThemedProvider } from './utils/themedProvider';
15
20
  import { ChatFloating } from '../chat';
16
- import { ASSISTANT_CHARACTERS } from '../chat/assistant/characters';
21
+ import { AssistantCharactersPlugin, assistantCharacterNamed, assistantCharactersOf, } from '../loop/plugins/assistant-characters';
22
+ import { OwlCharacterPlugin } from './utils/owlCharacterPlugin';
17
23
  import { readAcsCharacter, readClippyCharacter, } from '../chat/assistant/formats';
24
+ /** The plugins this page enables: Datalayer's characters and the owl's. */
25
+ const reactor = buildReactorFromPlugins([
26
+ AssistantCharactersPlugin,
27
+ OwlCharacterPlugin,
28
+ ]);
29
+ reactor.start();
30
+ /** The catalogue: what the enabled plugins contribute. */
31
+ const CATALOGUE = assistantCharactersOf(reactor).map(entry => ({
32
+ id: entry.id,
33
+ name: entry.character.name,
34
+ }));
18
35
  /**
19
36
  * A character file a person picked, read in the page (T-26): one `.acs`, or
20
37
  * a clippy.js character's `agent.js` and `map.png` (and its sounds file).
@@ -37,12 +54,16 @@ async function readPicked(files) {
37
54
  });
38
55
  }
39
56
  const ChatAssistantExample = () => {
40
- const [character, setCharacter] = useState(ASSISTANT_CHARACTERS[0].id);
57
+ const [character, setCharacter] = useState('paperclip');
58
+ // A character chosen by id is the one its plugin contributes.
59
+ const drawn = typeof character === 'string'
60
+ ? assistantCharacterNamed(reactor, character)
61
+ : character;
41
62
  const [loadError, setLoadError] = useState();
42
- return (_jsx(ThemedProvider, { children: _jsxs(Box, { sx: { minHeight: '100vh', bg: 'canvas.default', p: 4 }, children: [_jsxs(Box, { sx: { maxWidth: 720, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Floating assistant" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mb: 3 }, children: "The chat as a character on the page. It greets you, acts out what the agent is doing, and says the agent\u2019s words in a balloon while the conversation is closed. Click it to talk, drag it to move it, hover it to send it away." }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: ASSISTANT_CHARACTERS.map(option => (_jsx(Button, { variant: option.id === character ? 'primary' : 'default', onClick: () => {
63
+ return (_jsx(ThemedProvider, { children: _jsxs(Box, { sx: { minHeight: '100vh', bg: 'canvas.default', p: 4 }, children: [_jsxs(Box, { sx: { maxWidth: 720, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Floating assistant" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mb: 3 }, children: "The chat as a character on the page. It greets you, acts out what the agent is doing, and says the agent\u2019s words in a balloon while the conversation is closed. Click it to talk, drag it to move it, hover it to send it away." }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: CATALOGUE.map(option => (_jsx(Button, { variant: option.id === character ? 'primary' : 'default', onClick: () => {
43
64
  setCharacter(option.id);
44
65
  setLoadError(undefined);
45
- }, "aria-pressed": option.id === character, children: option.name }, option.id))) }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "A character you bring" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["The characters of Microsoft Office \u2014 Clippy, Merlin, Links and the others \u2014 are Microsoft\u2019s. Load only a character file you have the right to use: a Microsoft Agent ", _jsx("code", { children: ".acs" }), " file, or a clippy.js character\u2019s ", _jsx("code", { children: "agent.js" }), " and map image. It is read in this page and sent nowhere."] }), _jsx("input", { type: "file", multiple: true, accept: ".acs,.js,.png,.gif,.webp,.jpg,.jpeg", "aria-label": "Character files", onChange: async (event) => {
66
+ }, "aria-pressed": option.id === character, "data-assistant-character": option.id, children: option.name }, option.id))) }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "A character you bring" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["The characters of Microsoft Office \u2014 Clippy, Merlin, Links and the others \u2014 are Microsoft\u2019s. Load only a character file you have the right to use: a Microsoft Agent ", _jsx("code", { children: ".acs" }), " file, or a clippy.js character\u2019s ", _jsx("code", { children: "agent.js" }), " and map image. It is read in this page and sent nowhere."] }), _jsx("input", { type: "file", multiple: true, accept: ".acs,.js,.png,.gif,.webp,.jpg,.jpeg", "aria-label": "Character files", onChange: async (event) => {
46
67
  const files = Array.from(event.target.files ?? []);
47
68
  if (!files.length) {
48
69
  return;
@@ -54,6 +75,6 @@ const ChatAssistantExample = () => {
54
75
  catch (error) {
55
76
  setLoadError(error instanceof Error ? error.message : String(error));
56
77
  }
57
- } }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: character, protocol: "vercel-ai", endpoint: "http://127.0.0.1:8765/api/v1/vercel-ai/assistant", title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false }, typeof character === 'string' ? character : character.sprite)] }) }));
78
+ } }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, protocol: "vercel-ai", endpoint: "http://127.0.0.1:8765/api/v1/vercel-ai/assistant", title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false }, typeof character === 'string' ? character : character.sprite)] }) }));
58
79
  };
59
80
  export default ChatAssistantExample;
@@ -0,0 +1,7 @@
1
+ import type { AssistantCharacter } from '../../chat/assistant/characters';
2
+ /** The owl, as the example plugin contributes it. */
3
+ export declare const OWL_CHARACTER: AssistantCharacter;
4
+ export declare const OWL_CHARACTER_PLUGIN_NAME = "@datalayer/example-plugin-owl-character";
5
+ /** The example plugin: one character, contributed to the point. */
6
+ export declare const OwlCharacterPlugin: import("@datalayer/reactor").ReactorPlugin<Record<string, never>, unknown, unknown>;
7
+ export default OwlCharacterPlugin;
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { contribution, definePlugin } from '@datalayer/reactor';
3
+ import { LoopAssistantCharacter } from '../../loop/core';
4
+ const OWL = {
5
+ light: {
6
+ feathers: '#8A5A3C',
7
+ belly: '#E9D3B8',
8
+ tufts: '#6B4329',
9
+ beak: '#E0A54A',
10
+ eye: '#FFFFFF',
11
+ ring: '#1D1D1F',
12
+ pupil: '#1D1D1F',
13
+ },
14
+ dark: {
15
+ feathers: '#C08B66',
16
+ belly: '#F1E1CC',
17
+ tufts: '#D9A57F',
18
+ beak: '#F8D469',
19
+ eye: '#F5F5F7',
20
+ ring: '#1D1D1F',
21
+ pupil: '#131314',
22
+ },
23
+ };
24
+ function Owl({ size, mode = 'light', }) {
25
+ const c = OWL[mode];
26
+ return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "Owl", "data-assistant-mode": mode, children: _jsxs("g", { className: "assistant-body", children: [_jsx("path", { d: "M26 30 L22 10 L40 22 Z", fill: c.tufts }), _jsx("path", { d: "M70 30 L74 10 L56 22 Z", fill: c.tufts }), _jsx("ellipse", { cx: 48, cy: 54, rx: 28, ry: 34, fill: c.feathers }), _jsx("ellipse", { cx: 48, cy: 66, rx: 17, ry: 20, fill: c.belly }), [34, 62].map(x => (_jsx("circle", { cx: x, cy: 40, r: 11, fill: c.eye, stroke: c.ring, strokeWidth: 1.5 }, x))), _jsxs("g", { className: "assistant-pupils", children: [_jsx("circle", { cx: 35, cy: 41, r: 5, fill: c.pupil }), _jsx("circle", { cx: 63, cy: 41, r: 5, fill: c.pupil })] }), _jsxs("g", { className: "assistant-lids", children: [_jsx("ellipse", { cx: 34, cy: 40, rx: 11.5, ry: 11.5, fill: c.feathers }), _jsx("ellipse", { cx: 62, cy: 40, rx: 11.5, ry: 11.5, fill: c.feathers })] }), _jsx("path", { className: "assistant-mouth", d: "M44 50 L52 50 L48 58 Z", fill: c.beak })] }) }));
27
+ }
28
+ /** The owl, as the example plugin contributes it. */
29
+ export const OWL_CHARACTER = {
30
+ id: 'owl',
31
+ name: 'Owl',
32
+ Drawing: Owl,
33
+ };
34
+ export const OWL_CHARACTER_PLUGIN_NAME = '@datalayer/example-plugin-owl-character';
35
+ /** The example plugin: one character, contributed to the point. */
36
+ export const OwlCharacterPlugin = definePlugin({
37
+ name: OWL_CHARACTER_PLUGIN_NAME,
38
+ displayName: 'Owl character (example)',
39
+ description: 'An owl for the floating assistant, contributed by a plugin.',
40
+ contributes: [
41
+ contribution(LoopAssistantCharacter, { id: OWL_CHARACTER.id, character: OWL_CHARACTER }, { id: OWL_CHARACTER.id }),
42
+ ],
43
+ });
44
+ export default OwlCharacterPlugin;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The feedback strip above an application's prompt (LOOP V-18): once an
3
+ * answer is in, a thumb up or down and a comment, kept in the application's
4
+ * record under the conversation it was about.
5
+ *
6
+ * Read through what the workspace offers, not around it: the chat keeps the
7
+ * current turn — the question, the answer, the conversation's thread — in
8
+ * `LoopChatTurn`, and the composer hangs what is contributed to
9
+ * `LoopPromptPanel` beside the prompt. `AppRenderer` adds the plugin for an
10
+ * application whose record keeps feedback, in the Studio's Preview and on
11
+ * the hosted page alike.
12
+ *
13
+ * @module loop/apps/AppFeedback
14
+ */
15
+ import type { JSX } from 'react';
16
+ import type { AppSpec } from '../../types/agentspecs';
17
+ import { type LoopWorkspaceContext } from '../core';
18
+ export declare const APP_FEEDBACK_PLUGIN_NAME = "@datalayer/loop-plugin-app-feedback";
19
+ export declare function AppFeedback({ app, workspace, }: {
20
+ app: Pick<AppSpec, 'name'>;
21
+ workspace: LoopWorkspaceContext;
22
+ }): JSX.Element | null;
23
+ /**
24
+ * The plugin, for one application: the strip above its prompt. Made once per
25
+ * application — a new plugin rebuilds the workspace.
26
+ */
27
+ export declare function defineAppFeedbackPlugin(app: AppSpec): import("@datalayer/reactor").ReactorPlugin<Record<string, never>, unknown, unknown>;
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Button, IconButton, Text, Textarea } from '@primer/react';
4
+ import { ThumbsdownIcon, ThumbsupIcon } from '@primer/octicons-react';
5
+ import { contribution, definePlugin, signal } from '@datalayer/reactor';
6
+ import { useContributions, useSignalValue } from '@datalayer/reactor/react';
7
+ import { useIAMStore } from '../../state';
8
+ import { LoopChatTurn, LoopPromptPanel, } from '../core';
9
+ import { FEEDBACK_WORDS, sendFeedback } from './feedback';
10
+ export const APP_FEEDBACK_PLUGIN_NAME = '@datalayer/loop-plugin-app-feedback';
11
+ /* A signal to read when no chat contributed a turn: the hook needs one. */
12
+ const NO_TURN = signal({ id: 0, status: 'idle' });
13
+ export function AppFeedback({ app, workspace, }) {
14
+ const turns = useContributions(LoopChatTurn);
15
+ const turn = useSignalValue(turns[0]?.value.turn ?? NO_TURN);
16
+ const token = useIAMStore(state => state.token);
17
+ const [liked, setLiked] = useState(null);
18
+ const [comment, setComment] = useState('');
19
+ const [said, setSaid] = useState(null);
20
+ // Only under an answer that is in, in a conversation the runtime named.
21
+ if (turn.status !== 'done' || !turn.assistant || !turn.thread) {
22
+ return null;
23
+ }
24
+ const thread = turn.thread;
25
+ const choice = liked?.turn === turn.id ? liked.liked : null;
26
+ const done = said?.turn === turn.id ? said : null;
27
+ const agentBaseUrl = workspace.sandbox.agentBaseUrl || workspace.serverUrl || '';
28
+ const send = async () => {
29
+ if (choice === null) {
30
+ return;
31
+ }
32
+ setSaid({ turn: turn.id, state: 'sending', says: '' });
33
+ try {
34
+ await sendFeedback({ session: thread, liked: choice, comment }, { agentBaseUrl, token });
35
+ setSaid({
36
+ turn: turn.id,
37
+ state: 'kept',
38
+ says: FEEDBACK_WORDS.kept(app.name),
39
+ });
40
+ setComment('');
41
+ }
42
+ catch (error) {
43
+ setSaid({
44
+ turn: turn.id,
45
+ state: 'refused',
46
+ says: error instanceof Error ? error.message : String(error),
47
+ });
48
+ }
49
+ };
50
+ if (done?.state === 'kept') {
51
+ return (_jsx("div", { style: { padding: '4px 8px' }, children: _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: done.says }) }));
52
+ }
53
+ return (_jsxs("div", { role: "group", "aria-label": FEEDBACK_WORDS.ask, style: {
54
+ display: 'flex',
55
+ flexDirection: 'column',
56
+ gap: 4,
57
+ padding: '4px 8px',
58
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: FEEDBACK_WORDS.ask }), _jsx(IconButton, { size: "small", variant: choice === true ? 'primary' : 'invisible', icon: ThumbsupIcon, "aria-label": FEEDBACK_WORDS.liked, "aria-pressed": choice === true, onClick: () => setLiked({ turn: turn.id, liked: true }) }), _jsx(IconButton, { size: "small", variant: choice === false ? 'danger' : 'invisible', icon: ThumbsdownIcon, "aria-label": FEEDBACK_WORDS.disliked, "aria-pressed": choice === false, onClick: () => setLiked({ turn: turn.id, liked: false }) })] }), choice !== null ? (_jsxs(_Fragment, { children: [_jsx(Textarea, { "aria-label": FEEDBACK_WORDS.comment, placeholder: FEEDBACK_WORDS.comment, value: comment, rows: 2, resize: "vertical", block: true, onChange: event => setComment(event.target.value) }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx(Button, { size: "small", variant: "primary", disabled: done?.state === 'sending', onClick: () => void send(), children: FEEDBACK_WORDS.send }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: FEEDBACK_WORDS.where(app.name) })] })] })) : null, done?.state === 'refused' ? (_jsx(Text, { sx: { fontSize: 0, color: 'danger.fg' }, children: done.says })) : null] }));
59
+ }
60
+ /**
61
+ * The plugin, for one application: the strip above its prompt. Made once per
62
+ * application — a new plugin rebuilds the workspace.
63
+ */
64
+ export function defineAppFeedbackPlugin(app) {
65
+ function Panel({ workspace, }) {
66
+ return _jsx(AppFeedback, { app: app, workspace: workspace });
67
+ }
68
+ return definePlugin({
69
+ name: `${APP_FEEDBACK_PLUGIN_NAME}-${app.id}`,
70
+ displayName: `${app.name}: feedback`,
71
+ description: `A thumb and a comment on each answer of ${app.name}, kept in its record.`,
72
+ octicon: 'thumbsup',
73
+ emoji: '\u{1F44D}',
74
+ contributes: [
75
+ contribution(LoopPromptPanel, { id: 'app-feedback', placement: 'above', order: 20, Component: Panel }, { id: 'app-feedback' }),
76
+ ],
77
+ });
78
+ }
@@ -38,6 +38,8 @@ import { defineAgentCapacityPlugin } from '../plugins/agent-capacity';
38
38
  import { APP_PAGE_SURFACE, defineAppPagePlugin, hasAppPage, } from '../plugins/app-page';
39
39
  import { LoopEmbed } from '../embed/LoopEmbed';
40
40
  import { dumpAppspec } from './appspec';
41
+ import { defineAppFeedbackPlugin } from './AppFeedback';
42
+ import { keepsFeedback } from './feedback';
41
43
  /** The id of an agent or a Cog, without its version. */
42
44
  export const agentIdOf = (app) => {
43
45
  const reference = app.agent || app.team;
@@ -171,9 +173,22 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, f
171
173
  const pagePlugin = useMemo(() => (withPage ? defineAppPagePlugin(app) : null),
172
174
  // eslint-disable-next-line react-hooks/exhaustive-deps
173
175
  [source, withPage]);
176
+ /*
177
+ * A thumb and a comment on each answer, kept in its record (LOOP V-18):
178
+ * only for an application whose record keeps feedback.
179
+ */
180
+ const keeps = keepsFeedback(app);
181
+ const feedbackPlugin = useMemo(() => (keeps ? defineAppFeedbackPlugin(app) : null),
182
+ // eslint-disable-next-line react-hooks/exhaustive-deps
183
+ [source, keeps]);
174
184
  const allPlugins = useMemo(() => appPlugin
175
- ? [appPlugin, ...(pagePlugin ? [pagePlugin] : []), ...plugins]
176
- : plugins, [appPlugin, pagePlugin, plugins]);
185
+ ? [
186
+ appPlugin,
187
+ ...(pagePlugin ? [pagePlugin] : []),
188
+ ...(feedbackPlugin ? [feedbackPlugin] : []),
189
+ ...plugins,
190
+ ]
191
+ : plugins, [appPlugin, pagePlugin, feedbackPlugin, plugins]);
177
192
  /*
178
193
  * On Datalayer, the application runs on a runtime: allocated with a plain
179
194
  * agentspec, its agent created there with the application's spec — so that
@@ -105,6 +105,7 @@ export function emptyAppspec(kind = 'chat') {
105
105
  keepFor: DEFAULT_KEEP_FOR,
106
106
  retentionDays: retentionDays(DEFAULT_KEEP_FOR),
107
107
  include: [...DEFAULT_RECORD_INCLUDE],
108
+ suggestTests: false,
108
109
  },
109
110
  checks: { guards: [], gates: [], track: '' },
110
111
  deployment: {},
@@ -243,7 +244,7 @@ function parseDecision(data) {
243
244
  name: text(scenario.name),
244
245
  weights: Object.fromEntries(Object.entries(isData(scenario.weights) ? scenario.weights : {}).filter((entry) => typeof entry[1] === 'number')),
245
246
  })),
246
- judgmentModel: text(data.judgment_model),
247
+ decisionModel: text(data.decision_model),
247
248
  };
248
249
  }
249
250
  function parseDeployment(data) {
@@ -359,6 +360,7 @@ export function parseAppspec(document) {
359
360
  include: Array.isArray(record.include)
360
361
  ? texts(record.include)
361
362
  : [...DEFAULT_RECORD_INCLUDE],
363
+ suggestTests: record.suggest_tests === true,
362
364
  },
363
365
  checks: {
364
366
  guards: texts(checks.guards),
@@ -493,7 +495,7 @@ function dumpDecision(decision) {
493
495
  .text('name', scenario.name, '\u0000')
494
496
  // By criterion name, in alphabetical order: the spec declares no order.
495
497
  .part('weights', sorted(scenario.weights)).data))
496
- .text('judgment_model', decision.judgmentModel).data;
498
+ .text('decision_model', decision.decisionModel).data;
497
499
  }
498
500
  /**
499
501
  * An application as the document its file holds.
@@ -554,7 +556,8 @@ export function dumpAppspec(app) {
554
556
  }))).data)
555
557
  .part('record', new Writer()
556
558
  .text('keep_for', app.record.keepFor, DEFAULT_KEEP_FOR)
557
- .list('include', app.record.include, DEFAULT_RECORD_INCLUDE).data)
559
+ .list('include', app.record.include, DEFAULT_RECORD_INCLUDE)
560
+ .value('suggest_tests', app.record.suggestTests, false).data)
558
561
  .part('checks', new Writer()
559
562
  .list('guards', app.checks.guards)
560
563
  .list('gates', app.checks.gates)
@@ -16,8 +16,16 @@ export interface AppCheck {
16
16
  export declare const componentNamed: (name: string) => ComponentSpec | undefined;
17
17
  /** The Guards the runtime executes (agent-runtimes `loop/apps/guards.py`). */
18
18
  export declare const EXECUTED_GUARDS: readonly string[];
19
+ /**
20
+ * Where an application is checked: the plugins its organization has turned
21
+ * off (catalogue ids, `plugins_off` in IAM), none when it has decided nothing
22
+ * or the checks run with no organization.
23
+ */
24
+ export interface CheckContext {
25
+ pluginsOff: readonly string[];
26
+ }
19
27
  /** The instant checks of an application as an editor holds it. */
20
- export declare function checkApp(app: AppSpec, read?: string[]): AppCheck;
28
+ export declare function checkApp(app: AppSpec, read?: string[], context?: CheckContext): AppCheck;
21
29
  /**
22
30
  * What the document says in a shape the spec refuses — a list where a list is
23
31
  * not, a word where a choice is — which the tolerant reader replaces with a
@@ -25,4 +33,4 @@ export declare function checkApp(app: AppSpec, read?: string[]): AppCheck;
25
33
  */
26
34
  export declare function documentShapeProblems(document: unknown): string[];
27
35
  /** The instant checks of an application's document, what reading it found included. */
28
- export declare function checkAppspec(document: unknown): AppCheck;
36
+ export declare function checkAppspec(document: unknown, context?: CheckContext): AppCheck;
@@ -38,6 +38,7 @@ import { getSkillSpec } from '../../specs/skills';
38
38
  import { getTeamSpec } from '../../specs/teams';
39
39
  import { getToolSpec } from '../../specs/tools';
40
40
  import { getTrack } from '../../specs/tracks';
41
+ import { pluginsOffSetupNotes } from '../plugins/canvas-blocks';
41
42
  import { parseAppspec } from './appspec';
42
43
  import { classesOf, splitRef, toolBehaviours } from './rules';
43
44
  export const NOT_READY = 'Not ready';
@@ -192,14 +193,14 @@ function referenceProblems(app) {
192
193
  problems.push(`The surface's “${String(node.id)}” is a “${String(node.component)}”, which the catalog does not have.`);
193
194
  }
194
195
  }
195
- const judge = app.decision?.judgmentModel;
196
- if (judge) {
197
- const model = getModel(judge);
196
+ const decider = app.decision?.decisionModel;
197
+ if (decider) {
198
+ const model = getModel(decider);
198
199
  if (!model) {
199
- problems.push(`There is no model named “${judge}” to judge with.`);
200
+ problems.push(`There is no model named “${decider}” to decide with.`);
200
201
  }
201
- else if (!(model.capabilities ?? []).includes('judgments')) {
202
- problems.push(`The model “${judge}” does not answer typed judgments.`);
202
+ else if (!(model.capabilities ?? []).includes('decisions')) {
203
+ problems.push(`The model “${decider}” does not answer typed decisions.`);
203
204
  }
204
205
  }
205
206
  const run = new Set(app.checks.guards.map(idOf));
@@ -336,10 +337,16 @@ function setupNotes(app) {
336
337
  note('notification', ref, getNotificationSpec(idOf(ref)));
337
338
  return setup;
338
339
  }
340
+ const NO_ORGANIZATION = { pluginsOff: [] };
339
341
  /** The instant checks of an application as an editor holds it. */
340
- export function checkApp(app, read = []) {
342
+ export function checkApp(app, read = [], context = NO_ORGANIZATION) {
341
343
  const problems = [...read, ...shapeProblems(app), ...referenceProblems(app)];
342
- const setup = setupNotes(app);
344
+ // A block its page uses from a plugin its organization turned off is said
345
+ // with the rest of what is not enabled today (C-12).
346
+ const setup = [
347
+ ...setupNotes(app),
348
+ ...pluginsOffSetupNotes(app, context.pluginsOff),
349
+ ];
343
350
  if (problems.length > 0) {
344
351
  return { verdict: NOT_READY, problems, attention: [], setup };
345
352
  }
@@ -540,6 +547,10 @@ export function documentShapeProblems(document) {
540
547
  else
541
548
  record.include.forEach((item, index) => oneOf(item, ENUMS.record, `record.include.${index}`));
542
549
  }
550
+ if (record.suggest_tests !== undefined &&
551
+ typeof record.suggest_tests !== 'boolean') {
552
+ at('record.suggest_tests', 'is true or false');
553
+ }
543
554
  });
544
555
  mapping(d.checks, 'checks', checks => {
545
556
  texts(checks.guards, 'checks.guards');
@@ -596,7 +607,7 @@ export function documentShapeProblems(document) {
596
607
  return problems;
597
608
  }
598
609
  /** The instant checks of an application's document, what reading it found included. */
599
- export function checkAppspec(document) {
610
+ export function checkAppspec(document, context = NO_ORGANIZATION) {
600
611
  const { app, problems } = parseAppspec(document);
601
- return checkApp(app, [...problems, ...documentShapeProblems(document)]);
612
+ return checkApp(app, [...problems, ...documentShapeProblems(document)], context);
602
613
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Feedback from the people who use an application (LOOP V-18): a thumb up or
3
+ * down on an answer, and a comment, kept in the application's record.
4
+ *
5
+ * Kept only when the application's `record.include` names `feedback` — its
6
+ * builder says what its record keeps, and the chat offers nothing it would
7
+ * not keep. Sent to the runtime the conversation runs on
8
+ * (`POST /api/v1/apps/feedback`), which writes it through the recorder of
9
+ * that conversation, under its session: the conversation's AG-UI thread.
10
+ *
11
+ * Pure: no React; the network is the `fetch` it is handed.
12
+ *
13
+ * @module loop/apps/feedback
14
+ */
15
+ import type { AppSpec } from '../../types/agentspecs';
16
+ /** The longest comment kept, as the runtime holds it (`COMMENT_LIMIT`). */
17
+ export declare const FEEDBACK_COMMENT_LIMIT = 2000;
18
+ /** What a person says of an answer. */
19
+ export type Feedback = {
20
+ /** The conversation, as the runtime knows it. */
21
+ session: string;
22
+ liked: boolean;
23
+ comment: string;
24
+ };
25
+ /** What the feedback strip says, in a person's words. */
26
+ export declare const FEEDBACK_WORDS: {
27
+ readonly ask: "Was this answer useful?";
28
+ readonly liked: "Useful";
29
+ readonly disliked: "Not useful";
30
+ readonly comment: "Anything to add? (optional)";
31
+ readonly send: "Send";
32
+ readonly kept: (app: string) => string;
33
+ readonly where: (app: string) => string;
34
+ readonly tooLong: "A comment is at most 2000 characters.";
35
+ readonly noSession: "This conversation has no name the application knows yet: send a message first.";
36
+ };
37
+ /** Whether an application keeps feedback: its record names it. */
38
+ export declare const keepsFeedback: (app: Pick<AppSpec, "record">) => boolean;
39
+ /** Where feedback is sent, on the runtime an application runs on. */
40
+ export declare const feedbackEndpoint: (agentBaseUrl: string) => string;
41
+ /** Why feedback cannot be sent as it is; nothing when it can. */
42
+ export declare function feedbackProblems(feedback: Feedback): string[];
43
+ /**
44
+ * Send feedback to the runtime, and say what it kept: the entry, in a
45
+ * sentence. A refusal throws the runtime's own sentence.
46
+ */
47
+ export declare function sendFeedback(feedback: Feedback, { agentBaseUrl, token, fetcher, }: {
48
+ agentBaseUrl: string;
49
+ token?: string | null;
50
+ fetcher?: typeof fetch;
51
+ }): Promise<string>;
@@ -0,0 +1,61 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** The longest comment kept, as the runtime holds it (`COMMENT_LIMIT`). */
6
+ export const FEEDBACK_COMMENT_LIMIT = 2000;
7
+ /** What the feedback strip says, in a person's words. */
8
+ export const FEEDBACK_WORDS = {
9
+ ask: 'Was this answer useful?',
10
+ liked: 'Useful',
11
+ disliked: 'Not useful',
12
+ comment: 'Anything to add? (optional)',
13
+ send: 'Send',
14
+ kept: (app) => `Thank you. Kept in ${app}’s record.`,
15
+ where: (app) => `What you say is kept with this conversation in ${app}’s record, for its builder to read.`,
16
+ tooLong: `A comment is at most ${FEEDBACK_COMMENT_LIMIT} characters.`,
17
+ noSession: 'This conversation has no name the application knows yet: send a message first.',
18
+ };
19
+ /** Whether an application keeps feedback: its record names it. */
20
+ export const keepsFeedback = (app) => (app.record?.include ?? []).includes('feedback');
21
+ /** Where feedback is sent, on the runtime an application runs on. */
22
+ export const feedbackEndpoint = (agentBaseUrl) => `${agentBaseUrl.replace(/\/+$/, '')}/api/v1/apps/feedback`;
23
+ /** Why feedback cannot be sent as it is; nothing when it can. */
24
+ export function feedbackProblems(feedback) {
25
+ const problems = [];
26
+ if (!feedback.session.trim()) {
27
+ problems.push(FEEDBACK_WORDS.noSession);
28
+ }
29
+ if (feedback.comment.trim().length > FEEDBACK_COMMENT_LIMIT) {
30
+ problems.push(FEEDBACK_WORDS.tooLong);
31
+ }
32
+ return problems;
33
+ }
34
+ /**
35
+ * Send feedback to the runtime, and say what it kept: the entry, in a
36
+ * sentence. A refusal throws the runtime's own sentence.
37
+ */
38
+ export async function sendFeedback(feedback, { agentBaseUrl, token, fetcher = fetch, }) {
39
+ const problems = feedbackProblems(feedback);
40
+ if (problems.length) {
41
+ throw new Error(problems.join(' '));
42
+ }
43
+ const response = await fetcher(feedbackEndpoint(agentBaseUrl), {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
48
+ },
49
+ body: JSON.stringify({
50
+ session: feedback.session,
51
+ liked: feedback.liked,
52
+ comment: feedback.comment.trim(),
53
+ }),
54
+ });
55
+ const body = (await response.json().catch(() => ({})));
56
+ if (!response.ok) {
57
+ const detail = typeof body.detail === 'string' ? body.detail : `${response.status}`;
58
+ throw new Error(`The feedback was not kept: ${detail}`);
59
+ }
60
+ return String(body.summary ?? '');
61
+ }
@@ -4,8 +4,10 @@
4
4
  * @module loop/apps
5
5
  */
6
6
  export * from './agent';
7
+ export * from './AppFeedback';
7
8
  export * from './AppRenderer';
8
9
  export * from './appspec';
9
10
  export * from './checks';
11
+ export * from './feedback';
10
12
  export * from './rules';
11
13
  export * from './yaml';
@@ -8,8 +8,10 @@
8
8
  * @module loop/apps
9
9
  */
10
10
  export * from './agent';
11
+ export * from './AppFeedback';
11
12
  export * from './AppRenderer';
12
13
  export * from './appspec';
13
14
  export * from './checks';
15
+ export * from './feedback';
14
16
  export * from './rules';
15
17
  export * from './yaml';
@@ -39,6 +39,20 @@ export declare function classesOf(ref: string, args?: ToolArguments): ActionClas
39
39
  export declare const isReadOnly: (classes: ActionClass[]) => boolean;
40
40
  /** The most restricted of several behaviours. */
41
41
  export declare const strictest: (behaviours: AppBehaviour[]) => AppBehaviour;
42
+ /**
43
+ * Whether the application is given a tool of a server: its connection's
44
+ * level (LOOP U-15). A connection gives the tools of its server it reaches
45
+ * (`only`); one that only reads gives only the tools that only read — a tool
46
+ * nobody classed is taken to write, so a read connection does not give it.
47
+ * A tool of a server the application is not connected to is not given.
48
+ *
49
+ * The runtime gives its agent only these (`AppRulesCapability.prepare_tools`,
50
+ * through `agent_runtimes.loop.apps.rules.gives`, its Python twin).
51
+ *
52
+ * @throws When `tool` names no server: a catalogue tool is given by the
53
+ * spec's `tools`, not by a connection.
54
+ */
55
+ export declare function gives(app: Pick<AppSpec, 'connections'>, tool: string): boolean;
42
56
  /** What `behaviourFor` may be told about the call. */
43
57
  export interface BehaviourOptions {
44
58
  /** The arguments of the call; without them the decision is for the worst the tool can do. */
@@ -161,6 +161,30 @@ export const strictest = (behaviours) => behaviours.reduce((worst, behaviour) =>
161
161
  const connectionTo = (app, server) => app.connections.find(connection => idOf(connection.server) === server);
162
162
  const reaches = (connection, tool) => connection.only.length === 0 ||
163
163
  connection.only.some(pattern => matchesPattern(tool, pattern));
164
+ /**
165
+ * Whether the application is given a tool of a server: its connection's
166
+ * level (LOOP U-15). A connection gives the tools of its server it reaches
167
+ * (`only`); one that only reads gives only the tools that only read — a tool
168
+ * nobody classed is taken to write, so a read connection does not give it.
169
+ * A tool of a server the application is not connected to is not given.
170
+ *
171
+ * The runtime gives its agent only these (`AppRulesCapability.prepare_tools`,
172
+ * through `agent_runtimes.loop.apps.rules.gives`, its Python twin).
173
+ *
174
+ * @throws When `tool` names no server: a catalogue tool is given by the
175
+ * spec's `tools`, not by a connection.
176
+ */
177
+ export function gives(app, tool) {
178
+ const [server, name] = splitRef(tool);
179
+ if (server === undefined) {
180
+ throw new Error(`\`${tool}\` is not a tool of a server (\`server.tool\`).`);
181
+ }
182
+ const connection = connectionTo(app, server);
183
+ if (!connection || !reaches(connection, name)) {
184
+ return false;
185
+ }
186
+ return (connection.access !== 'read' || isReadOnly(classesOf(`${server}.${name}`)));
187
+ }
164
188
  const isClass = (target) => Object.prototype.hasOwnProperty.call(DEFAULT_BEHAVIOURS, target);
165
189
  /** What a rule applies to, versions aside. */
166
190
  const normal = (target) => {
@@ -0,0 +1,16 @@
1
+ import type { ComponentSpec } from '../../types/agentspecs';
2
+ /**
3
+ * A block the Canvas may place (LOOP C-12): one component of the catalog of
4
+ * visual components (C-13), contributed by the plugin of the UI plugin that
5
+ * renders it. The Canvas's palette is what the enabled plugins contribute
6
+ * here, and nothing else: a plugin disabled takes its blocks off it, and an
7
+ * application that uses one of them says so in its setup notes.
8
+ */
9
+ export type CanvasBlockContribution = {
10
+ /** The name a surface gives it, the component's id (e.g. 'Table'). */
11
+ id: string;
12
+ /** The UI plugin of the catalogue that renders it (e.g. 'a2ui'). */
13
+ uiPlugin: string;
14
+ component: ComponentSpec;
15
+ };
16
+ export declare const LoopCanvasBlock: import("@datalayer/reactor").ContributionPoint<CanvasBlockContribution>;
@@ -0,0 +1,13 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The Canvas's palette as a contribution point (LOOP C-12), re-exported by
7
+ * `loop/core`: kept apart so that reading the palette loads the reactor and
8
+ * the catalogue, and nothing of the workspace.
9
+ *
10
+ * @module loop/core/canvasBlocks
11
+ */
12
+ import { defineContributionPoint } from '@datalayer/reactor';
13
+ export const LoopCanvasBlock = defineContributionPoint('loop.canvas.block');
@@ -526,6 +526,7 @@ export type AssistantCharacterContribution = {
526
526
  character: AssistantCharacter | AssistantCharacterData;
527
527
  };
528
528
  export declare const LoopAssistantCharacter: import("@datalayer/reactor").ContributionPoint<AssistantCharacterContribution>;
529
+ export { LoopCanvasBlock, type CanvasBlockContribution } from './canvasBlocks';
529
530
  /**
530
531
  * The per-example chat extras a host feeds the loop's conversation live.
531
532
  *
@@ -935,6 +936,12 @@ export type ChatTurnSnapshot = {
935
936
  user?: string;
936
937
  /** What the agent has said so far. */
937
938
  assistant?: string;
939
+ /**
940
+ * The conversation the turn belongs to, as the runtime knows it (AG-UI's
941
+ * thread) — what an application's record keeps its session under, and
942
+ * what feedback on it names (LOOP V-18). Absent when the chat does not say.
943
+ */
944
+ thread?: string;
938
945
  status: ChatTurnStatus;
939
946
  /**
940
947
  * The context window as the agent last reported it — what a turn footer