@datalayer/agent-runtimes 1.3.58 → 1.3.60

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 (58) hide show
  1. package/lib/chat/ChatFloating.d.ts +11 -1
  2. package/lib/chat/ChatFloating.js +6 -2
  3. package/lib/chat/assistant/AssistantStage.d.ts +7 -1
  4. package/lib/chat/assistant/AssistantStage.js +5 -3
  5. package/lib/chat/assistant/DecisionAsk.d.ts +31 -0
  6. package/lib/chat/assistant/DecisionAsk.js +96 -0
  7. package/lib/chat/assistant/SpeechBalloon.d.ts +11 -2
  8. package/lib/chat/assistant/SpeechBalloon.js +5 -4
  9. package/lib/chat/assistant/decisions.d.ts +104 -0
  10. package/lib/chat/assistant/decisions.js +161 -0
  11. package/lib/chat/marks/toolMarks.js +2 -2
  12. package/lib/config/AgentConfiguration.d.ts +2 -1
  13. package/lib/config/AgentConfiguration.js +2 -2
  14. package/lib/examples/AgentDecideExample.d.ts +24 -0
  15. package/lib/examples/AgentDecideExample.js +73 -0
  16. package/lib/examples/AgentNotificationsExample.js +1 -1
  17. package/lib/examples/AgentSkillsExample.js +1 -1
  18. package/lib/examples/AgentspecsExample.js +3 -3
  19. package/lib/examples/ChatAssistantGalleryExample.d.ts +12 -0
  20. package/lib/examples/ChatAssistantGalleryExample.js +181 -0
  21. package/lib/examples/example-selector.js +2 -0
  22. package/lib/examples/index.d.ts +2 -0
  23. package/lib/examples/index.js +2 -0
  24. package/lib/examples/main.js +5 -0
  25. package/lib/examples/utils/AssistantGalleryGrid.d.ts +53 -0
  26. package/lib/examples/utils/AssistantGalleryGrid.js +123 -0
  27. package/lib/examples/utils/assistantGallery.d.ts +42 -0
  28. package/lib/examples/utils/assistantGallery.js +102 -0
  29. package/lib/examples/utils/testSpriteCharacter.d.ts +9 -0
  30. package/lib/examples/utils/testSpriteCharacter.js +60 -0
  31. package/lib/loop/apps/appspec.js +4 -4
  32. package/lib/loop/apps/checks.js +6 -6
  33. package/lib/loop/apps/rules.js +2 -2
  34. package/lib/loop/plugins/assistant/index.d.ts +14 -2
  35. package/lib/loop/plugins/assistant/index.js +13 -4
  36. package/lib/specs/actions.d.ts +1 -1
  37. package/lib/specs/actions.js +1 -1
  38. package/lib/specs/agents/agents.js +197 -192
  39. package/lib/specs/apps.js +13 -13
  40. package/lib/specs/appspecSchema.js +3 -3
  41. package/lib/specs/backendTools.d.ts +31 -0
  42. package/lib/specs/{tools.js → backendTools.js} +48 -48
  43. package/lib/specs/cogs.js +8 -7
  44. package/lib/specs/frames.js +5 -5
  45. package/lib/specs/index.d.ts +1 -1
  46. package/lib/specs/index.js +1 -1
  47. package/lib/stores/agentRuntimeStore.js +2 -2
  48. package/lib/stories/loop/pictures-main.js +16 -1
  49. package/lib/types/agentspecs.d.ts +7 -5
  50. package/lib/types/config.d.ts +2 -2
  51. package/lib/types/tools.d.ts +3 -3
  52. package/package.json +1 -1
  53. package/scripts/codegen/generate_agents.py +23 -12
  54. package/scripts/codegen/generate_apps.py +3 -2
  55. package/scripts/codegen/{generate_tools.py → generate_backend_tools.py} +29 -29
  56. package/scripts/codegen/generate_cogs.py +2 -2
  57. package/scripts/codegen/generate_frames.py +2 -2
  58. package/lib/specs/tools.d.ts +0 -31
@@ -110,6 +110,16 @@ export interface ChatFloatingProps extends ChatCommonProps {
110
110
  * @default 'paperclip'
111
111
  */
112
112
  assistantCharacter?: string | AssistantCharacter | AssistantCharacterData;
113
+ /**
114
+ * The runtime the floating assistant asks typed decisions at — its
115
+ * `/api/v1/configure/inference/decisions`, as the `decide` tool asks — and
116
+ * the token to ask with, if it needs one: its balloon offers *Ask a
117
+ * decision*, answered there in plain words with its confidence.
118
+ */
119
+ decisions?: {
120
+ serverUrl: string;
121
+ token?: string;
122
+ };
113
123
  /**
114
124
  * Callback when the user switches view mode via the header toggle.
115
125
  * The parent component receives the new ChatViewMode value.
@@ -144,5 +154,5 @@ export interface ChatFloatingProps extends ChatCommonProps {
144
154
  * ChatFloating component
145
155
  * A floating chat window built on ChatBase
146
156
  */
147
- export declare function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode, title, description, position, defaultOpen, width, height, showHeader, showButton, showNewChatButton, showClearButton, showSettingsButton, enableKeyboardShortcuts, toggleShortcut, showPoweredBy, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose, escapeToClose, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip, brandColor, buttonGlow, offset, animationDuration, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick, hideMessagesAfterToolUI, defaultViewMode, onViewModeChange, sidebarMountPoint, showPanelBackdrop, availableModels, showModelSelector, showToolsMenu, showSkillsMenu, showTokenUsage, showContextRing, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching, launchingMessage, assistantCharacter, conversation, }: ChatFloatingProps): React.JSX.Element;
157
+ export declare function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode, title, description, position, defaultOpen, width, height, showHeader, showButton, showNewChatButton, showClearButton, showSettingsButton, enableKeyboardShortcuts, toggleShortcut, showPoweredBy, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose, escapeToClose, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip, brandColor, buttonGlow, offset, animationDuration, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick, hideMessagesAfterToolUI, defaultViewMode, onViewModeChange, sidebarMountPoint, showPanelBackdrop, availableModels, showModelSelector, showToolsMenu, showSkillsMenu, showTokenUsage, showContextRing, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching, launchingMessage, assistantCharacter, conversation, decisions, }: ChatFloatingProps): React.JSX.Element;
148
158
  export default ChatFloating;
@@ -31,6 +31,7 @@ import { ButtonGlow } from './display/ButtonGlow';
31
31
  import { useViewportDrag } from './useViewportDrag';
32
32
  import { disabledChatViewModes, resolveMountPoint } from './viewModes';
33
33
  import { AssistantStage } from './assistant/AssistantStage';
34
+ import { decisionsAskerAt } from './assistant/decisions';
34
35
  import { SpeechBalloon } from './assistant/SpeechBalloon';
35
36
  import { DEFAULT_ASSISTANT_CHARACTER, } from './assistant/characters';
36
37
  import { assistantStateOf, latestSaying, newestIsAnswer, keepAway, keptAway, ASSISTANT_WORDS, } from './assistant/state';
@@ -63,7 +64,7 @@ const clamp = (value, max) => Math.max(8, Math.min(value, Math.max(8, max)));
63
64
  * ChatFloating component
64
65
  * A floating chat window built on ChatBase
65
66
  */
66
- export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode = true, title = 'Chat', description = 'Start a conversation with the AI agent.', position = 'bottom-right', defaultOpen = false, width = 400, height = 550, showHeader = true, showButton = true, showNewChatButton = true, showClearButton = true, showSettingsButton = false, enableKeyboardShortcuts = true, toggleShortcut = '/', showPoweredBy = true, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose = false, escapeToClose = true, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip = 'Chat with AI', brandColor, buttonGlow = true, offset = 20, animationDuration = 200, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, defaultViewMode = 'floating', onViewModeChange, sidebarMountPoint = null, showPanelBackdrop = false, availableModels, showModelSelector = false, showToolsMenu = false, showSkillsMenu = false, showTokenUsage = true, showContextRing = false, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation = false, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching = false, launchingMessage, assistantCharacter = DEFAULT_ASSISTANT_CHARACTER, conversation, }) {
67
+ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode = true, title = 'Chat', description = 'Start a conversation with the AI agent.', position = 'bottom-right', defaultOpen = false, width = 400, height = 550, showHeader = true, showButton = true, showNewChatButton = true, showClearButton = true, showSettingsButton = false, enableKeyboardShortcuts = true, toggleShortcut = '/', showPoweredBy = true, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose = false, escapeToClose = true, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip = 'Chat with AI', brandColor, buttonGlow = true, offset = 20, animationDuration = 200, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, defaultViewMode = 'floating', onViewModeChange, sidebarMountPoint = null, showPanelBackdrop = false, availableModels, showModelSelector = false, showToolsMenu = false, showSkillsMenu = false, showTokenUsage = true, showContextRing = false, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation = false, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching = false, launchingMessage, assistantCharacter = DEFAULT_ASSISTANT_CHARACTER, conversation, decisions, }) {
67
68
  // Store-based state
68
69
  const storeIsOpen = useChatOpen();
69
70
  const storeMessages = useChatMessages();
@@ -194,6 +195,9 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
194
195
  },
195
196
  }
196
197
  : undefined;
198
+ const assistantDecide = useMemo(() => decisions?.serverUrl
199
+ ? decisionsAskerAt(decisions.serverUrl, decisions.token)
200
+ : undefined, [decisions?.serverUrl, decisions?.token]);
197
201
  const assistantBalloon = assistantState === 'paused'
198
202
  ? { text: ASSISTANT_WORDS.paused }
199
203
  : balloonApproval
@@ -835,7 +839,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
835
839
  left: px(stageDrag.position.left),
836
840
  top: px(stageDrag.position.top),
837
841
  }
838
- : getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
842
+ : getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef, decide: assistantDecide })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
839
843
  ...getPositionStyles(),
840
844
  }, children: _jsxs(Box, { sx: {
841
845
  position: 'relative',
@@ -17,6 +17,7 @@
17
17
  import type { JSX, RefObject } from 'react';
18
18
  import { type AssistantCharacter } from './characters';
19
19
  import type { AssistantCharacterData } from './formats/types';
20
+ import type { DecisionAsker } from './decisions';
20
21
  import { type AssistantAway, type AssistantState, type BalloonApproval } from './state';
21
22
  /**
22
23
  * Where the balloon goes so that it stays inside the window (T-23): toward
@@ -86,6 +87,11 @@ export interface AssistantStageProps {
86
87
  * steps aside for, nor is the pointer working there (T-27).
87
88
  */
88
89
  ownRef?: RefObject<HTMLElement | null>;
90
+ /**
91
+ * Asks a typed decision of the runtime: the balloon offers *Ask a
92
+ * decision*, and stays while its form or its answer is on screen.
93
+ */
94
+ decide?: DecisionAsker;
89
95
  }
90
- export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, ownRef, }: AssistantStageProps): JSX.Element;
96
+ export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, ownRef, decide, }: AssistantStageProps): JSX.Element;
91
97
  export default AssistantStage;
@@ -180,7 +180,7 @@ export function useKeepClear(stageRef, own, paused) {
180
180
  }, [stageRef, own, paused]);
181
181
  return obstructed ? 'obstacle' : pointerBusy ? 'pointer' : undefined;
182
182
  }
183
- export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, }) {
183
+ export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, decide, }) {
184
184
  // A shipped one by id, a drawing contributed by a plugin (T-24), or a
185
185
  // character read from a file (T-26).
186
186
  const shipped = typeof character === 'string'
@@ -199,7 +199,9 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
199
199
  // Where the press began: a press that moves is a drag, not a click.
200
200
  const pressedAt = useRef(null);
201
201
  const aside = useKeepClear(stageRef, ownRef, menuOpen);
202
- const showBalloon = !aside && !open && !!balloon && (hovered || insist);
202
+ // A decision being asked, or its answer, keeps the balloon up.
203
+ const [deciding, setDeciding] = useState(false);
204
+ const showBalloon = !aside && !open && !!balloon && (hovered || insist || deciding);
203
205
  return (_jsxs(Box, { ref: stageRef, "data-assistant-state": state, "data-assistant-aside": aside, sx: {
204
206
  position: 'fixed',
205
207
  zIndex: 1002,
@@ -277,7 +279,7 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
277
279
  '@media (prefers-reduced-motion: reduce)': {
278
280
  '& *': { animation: 'none !important' },
279
281
  },
280
- }, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, onOpen: onToggle, above: size + 8, side: balloonSide(place).side, align: balloonSide(place).align, tailAt: size / 2 })), _jsx(Box, { as: "button", type: "button", "aria-label": open ? `Close the conversation with ${name}` : `Talk to ${name}`, "aria-expanded": open, onPointerDown: (event) => {
282
+ }, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, decide: decide, onDecisionActive: setDeciding, wide: deciding, onOpen: onToggle, above: size + 8, side: balloonSide(place).side, align: balloonSide(place).align, tailAt: size / 2 })), _jsx(Box, { as: "button", type: "button", "aria-label": open ? `Close the conversation with ${name}` : `Talk to ${name}`, "aria-expanded": open, onPointerDown: (event) => {
281
283
  pressedAt.current = { x: event.clientX, y: event.clientY };
282
284
  onDragStart(event);
283
285
  }, onClick: (event) => {
@@ -0,0 +1,31 @@
1
+ /**
2
+ * *Ask a decision* in the floating assistant's balloon: a small form — the
3
+ * situation, the question, its type (yes or no; a choice with its options
4
+ * listed; a score with its range) — asked of Jev through the runtime, and
5
+ * the answer said back in the balloon in plain words with its confidence.
6
+ *
7
+ * @module chat/assistant/DecisionAsk
8
+ */
9
+ import type { JSX } from 'react';
10
+ import { type DecisionAsker } from './decisions';
11
+ /** The balloon's words for a decision. */
12
+ export declare const DECISION_WORDS: {
13
+ readonly ask: "Ask a decision";
14
+ readonly asking: "Asking…";
15
+ readonly submit: "Ask";
16
+ readonly cancel: "Cancel";
17
+ readonly again: "Ask another";
18
+ readonly types: {
19
+ noul: string;
20
+ choice: string;
21
+ score: string;
22
+ };
23
+ };
24
+ export interface DecisionAskProps {
25
+ /** Asks the runtime. */
26
+ ask: DecisionAsker;
27
+ /** The form or its answer is on screen: the balloon must stay. */
28
+ onActiveChange?: (active: boolean) => void;
29
+ }
30
+ export declare function DecisionAsk({ ask, onActiveChange, }: DecisionAskProps): JSX.Element;
31
+ export default DecisionAsk;
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Button, FormControl, Select, Text, Textarea, TextInput, } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { DEFAULT_SCORE_RANGE, decisionAskProblem, decisionRequest, decisionResponseWords, } from './decisions';
6
+ /** The balloon's words for a decision. */
7
+ export const DECISION_WORDS = {
8
+ ask: 'Ask a decision',
9
+ asking: 'Asking…',
10
+ submit: 'Ask',
11
+ cancel: 'Cancel',
12
+ again: 'Ask another',
13
+ types: {
14
+ noul: 'Yes or no',
15
+ choice: 'A choice',
16
+ score: 'A score',
17
+ },
18
+ };
19
+ const LINK_SX = {
20
+ mt: 1,
21
+ p: 0,
22
+ border: 0,
23
+ bg: 'transparent',
24
+ color: 'accent.fg',
25
+ textDecoration: 'underline',
26
+ cursor: 'pointer',
27
+ fontSize: 1,
28
+ };
29
+ export function DecisionAsk({ ask, onActiveChange, }) {
30
+ const [open, setOpen] = useState(false);
31
+ const [situation, setSituation] = useState('');
32
+ const [question, setQuestion] = useState('');
33
+ const [name, setName] = useState('');
34
+ const [type, setType] = useState('noul');
35
+ const [options, setOptions] = useState('');
36
+ const [from, setFrom] = useState(String(DEFAULT_SCORE_RANGE.from));
37
+ const [to, setTo] = useState(String(DEFAULT_SCORE_RANGE.to));
38
+ const [problem, setProblem] = useState();
39
+ const [asking, setAsking] = useState(false);
40
+ const [answer, setAnswer] = useState();
41
+ // Gone from the screen — the conversation opened, the balloon closed — it
42
+ // no longer holds the balloon up.
43
+ const activeChange = useRef(onActiveChange);
44
+ activeChange.current = onActiveChange;
45
+ useEffect(() => () => activeChange.current?.(false), []);
46
+ const show = (active) => {
47
+ setOpen(active);
48
+ onActiveChange?.(active);
49
+ };
50
+ const close = () => {
51
+ setAnswer(undefined);
52
+ setProblem(undefined);
53
+ show(false);
54
+ };
55
+ if (!open) {
56
+ return (_jsx(Box, { as: "button", type: "button", "data-balloon-ask-decision": "", onClick: () => show(true), sx: { ...LINK_SX, display: 'block' }, children: DECISION_WORDS.ask }));
57
+ }
58
+ if (answer !== undefined) {
59
+ return (_jsxs(Box, { "data-balloon-decision": "answered", sx: { mt: 2 }, children: [_jsx(Text, { as: "p", "data-decision-answer": "", sx: { m: 0, fontWeight: 'semibold' }, children: answer }), _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { size: "small", onClick: () => setAnswer(undefined), children: DECISION_WORDS.again }), _jsx(Button, { size: "small", variant: "invisible", onClick: close, children: DECISION_WORDS.cancel })] })] }));
60
+ }
61
+ const submit = async () => {
62
+ const asked = {
63
+ situation,
64
+ question,
65
+ type,
66
+ name,
67
+ options: options.split('\n'),
68
+ range: { from: Number(from), to: Number(to) },
69
+ };
70
+ const refused = decisionAskProblem(asked);
71
+ setProblem(refused);
72
+ if (refused) {
73
+ return;
74
+ }
75
+ const request = decisionRequest(asked);
76
+ setAsking(true);
77
+ try {
78
+ setAnswer(decisionResponseWords(request, await ask(request)));
79
+ }
80
+ catch (error) {
81
+ setAnswer(`Nothing was decided: ${error instanceof Error ? error.message : String(error)}`);
82
+ }
83
+ finally {
84
+ setAsking(false);
85
+ }
86
+ };
87
+ return (_jsxs(Box, { as: "form", "data-balloon-decision": "asking", "aria-label": DECISION_WORDS.ask, onSubmit: (event) => {
88
+ event.preventDefault();
89
+ void submit();
90
+ }, onKeyDown: (event) => {
91
+ if (event.key === 'Escape') {
92
+ close();
93
+ }
94
+ }, sx: { mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Situation" }), _jsx(Textarea, { block: true, rows: 3, resize: "vertical", name: "situation", placeholder: "Help! My payouts have been failing for 3 days.", value: situation, onChange: event => setSituation(event.target.value) })] }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Question" }), _jsx(TextInput, { block: true, size: "small", name: "question", placeholder: "Is it urgent?", value: question, onChange: event => setQuestion(event.target.value) })] }), _jsxs(Box, { sx: { display: 'flex', gap: 2 }, children: [_jsxs(FormControl, { sx: { flex: 1 }, children: [_jsx(FormControl.Label, { children: "Type" }), _jsx(Select, { block: true, size: "small", name: "type", value: type, onChange: event => setType(event.target.value), children: Object.keys(DECISION_WORDS.types).map(option => (_jsx(Select.Option, { value: option, children: DECISION_WORDS.types[option] }, option))) })] }), _jsxs(FormControl, { sx: { flex: 1 }, children: [_jsx(FormControl.Label, { children: "Name" }), _jsx(TextInput, { block: true, size: "small", name: "name", placeholder: "Answer", value: name, onChange: event => setName(event.target.value) })] })] }), type === 'choice' && (_jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Options, one per line" }), _jsx(Textarea, { block: true, rows: 3, resize: "vertical", name: "options", placeholder: 'Billing\nTech\nSales', value: options, onChange: event => setOptions(event.target.value) })] })), type === 'score' && (_jsxs(Box, { sx: { display: 'flex', gap: 2 }, children: [_jsxs(FormControl, { sx: { flex: 1 }, children: [_jsx(FormControl.Label, { children: "From" }), _jsx(TextInput, { block: true, size: "small", type: "number", name: "from", value: from, onChange: event => setFrom(event.target.value) })] }), _jsxs(FormControl, { sx: { flex: 1 }, children: [_jsx(FormControl.Label, { children: "To" }), _jsx(TextInput, { block: true, size: "small", type: "number", name: "to", value: to, onChange: event => setTo(event.target.value) })] })] })), problem ? (_jsx(Text, { as: "p", role: "alert", sx: { m: 0, color: 'danger.fg', fontSize: 0 }, children: problem })) : null, _jsxs(Box, { sx: { display: 'flex', gap: 2 }, children: [_jsx(Button, { size: "small", variant: "primary", type: "submit", disabled: asking, children: asking ? DECISION_WORDS.asking : DECISION_WORDS.submit }), _jsx(Button, { size: "small", type: "button", onClick: close, disabled: asking, children: DECISION_WORDS.cancel })] })] }));
95
+ }
96
+ export default DecisionAsk;
@@ -3,12 +3,15 @@
3
3
  * assistant's character, or the floating popup's button while the chat is
4
4
  * closed: the agent's newest words, as the Office Assistant said them
5
5
  * (LOOP T-23), with *Open the conversation* when there is more — and an
6
- * approval it waits on, answered there with *Approve* or *Deny*.
6
+ * approval it waits on, answered there with *Approve* or *Deny* — and, where
7
+ * the runtime can be asked, *Ask a decision*: a typed decision asked of Jev,
8
+ * its answer said back in the balloon.
7
9
  *
8
10
  * @module chat/assistant/SpeechBalloon
9
11
  */
10
12
  import type { JSX } from 'react';
11
13
  import { type BalloonApproval } from './state';
14
+ import type { DecisionAsker } from './decisions';
12
15
  export interface SpeechBalloonProps {
13
16
  text: string;
14
17
  /** There is more than the balloon holds. */
@@ -25,6 +28,12 @@ export interface SpeechBalloonProps {
25
28
  tailAt: number;
26
29
  /** An approval it waits on: what is asked, and the two answers. */
27
30
  approval?: BalloonApproval;
31
+ /** Asks a typed decision of the runtime: offers *Ask a decision*. */
32
+ decide?: DecisionAsker;
33
+ /** The decision's form or answer is on screen: it stays, and is wider. */
34
+ onDecisionActive?: (active: boolean) => void;
35
+ /** Wider, for the decision's form. */
36
+ wide?: boolean;
28
37
  }
29
- export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, }: SpeechBalloonProps): JSX.Element;
38
+ export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, decide, onDecisionActive, wide, }: SpeechBalloonProps): JSX.Element;
30
39
  export default SpeechBalloon;
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button, Text } from '@primer/react';
3
3
  import { Box } from '@datalayer/primer-addons';
4
4
  import { ASSISTANT_WORDS } from './state';
5
- export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, }) {
5
+ import { DecisionAsk } from './DecisionAsk';
6
+ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, decide, onDecisionActive, wide = false, }) {
6
7
  return (_jsxs(Box, { role: "status", "aria-live": "polite", "data-speech-balloon": "", sx: {
7
8
  position: 'absolute',
8
9
  // Pixels, as strings: a number here is read as the theme's space
@@ -11,8 +12,8 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
11
12
  ? { bottom: `${above}px` }
12
13
  : { top: `${above}px` }),
13
14
  [align]: 0,
14
- maxWidth: 280,
15
- width: 'max-content',
15
+ maxWidth: wide ? 300 : 280,
16
+ width: wide ? 300 : 'max-content',
16
17
  px: 3,
17
18
  py: 2,
18
19
  bg: 'canvas.default',
@@ -37,7 +38,7 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
37
38
  borderColor: 'border.default',
38
39
  transform: side === 'above' ? 'rotate(45deg)' : 'rotate(225deg)',
39
40
  },
40
- }, children: [_jsx(Text, { as: "p", sx: { m: 0 }, children: text }), approval && (_jsxs(Box, { "data-balloon-approval": approval.id, sx: { mt: 1 }, children: [_jsx(Text, { as: "p", sx: { m: 0, fontWeight: 'semibold' }, children: approval.asks }), approval.why ? (_jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: approval.why })) : null, _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { size: "small", variant: "primary", disabled: approval.deciding, onClick: approval.onApprove, children: ASSISTANT_WORDS.approve }), _jsx(Button, { size: "small", disabled: approval.deciding, onClick: approval.onDeny, children: ASSISTANT_WORDS.deny })] }), approval.others > 0 ? (_jsx(Text, { as: "p", sx: { m: 0, mt: 1, color: 'fg.muted', fontSize: 0 }, children: `${approval.others} more waiting.` })) : null] })), more && (_jsx(Box, { as: "button", type: "button", onClick: onOpen, sx: {
41
+ }, children: [_jsx(Text, { as: "p", sx: { m: 0 }, children: text }), approval && (_jsxs(Box, { "data-balloon-approval": approval.id, sx: { mt: 1 }, children: [_jsx(Text, { as: "p", sx: { m: 0, fontWeight: 'semibold' }, children: approval.asks }), approval.why ? (_jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: approval.why })) : null, _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { size: "small", variant: "primary", disabled: approval.deciding, onClick: approval.onApprove, children: ASSISTANT_WORDS.approve }), _jsx(Button, { size: "small", disabled: approval.deciding, onClick: approval.onDeny, children: ASSISTANT_WORDS.deny })] }), approval.others > 0 ? (_jsx(Text, { as: "p", sx: { m: 0, mt: 1, color: 'fg.muted', fontSize: 0 }, children: `${approval.others} more waiting.` })) : null] })), decide && _jsx(DecisionAsk, { ask: decide, onActiveChange: onDecisionActive }), more && (_jsx(Box, { as: "button", type: "button", onClick: onOpen, sx: {
41
42
  mt: 1,
42
43
  p: 0,
43
44
  border: 0,
@@ -0,0 +1,104 @@
1
+ /**
2
+ * A decision asked from the floating assistant's balloon: a situation, a
3
+ * question about it and its type — yes or no, a choice among options, a score
4
+ * on a range — asked of Jev, the typed-decision model, through the runtime's
5
+ * `POST /api/v1/configure/inference/decisions`: the route that asks as the
6
+ * `decide` tool does, with the token the runtime calls its models with.
7
+ *
8
+ * The answer comes back in plain words with its confidence: "Urgent: yes
9
+ * (0.87)", "Team: Billing (0.94)", "Score: 4 of 5 (0.5)".
10
+ *
11
+ * @module chat/assistant/decisions
12
+ */
13
+ /** The three types of a question, as ai-inference names them. */
14
+ export type DecisionType = 'noul' | 'choice' | 'score';
15
+ /** What the balloon's form asks. */
16
+ export interface DecisionAsk {
17
+ /** The text the question is about: a ticket, a message, a review. */
18
+ situation: string;
19
+ /** The question: a statement (yes or no) or a question (choice, score). */
20
+ question: string;
21
+ /** Its type. */
22
+ type: DecisionType;
23
+ /** A short name the answer is said under ("Urgent", "Team"). */
24
+ name?: string;
25
+ /** A choice's options, at least two. */
26
+ options?: string[];
27
+ /** A score's range, lowest first: from 1 to 5 by default. */
28
+ range?: {
29
+ from: number;
30
+ to: number;
31
+ };
32
+ }
33
+ /** One question as the runtime's route takes it (`DecisionQuestion`). */
34
+ export interface DecisionQuestionBody {
35
+ name: string;
36
+ type: DecisionType;
37
+ instructions: string;
38
+ options: string[];
39
+ }
40
+ /** The body of `POST /configure/inference/decisions`. */
41
+ export interface DecisionsRequestBody {
42
+ state: string;
43
+ questions: DecisionQuestionBody[];
44
+ }
45
+ /** One typed answer, as ai-inference gives it. */
46
+ export interface DecisionAnswer {
47
+ type?: DecisionType;
48
+ /** noul: the probability that the statement holds. */
49
+ probability?: number;
50
+ /** choice: the option chosen. */
51
+ choice?: string;
52
+ /** score: the step, from 0. */
53
+ score?: number;
54
+ confidence?: number;
55
+ probabilities?: Record<string, number>;
56
+ /** score: each step's label, by its index. */
57
+ legend?: Record<string, string>;
58
+ }
59
+ /** What the route answers: the answers by name, or why nothing was decided. */
60
+ export interface DecisionsResponseBody {
61
+ model?: string;
62
+ answers?: Record<string, DecisionAnswer>;
63
+ refusal?: string;
64
+ }
65
+ /** The balloon's asker: a request in, the route's answer out. */
66
+ export type DecisionAsker = (body: DecisionsRequestBody) => Promise<DecisionsResponseBody>;
67
+ /** The name an answer is said under when the form gave none. */
68
+ export declare const DEFAULT_DECISION_NAME = "Answer";
69
+ /** The default range of a score. */
70
+ export declare const DEFAULT_SCORE_RANGE: {
71
+ readonly from: 1;
72
+ readonly to: 5;
73
+ };
74
+ /** The steps of a score's range, lowest first: `1..5` is `["1", …, "5"]`. */
75
+ export declare function scoreSteps(range: {
76
+ from: number;
77
+ to: number;
78
+ }): string[];
79
+ /**
80
+ * Why the form cannot be asked as it is, in a sentence, or `undefined`.
81
+ */
82
+ export declare function decisionAskProblem(ask: DecisionAsk): string | undefined;
83
+ /** The form's ask as the runtime's route takes it: one named question. */
84
+ export declare function decisionRequest(ask: DecisionAsk): DecisionsRequestBody;
85
+ /** A probability or a confidence as said: two places at most, `0.5`, `0.87`. */
86
+ export declare function confidenceWords(value: number): string;
87
+ /**
88
+ * One answer in plain words with its confidence: "Urgent: yes (0.87)" — a
89
+ * no is said with the probability that it does not hold — "Team: Billing
90
+ * (0.94)", "Score: 4 of 5 (0.5)" — a rubric of words by its step,
91
+ * "Severity: blocking (0.7)".
92
+ */
93
+ export declare function decisionAnswerWords(name: string, answer: DecisionAnswer, options?: readonly string[]): string;
94
+ /**
95
+ * What the route answered, in the balloon's words: the one answer, or the
96
+ * sentence saying why nothing was decided.
97
+ */
98
+ export declare function decisionResponseWords(request: DecisionsRequestBody, response: DecisionsResponseBody): string;
99
+ /**
100
+ * Ask at a runtime: `POST {serverUrl}/api/v1/configure/inference/decisions`,
101
+ * with the person's token when there is one. A refusal of the route itself
102
+ * comes back as a sentence, never a throw.
103
+ */
104
+ export declare function decisionsAskerAt(serverUrl: string, token?: string): DecisionAsker;
@@ -0,0 +1,161 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** The name an answer is said under when the form gave none. */
6
+ export const DEFAULT_DECISION_NAME = 'Answer';
7
+ /** The default range of a score. */
8
+ export const DEFAULT_SCORE_RANGE = { from: 1, to: 5 };
9
+ /** The longest range a score may have, in steps. */
10
+ const MAX_SCORE_STEPS = 11;
11
+ /** The steps of a score's range, lowest first: `1..5` is `["1", …, "5"]`. */
12
+ export function scoreSteps(range) {
13
+ const steps = [];
14
+ for (let step = range.from; step <= range.to; step += 1) {
15
+ steps.push(String(step));
16
+ }
17
+ return steps;
18
+ }
19
+ /**
20
+ * Why the form cannot be asked as it is, in a sentence, or `undefined`.
21
+ */
22
+ export function decisionAskProblem(ask) {
23
+ if (!ask.situation.trim()) {
24
+ return 'Say the situation the question is about.';
25
+ }
26
+ if (!ask.question.trim()) {
27
+ return 'Ask a question.';
28
+ }
29
+ if (ask.type === 'choice') {
30
+ const options = (ask.options ?? []).filter(option => option.trim());
31
+ if (options.length < 2) {
32
+ return 'A choice needs at least two options.';
33
+ }
34
+ if (new Set(options.map(option => option.trim())).size !== options.length) {
35
+ return 'Two options are the same.';
36
+ }
37
+ }
38
+ if (ask.type === 'score') {
39
+ const range = ask.range ?? DEFAULT_SCORE_RANGE;
40
+ if (!Number.isInteger(range.from) || !Number.isInteger(range.to)) {
41
+ return 'A score goes from a whole number to another.';
42
+ }
43
+ if (range.to <= range.from) {
44
+ return 'A score goes from a lower number to a higher one.';
45
+ }
46
+ if (range.to - range.from + 1 > MAX_SCORE_STEPS) {
47
+ return `A score has at most ${MAX_SCORE_STEPS} steps.`;
48
+ }
49
+ }
50
+ return undefined;
51
+ }
52
+ /** The form's ask as the runtime's route takes it: one named question. */
53
+ export function decisionRequest(ask) {
54
+ const options = ask.type === 'choice'
55
+ ? (ask.options ?? []).map(option => option.trim()).filter(Boolean)
56
+ : ask.type === 'score'
57
+ ? scoreSteps(ask.range ?? DEFAULT_SCORE_RANGE)
58
+ : [];
59
+ return {
60
+ state: ask.situation.trim(),
61
+ questions: [
62
+ {
63
+ name: ask.name?.trim() || DEFAULT_DECISION_NAME,
64
+ type: ask.type,
65
+ instructions: ask.question.trim(),
66
+ options,
67
+ },
68
+ ],
69
+ };
70
+ }
71
+ /** A probability or a confidence as said: two places at most, `0.5`, `0.87`. */
72
+ export function confidenceWords(value) {
73
+ return String(Number(value.toFixed(2)));
74
+ }
75
+ /**
76
+ * One answer in plain words with its confidence: "Urgent: yes (0.87)" — a
77
+ * no is said with the probability that it does not hold — "Team: Billing
78
+ * (0.94)", "Score: 4 of 5 (0.5)" — a rubric of words by its step,
79
+ * "Severity: blocking (0.7)".
80
+ */
81
+ export function decisionAnswerWords(name, answer, options = []) {
82
+ if (typeof answer.probability === 'number' && answer.choice === undefined) {
83
+ const holds = answer.probability >= 0.5;
84
+ return `${name}: ${holds ? 'yes' : 'no'} (${confidenceWords(holds ? answer.probability : 1 - answer.probability)})`;
85
+ }
86
+ const confidence = typeof answer.confidence === 'number'
87
+ ? ` (${confidenceWords(answer.confidence)})`
88
+ : '';
89
+ if (typeof answer.score === 'number') {
90
+ const steps = options.length
91
+ ? options
92
+ : Object.keys(answer.legend ?? {})
93
+ .sort((a, b) => Number(a) - Number(b))
94
+ .map(key => answer.legend?.[key] ?? key);
95
+ const said = answer.legend?.[String(answer.score)] ?? steps[answer.score];
96
+ const last = steps[steps.length - 1];
97
+ // A range of numbers says where on it ("4 of 5"); a rubric of words says
98
+ // its step ("blocking").
99
+ const numeric = steps.length > 0 && steps.every(step => /^-?\d+$/.test(step.trim()));
100
+ return `${name}: ${said ?? answer.score + 1}${numeric && last !== undefined ? ` of ${last}` : ''}${confidence}`;
101
+ }
102
+ if (typeof answer.choice === 'string') {
103
+ return `${name}: ${answer.choice}${confidence}`;
104
+ }
105
+ return `${name}: no answer`;
106
+ }
107
+ /**
108
+ * What the route answered, in the balloon's words: the one answer, or the
109
+ * sentence saying why nothing was decided.
110
+ */
111
+ export function decisionResponseWords(request, response) {
112
+ if (response.refusal) {
113
+ return response.refusal;
114
+ }
115
+ const [question] = request.questions;
116
+ const answer = response.answers?.[question.name];
117
+ if (!answer) {
118
+ return 'Nothing was decided: the answer did not come back.';
119
+ }
120
+ return decisionAnswerWords(question.name, answer, question.options);
121
+ }
122
+ /**
123
+ * Ask at a runtime: `POST {serverUrl}/api/v1/configure/inference/decisions`,
124
+ * with the person's token when there is one. A refusal of the route itself
125
+ * comes back as a sentence, never a throw.
126
+ */
127
+ export function decisionsAskerAt(serverUrl, token) {
128
+ const base = serverUrl.replace(/\/+$/, '');
129
+ return async (body) => {
130
+ let response;
131
+ try {
132
+ response = await fetch(`${base}/api/v1/configure/inference/decisions`, {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
137
+ },
138
+ body: JSON.stringify(body),
139
+ });
140
+ }
141
+ catch {
142
+ return {
143
+ refusal: 'Nothing was decided: the runtime could not be reached.',
144
+ };
145
+ }
146
+ if (!response.ok) {
147
+ let detail = '';
148
+ try {
149
+ const said = (await response.json());
150
+ detail = typeof said.detail === 'string' ? said.detail : '';
151
+ }
152
+ catch {
153
+ // No body worth saying.
154
+ }
155
+ return {
156
+ refusal: `Nothing was decided: the runtime refused it (${response.status})${detail ? `: ${detail}` : '.'}`,
157
+ };
158
+ }
159
+ return (await response.json());
160
+ };
161
+ }
@@ -16,7 +16,7 @@
16
16
  import { MCP_SERVER_LIBRARY } from '../../specs/mcpServers';
17
17
  import { SKILLS_CATALOG } from '../../specs/skills';
18
18
  import { FRONTEND_TOOL_CATALOG } from '../../specs/frontendTools';
19
- import { TOOL_CATALOG } from '../../specs/tools';
19
+ import { BACKEND_TOOL_CATALOG } from '../../specs/backendTools';
20
20
  /** The tools the skills area runs a skill through. */
21
21
  export const SKILL_TOOLS = new Set([
22
22
  'run_skill_script',
@@ -57,7 +57,7 @@ function frontendTools() {
57
57
  function runtimeTools() {
58
58
  if (!runtimeIndex) {
59
59
  runtimeIndex = new Map();
60
- for (const spec of Object.values(TOOL_CATALOG)) {
60
+ for (const spec of Object.values(BACKEND_TOOL_CATALOG)) {
61
61
  const method = spec.runtime?.method;
62
62
  if (method && !runtimeIndex.has(method)) {
63
63
  runtimeIndex.set(method, { id: spec.id, ...marksOf(spec) });
@@ -19,7 +19,8 @@ export interface LibraryAgentspec {
19
19
  icon?: string | null;
20
20
  color?: string | null;
21
21
  skills: string[];
22
- tools?: string[];
22
+ /** Backend tools (agentspecs/backend-tools), as the runtime serves them. */
23
+ backendTools?: string[];
23
24
  systemPrompt?: string | null;
24
25
  systemPromptCodemodeAddons?: string | null;
25
26
  suggestions: string[];
@@ -477,10 +477,10 @@ export const AgentConfiguration = ({ agentLibrary, protocol: transport, extensio
477
477
  .map(selection => normalizeMcpServerId(selection.id));
478
478
  }, [selectedMcpServers, catalogServerIdSet]);
479
479
  const resolvedSpecTools = useMemo(() => {
480
- if (!activeSpec || !Array.isArray(activeSpec.tools)) {
480
+ if (!activeSpec || !Array.isArray(activeSpec.backendTools)) {
481
481
  return [];
482
482
  }
483
- return activeSpec.tools
483
+ return activeSpec.backendTools
484
484
  .map(tool => normalizeToolLabel(tool))
485
485
  .filter((tool) => !!tool);
486
486
  }, [activeSpec]);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Decide: typed decisions asked of Jev (`cloudflare:wrk/typesafe/jev`), the
3
+ * typed-decision model, from the chat and from the floating assistant.
4
+ *
5
+ * The *Decide* application of the catalogue (agentspecs `apps/decide.yaml`)
6
+ * in the LOOP workspace, on the Local target: its agent — `example-simple`
7
+ * with the `decide` tool — runs on the local agent-runtimes server, and
8
+ * answers a question about a text by asking a typed decision. The floating
9
+ * assistant beside it offers *Ask a decision* in its balloon: the situation,
10
+ * the question and its type, asked through the same server's
11
+ * `/api/v1/configure/inference/decisions`, the route that asks as `decide`
12
+ * does, with the same token.
13
+ *
14
+ * The server needs ai-inference: start it with
15
+ * `AGENT_RUNTIMES_INFERENCE_PROVIDER_OVERRIDE=datalayer`,
16
+ * `DATALAYER_AI_INFERENCE_URL=https://r1.datalayer.run` and
17
+ * `DATALAYER_AI_INFERENCE_API_KEY` set from `DATALAYER_API_KEY`. Another
18
+ * server than `http://localhost:8765` is named with `?agentRuntimesUrl=`.
19
+ *
20
+ * @module examples/AgentDecideExample
21
+ */
22
+ import React from 'react';
23
+ declare const AgentDecideExample: React.FC;
24
+ export default AgentDecideExample;