@datalayer/agent-runtimes 1.3.59 → 1.3.61

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 +142 -23
  3. package/lib/chat/assistant/AssistantStage.d.ts +13 -5
  4. package/lib/chat/assistant/AssistantStage.js +5 -3
  5. package/lib/chat/assistant/ConversationBalloon.d.ts +71 -0
  6. package/lib/chat/assistant/ConversationBalloon.js +102 -0
  7. package/lib/chat/assistant/DecisionAsk.d.ts +31 -0
  8. package/lib/chat/assistant/DecisionAsk.js +96 -0
  9. package/lib/chat/assistant/SpeechBalloon.d.ts +17 -4
  10. package/lib/chat/assistant/SpeechBalloon.js +20 -14
  11. package/lib/chat/assistant/decisions.d.ts +104 -0
  12. package/lib/chat/assistant/decisions.js +161 -0
  13. package/lib/chat/assistant/state.d.ts +1 -1
  14. package/lib/chat/assistant/state.js +1 -1
  15. package/lib/chat/base/ChatBase.js +22 -4
  16. package/lib/chat/marks/toolMarks.js +2 -2
  17. package/lib/config/AgentConfiguration.d.ts +2 -1
  18. package/lib/config/AgentConfiguration.js +2 -2
  19. package/lib/examples/AgentDecideExample.d.ts +24 -0
  20. package/lib/examples/AgentDecideExample.js +73 -0
  21. package/lib/examples/AgentNotificationsExample.js +1 -1
  22. package/lib/examples/AgentSkillsExample.js +1 -1
  23. package/lib/examples/AgentspecsExample.js +3 -3
  24. package/lib/examples/ChatAssistantExample.js +12 -1
  25. package/lib/examples/example-selector.js +1 -0
  26. package/lib/examples/index.d.ts +1 -0
  27. package/lib/examples/index.js +1 -0
  28. package/lib/examples/main.js +3 -0
  29. package/lib/loop/apps/appspec.js +4 -4
  30. package/lib/loop/apps/checks.js +6 -6
  31. package/lib/loop/apps/rules.js +2 -2
  32. package/lib/loop/plugins/assistant/index.d.ts +14 -2
  33. package/lib/loop/plugins/assistant/index.js +22 -5
  34. package/lib/specs/actions.d.ts +1 -1
  35. package/lib/specs/actions.js +1 -1
  36. package/lib/specs/agents/agents.js +197 -192
  37. package/lib/specs/apps.js +13 -13
  38. package/lib/specs/appspecSchema.js +3 -3
  39. package/lib/specs/backendTools.d.ts +31 -0
  40. package/lib/specs/{tools.js → backendTools.js} +48 -48
  41. package/lib/specs/cogs.js +8 -7
  42. package/lib/specs/frames.js +5 -5
  43. package/lib/specs/index.d.ts +1 -1
  44. package/lib/specs/index.js +1 -1
  45. package/lib/stores/agentRuntimeStore.js +2 -2
  46. package/lib/stories/loop/ReferenceScreens.d.ts +6 -2
  47. package/lib/stories/loop/ReferenceScreens.js +63 -6
  48. package/lib/types/agentspecs.d.ts +7 -5
  49. package/lib/types/chat.d.ts +12 -0
  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
@@ -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;
@@ -1,14 +1,19 @@
1
1
  /**
2
2
  * A speech balloon over something that stands for the agent — the floating
3
3
  * assistant's character, or the floating popup's button while the chat is
4
- * closed: the agent's newest words, as the Office Assistant said them
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*.
4
+ * closed: a peek, one short line — the agent's newest words, as the Office
5
+ * Assistant said them (LOOP T-23), cut to their first words, or a
6
+ * notification — that opens the conversation when clicked, and can be
7
+ * dismissed; an approval it waits on, answered there with *Approve* or
8
+ * *Deny*, and how many more wait; and, where the runtime can be asked and
9
+ * the conversation is elsewhere (the LOOP workspace), *Ask a decision*: a
10
+ * typed decision asked of Jev, its answer said back in the balloon.
7
11
  *
8
12
  * @module chat/assistant/SpeechBalloon
9
13
  */
10
14
  import type { JSX } from 'react';
11
15
  import { type BalloonApproval } from './state';
16
+ import type { DecisionAsker } from './decisions';
12
17
  export interface SpeechBalloonProps {
13
18
  text: string;
14
19
  /** There is more than the balloon holds. */
@@ -25,6 +30,14 @@ export interface SpeechBalloonProps {
25
30
  tailAt: number;
26
31
  /** An approval it waits on: what is asked, and the two answers. */
27
32
  approval?: BalloonApproval;
33
+ /** Asks a typed decision of the runtime: offers *Ask a decision*. */
34
+ decide?: DecisionAsker;
35
+ /** The decision's form or answer is on screen: it stays, and is wider. */
36
+ onDecisionActive?: (active: boolean) => void;
37
+ /** Wider, for the decision's form. */
38
+ wide?: boolean;
39
+ /** Puts the peek away: a × beside its line. Never for an approval. */
40
+ onDismissPeek?: () => void;
28
41
  }
29
- export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, }: SpeechBalloonProps): JSX.Element;
42
+ export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, decide, onDecisionActive, wide, onDismissPeek, }: SpeechBalloonProps): JSX.Element;
30
43
  export default SpeechBalloon;
@@ -1,8 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, Text } from '@primer/react';
2
+ import { Button, IconButton, Text } from '@primer/react';
3
3
  import { Box } from '@datalayer/primer-addons';
4
+ import { XIcon } from '@primer/octicons-react';
4
5
  import { ASSISTANT_WORDS } from './state';
5
- export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, }) {
6
+ import { moreWaiting } from './ConversationBalloon';
7
+ import { DecisionAsk } from './DecisionAsk';
8
+ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, decide, onDecisionActive, wide = false, onDismissPeek, }) {
6
9
  return (_jsxs(Box, { role: "status", "aria-live": "polite", "data-speech-balloon": "", sx: {
7
10
  position: 'absolute',
8
11
  // Pixels, as strings: a number here is read as the theme's space
@@ -11,8 +14,8 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
11
14
  ? { bottom: `${above}px` }
12
15
  : { top: `${above}px` }),
13
16
  [align]: 0,
14
- maxWidth: 280,
15
- width: 'max-content',
17
+ maxWidth: wide ? 300 : 280,
18
+ width: wide ? 300 : 'max-content',
16
19
  px: 3,
17
20
  py: 2,
18
21
  bg: 'canvas.default',
@@ -37,15 +40,18 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
37
40
  borderColor: 'border.default',
38
41
  transform: side === 'above' ? 'rotate(45deg)' : 'rotate(225deg)',
39
42
  },
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
- mt: 1,
42
- p: 0,
43
- border: 0,
44
- bg: 'transparent',
45
- color: 'accent.fg',
46
- textDecoration: 'underline',
47
- cursor: 'pointer',
48
- fontSize: 1,
49
- }, children: "Open the conversation" }))] }));
43
+ }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'flex-start', gap: 1 }, children: [_jsx(Box, { as: "button", type: "button", "data-balloon-peek": "", "data-balloon-more": more ? '' : undefined, onClick: onOpen, sx: {
44
+ flex: 1,
45
+ minWidth: 0,
46
+ m: 0,
47
+ p: 0,
48
+ border: 0,
49
+ bg: 'transparent',
50
+ color: 'inherit',
51
+ font: 'inherit',
52
+ textAlign: 'left',
53
+ cursor: 'pointer',
54
+ '&:hover': { textDecoration: 'underline' },
55
+ }, children: text }), onDismissPeek && !approval ? (_jsx(IconButton, { icon: XIcon, size: "small", variant: "invisible", "aria-label": "Dismiss", "data-balloon-dismiss": "", onClick: onDismissPeek, sx: { mt: '-4px', mr: '-8px', flexShrink: 0 } })) : null] }), 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: moreWaiting(approval.others) })) : null] })), decide && _jsx(DecisionAsk, { ask: decide, onActiveChange: onDecisionActive })] }));
50
56
  }
51
57
  export default SpeechBalloon;
@@ -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
+ }
@@ -71,7 +71,7 @@ export interface AssistantSaying {
71
71
  /** Whether there was more than the balloon holds. */
72
72
  more: boolean;
73
73
  }
74
- /** How much of a message the balloon says before *Open the conversation*. */
74
+ /** How much of a message is kept as the agent's saying before it is cut. */
75
75
  export declare const SAYING_LIMIT = 220;
76
76
  /**
77
77
  * The newest thing the agent said, as the Office Assistant said it: in the
@@ -49,7 +49,7 @@ export const ASSISTANT_WORDS = {
49
49
  /** Its deployment is paused (R-17). */
50
50
  paused: 'Paused — it answers again once it is resumed.',
51
51
  };
52
- /** How much of a message the balloon says before *Open the conversation*. */
52
+ /** How much of a message is kept as the agent's saying before it is cut. */
53
53
  export const SAYING_LIMIT = 220;
54
54
  /**
55
55
  * The newest thing the agent said, as the Office Assistant said it: in the
@@ -463,6 +463,8 @@ function parseApprovedMcpToolsByServer(mcpStatusData) {
463
463
  // ---------------------------------------------------------------------------
464
464
  // ChatBase (outer wrapper — ensures QueryClient is available)
465
465
  // ---------------------------------------------------------------------------
466
+ /** When the welcome is said: it has no time of its own. */
467
+ const WELCOME_AT = new Date(0);
466
468
  /**
467
469
  * ChatBase component — Universal chat panel supporting store, protocol, and custom modes.
468
470
  */
@@ -507,7 +509,7 @@ function ChatBaseInner({ title, subtitle, showHeader = false, showTokenUsage = t
507
509
  // Mode selection
508
510
  useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, onSendReady, onLoadingChange, onContextSnapshot, enableStreaming = false,
509
511
  // Extended props
510
- brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, onItemsChange, onDisplayItemsChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookToolbarExtraItems, ephemeralDocumentToolbarExtraItems, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
512
+ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, onItemsChange, onDisplayItemsChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookToolbarExtraItems, ephemeralDocumentToolbarExtraItems, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
511
513
  // Tool invocation hooks
512
514
  onToolCallStart, onToolCallComplete,
513
515
  // Identity/Authorization props
@@ -3478,7 +3480,23 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3478
3480
  })
3479
3481
  .filter(item => item.message)
3480
3482
  : undefined;
3481
- const messagesContent = children ? (children) : (_jsx(Box, { sx: {
3483
+ /*
3484
+ * The welcome, as the first message: drawn as the agent's own words are,
3485
+ * never sent nor kept. With it, the history is never empty, so the empty
3486
+ * state gives way to it.
3487
+ */
3488
+ const welcomedItems = welcome
3489
+ ? [
3490
+ {
3491
+ id: '__welcome__',
3492
+ role: 'assistant',
3493
+ content: welcome,
3494
+ createdAt: WELCOME_AT,
3495
+ },
3496
+ ...displayItems,
3497
+ ]
3498
+ : displayItems;
3499
+ const messagesContent = children ? (children) : (_jsxs(Box, { sx: {
3482
3500
  display: 'flex',
3483
3501
  flexDirection: 'column',
3484
3502
  minHeight: 0,
@@ -3494,7 +3512,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3494
3512
  maxWidth: 920,
3495
3513
  mx: 'auto',
3496
3514
  bg: 'canvas.default',
3497
- }, children: _jsx(ChatMessageList, { displayItems: displayItems, isLoading: isLoading, isStreaming: isStreaming, showLoadingIndicator: showLoadingIndicator, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: defaultAvatarConfig, padding: padding, renderToolResult: effectiveRenderToolResult, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, agentUsage: agentUsage, onRemoveItems: handleRemoveItems, mcpServers: filteredMcpServers, emptyContent: launching ? null : (_jsx(ChatEmptyState, { emptyState: emptyState, brandIcon: brandIcon, description: resolvedDescription, suggestions: resolvedSuggestions, submitOnSuggestionClick: submitOnSuggestionClick, onSuggestionSubmit: handleSuggestionSubmit, onSuggestionFill: handleSuggestionFill })) }) }));
3515
+ }, children: [_jsx(ChatMessageList, { displayItems: welcomedItems, isLoading: isLoading, isStreaming: isStreaming, showLoadingIndicator: showLoadingIndicator, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: defaultAvatarConfig, padding: padding, renderToolResult: effectiveRenderToolResult, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, agentUsage: agentUsage, onRemoveItems: handleRemoveItems, mcpServers: filteredMcpServers, emptyContent: launching ? null : (_jsx(ChatEmptyState, { emptyState: emptyState, brandIcon: brandIcon, description: resolvedDescription, suggestions: resolvedSuggestions, submitOnSuggestionClick: submitOnSuggestionClick, onSuggestionSubmit: handleSuggestionSubmit, onSuggestionFill: handleSuggestionFill })) }), trailingContent] }));
3498
3516
  const inputToolbar = showInput ? (_jsx(InputPrompt, { input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
3499
3517
  /*
3500
3518
  The same openers the empty state offers, typed into the box they would
@@ -3695,7 +3713,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3695
3713
  top: 8,
3696
3714
  right: 8,
3697
3715
  zIndex: 6,
3698
- }, children: _jsx(IconButton, { icon: SidebarExpandIcon, "aria-label": "Open chat", onClick: onExpandFromCollapsed, variant: "default", size: "small" }) })) : (_jsx(FloatingBrandButton, { isOpen: false, onToggle: onExpandFromCollapsed, position: "bottom-right", tooltip: "Open chat" })))] })) : (_jsxs(_Fragment, { children: [_jsx(Box, { ref: messagesContainerRef, sx: {
3716
+ }, children: _jsx(IconButton, { icon: SidebarExpandIcon, "aria-label": "Open chat", onClick: onExpandFromCollapsed, variant: "default", size: "small" }) })) : (_jsx(FloatingBrandButton, { isOpen: false, onToggle: onExpandFromCollapsed, position: "bottom-right", tooltip: "Open chat" })))] })) : (_jsxs(_Fragment, { children: [_jsx(Box, { ref: messagesContainerRef, "data-chat-history": "", sx: {
3699
3717
  flex: 1,
3700
3718
  flexGrow: 1,
3701
3719
  minHeight: 0,
@@ -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;