@datalayer/agent-runtimes 1.5.42 → 1.5.44

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 (83) hide show
  1. package/dist-embed/chunks/1f30a-DxwAt2Ba.js +1 -0
  2. package/dist-embed/chunks/1f310-AqUM9a8q.js +1 -0
  3. package/dist-embed/chunks/1f324-8MHLOhTj.js +1 -0
  4. package/dist-embed/chunks/1f326-YSBr3aXI.js +1 -0
  5. package/dist-embed/chunks/1f327-Ie-nQBNO.js +1 -0
  6. package/dist-embed/chunks/1f33e-Cfl2WM63.js +1 -0
  7. package/dist-embed/chunks/1f372-3XLDlXkE.js +1 -0
  8. package/dist-embed/chunks/1f373-BPUJt2bp.js +1 -0
  9. package/dist-embed/chunks/1f39b-CSNtbAXr.js +1 -0
  10. package/dist-embed/chunks/1f3ab-BUCNQHz1.js +1 -0
  11. package/dist-embed/chunks/1f41b-Dre1z0gP.js +1 -0
  12. package/dist-embed/chunks/1f441-D8yah4C4.js +1 -0
  13. package/dist-embed/chunks/1f465-CPle6Hjq.js +1 -0
  14. package/dist-embed/chunks/1f4b9-aacFyckt.js +1 -0
  15. package/dist-embed/chunks/1f4be-DPAcAU4R.js +1 -0
  16. package/dist-embed/chunks/1f4c1-Ch6aJ0cj.js +1 -0
  17. package/dist-embed/chunks/1f4c2-Dx7BKmLO.js +1 -0
  18. package/dist-embed/chunks/1f4c4-CNvjvC5k.js +1 -0
  19. package/dist-embed/chunks/1f4d3-GTxRgjv7.js +1 -0
  20. package/dist-embed/chunks/1f4d6-COOZY-Q_.js +1 -0
  21. package/dist-embed/chunks/1f4dc-BhoW4cjD.js +1 -0
  22. package/dist-embed/chunks/1f4e1-DezU8ow-.js +1 -0
  23. package/dist-embed/chunks/1f4e2-DsStZsI-.js +1 -0
  24. package/dist-embed/chunks/1f4e7-BBi7c_zx.js +1 -0
  25. package/dist-embed/chunks/1f500-D9xRo-IT.js +1 -0
  26. package/dist-embed/chunks/1f501-CzKeDPcw.js +1 -0
  27. package/dist-embed/chunks/1f504-BYIHMoDP.js +1 -0
  28. package/dist-embed/chunks/1f52d-2QxCLKhU.js +1 -0
  29. package/dist-embed/chunks/1f552-4d0Y0kn7.js +1 -0
  30. package/dist-embed/chunks/1f58b-Cp8F_HBg.js +1 -0
  31. package/dist-embed/chunks/1f5b2-D6BUULuh.js +1 -0
  32. package/dist-embed/chunks/1f5c3-DjkgJLaz.js +1 -0
  33. package/dist-embed/chunks/1f5d1-C4Kpe4HL.js +1 -0
  34. package/dist-embed/chunks/1f5dc-DyLO7aB9.js +1 -0
  35. package/dist-embed/chunks/1f604-CEekcaYE.js +1 -0
  36. package/dist-embed/chunks/1f917-CyyS9fgf.js +1 -0
  37. package/dist-embed/chunks/1f9d1-200d-2696-D9eSB-0q.js +1 -0
  38. package/dist-embed/chunks/1fa79-DHhIZbWB.js +1 -0
  39. package/dist-embed/chunks/1fa9d-DUI9e77m.js +1 -0
  40. package/dist-embed/chunks/1fa9f-10ALsMaD.js +1 -0
  41. package/dist-embed/chunks/23f3-ASjqFsbT.js +1 -0
  42. package/dist-embed/chunks/2611-BnnmLm-K.js +1 -0
  43. package/dist-embed/chunks/2702-CfLyp35r.js +1 -0
  44. package/dist-embed/chunks/270f-CLwo30mk.js +1 -0
  45. package/dist-embed/chunks/2728-Qg6be-6p.js +1 -0
  46. package/dist-embed/chunks/2b1c-Uc1aRwAf.js +1 -0
  47. package/dist-embed/datalayer-app-main.js +67 -67
  48. package/lib/apps/apps/sessionRules.d.ts +14 -1
  49. package/lib/apps/apps/sessionRules.js +68 -2
  50. package/lib/apps/core/canvasParts.d.ts +7 -1
  51. package/lib/apps/plugins/app-rules/AppRulesCard.js +2 -2
  52. package/lib/apps/plugins/canvas-parts/index.d.ts +9 -1
  53. package/lib/apps/plugins/canvas-parts/index.js +28 -0
  54. package/lib/chat/ChatInline.js +43 -22
  55. package/lib/chat/base/ChatBase.js +43 -2
  56. package/lib/components/agents/AgentSummary.d.ts +2 -24
  57. package/lib/components/agents/AgentSummary.js +3 -19
  58. package/lib/hooks/useChat.js +110 -144
  59. package/lib/portable/activity.d.ts +78 -0
  60. package/lib/portable/activity.js +129 -0
  61. package/lib/portable/fixtures.d.ts +7 -0
  62. package/lib/portable/fixtures.js +50 -0
  63. package/lib/portable/index.d.ts +4 -1
  64. package/lib/portable/index.js +7 -1
  65. package/lib/portable/lifecycle.d.ts +77 -0
  66. package/lib/portable/lifecycle.js +135 -0
  67. package/lib/portable/runtimes.d.ts +68 -0
  68. package/lib/portable/runtimes.js +69 -0
  69. package/lib/voice/VoiceMenu.d.ts +2 -1
  70. package/lib/voice/VoiceMenu.js +2 -2
  71. package/lib/voice/chatVoice.d.ts +5 -2
  72. package/lib/voice/chatVoice.js +10 -4
  73. package/lib/voice/choices.d.ts +2 -0
  74. package/lib/voice/choices.js +6 -0
  75. package/lib/voice/index.d.ts +1 -0
  76. package/lib/voice/index.js +1 -0
  77. package/lib/voice/sayAloud.d.ts +25 -0
  78. package/lib/voice/sayAloud.js +34 -0
  79. package/lib/voice/serverHearing.d.ts +34 -2
  80. package/lib/voice/serverHearing.js +234 -6
  81. package/lib/voice/settings.d.ts +12 -0
  82. package/lib/voice/settings.js +9 -0
  83. package/package.json +1 -1
@@ -61,8 +61,21 @@ export declare const SESSION_RULES_WORDS: {
61
61
  readonly goneWithSession: "In force at once, for this session only; gone when it ends.";
62
62
  readonly unavailable: (sentence: string) => string;
63
63
  };
64
- /** A rule in words: *writing, sending: leave it to me*. */
64
+ /** Where a file rule holds, in words (SHIELD decision 5). */
65
+ export declare const WHERE_WORDS: Record<'inside' | 'outside', string>;
66
+ /**
67
+ * A rule in words: *writing, sending: leave it to me*; with decision 5's file
68
+ * and shell wording when it says where or what (SHIELD SH-43) — *writing
69
+ * inside the workspace (src/**): do it*, *running `git push`: ask me first*.
70
+ */
65
71
  export declare function sessionRuleInWords(rule: SessionRule): string;
72
+ /**
73
+ * Decision 5's file and shell rules, offered on a running session in the
74
+ * Shield's own words: each one a rule a person adds as it stands, and the
75
+ * runtime refuses one that would loosen what the organization says
76
+ * (SHIELD §6.2, §6.3).
77
+ */
78
+ export declare const FILE_AND_SHELL_RULES: readonly SessionRule[];
66
79
  /** What the person typed, as the rule the runtime takes; a sentence when it cannot be one. */
67
80
  export declare function sessionRuleOf(input: {
68
81
  name: string;
@@ -54,12 +54,78 @@ export const SESSION_RULES_WORDS = {
54
54
  goneWithSession: 'In force at once, for this session only; gone when it ends.',
55
55
  unavailable: (sentence) => `Nothing is decided right now: ${sentence}`,
56
56
  };
57
- /** A rule in words: *writing, sending: leave it to me*. */
57
+ /** Where a file rule holds, in words (SHIELD decision 5). */
58
+ export const WHERE_WORDS = {
59
+ inside: 'inside the workspace',
60
+ outside: 'outside the workspace',
61
+ };
62
+ /**
63
+ * A rule in words: *writing, sending: leave it to me*; with decision 5's file
64
+ * and shell wording when it says where or what (SHIELD SH-43) — *writing
65
+ * inside the workspace (src/**): do it*, *running `git push`: ask me first*.
66
+ */
58
67
  export function sessionRuleInWords(rule) {
59
68
  const classes = (rule.classes ?? []).map(item => SESSION_RULES_WORDS.classes[item] ?? item);
60
- const covers = classes.length > 0 ? classes.join(', ') : 'everything';
69
+ let covers = classes.length > 0 ? classes.join(', ') : 'everything';
70
+ if (rule.where === 'inside' || rule.where === 'outside') {
71
+ covers = `${covers} ${WHERE_WORDS[rule.where]}`;
72
+ }
73
+ if (rule.paths && rule.paths.length > 0) {
74
+ covers = `${covers} (${rule.paths.join(', ')})`;
75
+ }
76
+ if (rule.commands && rule.commands.length > 0) {
77
+ covers = `${covers} matching ${rule.commands.map(command => `\`${command}\``).join(' or ')}`;
78
+ }
61
79
  return `${covers}: ${SESSION_RULES_WORDS.decisions[rule.decision]}`;
62
80
  }
81
+ /**
82
+ * Decision 5's file and shell rules, offered on a running session in the
83
+ * Shield's own words: each one a rule a person adds as it stands, and the
84
+ * runtime refuses one that would loosen what the organization says
85
+ * (SHIELD §6.2, §6.3).
86
+ */
87
+ export const FILE_AND_SHELL_RULES = [
88
+ {
89
+ name: 'Edit inside the workspace',
90
+ decision: 'allow',
91
+ classes: ['write'],
92
+ where: 'inside',
93
+ },
94
+ {
95
+ name: 'Ask before deleting a file',
96
+ decision: 'ask',
97
+ classes: ['delete'],
98
+ },
99
+ {
100
+ name: 'Ask before running a command',
101
+ decision: 'ask',
102
+ classes: ['execute'],
103
+ },
104
+ {
105
+ name: 'Leave files outside the workspace to me',
106
+ decision: 'deny',
107
+ classes: ['read', 'write', 'delete'],
108
+ where: 'outside',
109
+ },
110
+ {
111
+ name: 'Ask before git push',
112
+ decision: 'ask',
113
+ classes: ['execute'],
114
+ commands: ['^git\\s+push\\b'],
115
+ },
116
+ {
117
+ name: 'Ask before installing a package',
118
+ decision: 'ask',
119
+ classes: ['execute'],
120
+ commands: ['\\b(pip|npm|uv|yarn|apt(-get)?)\\s+(install|add)\\b'],
121
+ },
122
+ {
123
+ name: 'Leave sudo to me',
124
+ decision: 'deny',
125
+ classes: ['execute'],
126
+ commands: ['^sudo\\b'],
127
+ },
128
+ ];
63
129
  /** What the person typed, as the rule the runtime takes; a sentence when it cannot be one. */
64
130
  export function sessionRuleOf(input) {
65
131
  const name = input.name.trim();
@@ -1,5 +1,5 @@
1
1
  /** The kinds of part an agent is built from on the Canvas. */
2
- export type CanvasPartKind = 'connection' | 'rule' | 'skill' | 'test' | 'event' | 'model' | 'protocol' | 'ui' | 'character';
2
+ export type CanvasPartKind = 'agent' | 'connection' | 'rule' | 'skill' | 'test' | 'event' | 'model' | 'protocol' | 'ui' | 'character';
3
3
  /**
4
4
  * A part of an agent the Canvas may place (LOOP C-20): a connection to a
5
5
  * server of the catalogue, a skill, a model, a rule, a test, an event, a
@@ -18,6 +18,12 @@ export type CanvasPartContribution = {
18
18
  /** One line of what it is. */
19
19
  says: string;
20
20
  emoji?: string;
21
+ /**
22
+ * The harness it runs on, for an agent part (SHIELD SH-43): the catalogue
23
+ * harness's id, whose terms, credential and what it honours the Properties
24
+ * panel reads from `specs/harnesses`.
25
+ */
26
+ harness?: string;
21
27
  /**
22
28
  * What it arrives with, spelled as the Appspec's text spells it: a
23
29
  * connection's, a rule's, a test's or an event's object, a skill's, a
@@ -5,7 +5,7 @@ import { Box } from '@datalayer/primer-addons';
5
5
  import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
6
6
  import { agentServerOf } from '../../core';
7
7
  import { BEHAVIOUR_WORDS, coverOf } from '../../apps/rules';
8
- import { SESSION_RULES_WORDS, SESSION_RULE_CLASSES, SESSION_RULE_DECISIONS, addSessionRule, readSessionRules, removeSessionRule, sessionRuleInWords, sessionRuleOf, } from '../../apps/sessionRules';
8
+ import { FILE_AND_SHELL_RULES, SESSION_RULES_WORDS, SESSION_RULE_CLASSES, SESSION_RULE_DECISIONS, addSessionRule, readSessionRules, removeSessionRule, sessionRuleInWords, sessionRuleOf, } from '../../apps/sessionRules';
9
9
  import { SAVE_WORDS, draftOfApproval } from '../../apps/saved';
10
10
  import { useApproveToolRequest, useRejectToolRequest, useToolApprovalsQuery, } from '../../../hooks/useToolApprovals';
11
11
  /** What the card says. */
@@ -105,7 +105,7 @@ export function SessionRules({ agentServerUrl, sessionUid, token, }) {
105
105
  }
106
106
  void act(() => addSessionRule(agentServerUrl, token, sessionUid, rule)).then(() => setName(''));
107
107
  };
108
- return (_jsxs(Box, { "data-testid": "session-rules", mt: 3, children: [_jsx(Heading, { as: "h4", sx: { fontSize: 1, mb: 1 }, children: SESSION_RULES_WORDS.title }), inForce?.unavailable ? (_jsx(Text, { as: "p", role: "alert", sx: { fontSize: 0, m: 0, mb: 1 }, children: SESSION_RULES_WORDS.unavailable(inForce.unavailable) })) : null, inForce && inForce.session_rules.length === 0 ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: SESSION_RULES_WORDS.none })) : (_jsx(Box, { as: "ul", listStyle: "none", p: 0, m: 0, children: (inForce?.session_rules ?? []).map(rule => (_jsxs(Box, { as: "li", py: 1, fontSize: 1, display: "flex", gap: 2, alignItems: "center", children: [_jsx(Text, { sx: { fontWeight: 'semibold' }, children: rule.name }), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 0 }, children: sessionRuleInWords(rule) }), _jsx(Button, { size: "small", variant: "invisible", disabled: busy, onClick: () => void act(() => removeSessionRule(agentServerUrl, token, sessionUid, rule.name)), children: SESSION_RULES_WORDS.remove })] }, rule.name))) })), _jsxs(Box, { display: "flex", gap: 1, mt: 1, flexWrap: "wrap", alignItems: "center", children: [_jsx(TextInput, { size: "small", value: name, placeholder: SESSION_RULES_WORDS.namePlaceholder, "aria-label": "The rule's name", onChange: event => setName(event.target.value) }), _jsx(Select, { size: "small", value: kind, "aria-label": "What it covers", onChange: event => setKind(event.target.value), children: SESSION_RULE_CLASSES.map(item => (_jsx(Select.Option, { value: item, children: SESSION_RULES_WORDS.classes[item] }, item))) }), _jsx(Select, { size: "small", value: decision, "aria-label": "What it says", onChange: event => setDecision(event.target.value), children: SESSION_RULE_DECISIONS.map(item => (_jsx(Select.Option, { value: item, children: SESSION_RULES_WORDS.decisions[item] }, item))) }), _jsx(Button, { size: "small", disabled: busy, onClick: add, children: SESSION_RULES_WORDS.add })] }), problem ? (_jsx(Text, { as: "p", role: "alert", sx: { fontSize: 0, m: 0, mt: 1 }, children: problem })) : null, _jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', mt: 1, mb: 0 }, children: SESSION_RULES_WORDS.goneWithSession })] }));
108
+ return (_jsxs(Box, { "data-testid": "session-rules", mt: 3, children: [_jsx(Heading, { as: "h4", sx: { fontSize: 1, mb: 1 }, children: SESSION_RULES_WORDS.title }), inForce?.unavailable ? (_jsx(Text, { as: "p", role: "alert", sx: { fontSize: 0, m: 0, mb: 1 }, children: SESSION_RULES_WORDS.unavailable(inForce.unavailable) })) : null, inForce && inForce.session_rules.length === 0 ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: SESSION_RULES_WORDS.none })) : (_jsx(Box, { as: "ul", listStyle: "none", p: 0, m: 0, children: (inForce?.session_rules ?? []).map(rule => (_jsxs(Box, { as: "li", py: 1, fontSize: 1, display: "flex", gap: 2, alignItems: "center", children: [_jsx(Text, { sx: { fontWeight: 'semibold' }, children: rule.name }), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 0 }, children: sessionRuleInWords(rule) }), _jsx(Button, { size: "small", variant: "invisible", disabled: busy, onClick: () => void act(() => removeSessionRule(agentServerUrl, token, sessionUid, rule.name)), children: SESSION_RULES_WORDS.remove })] }, rule.name))) })), _jsxs(Box, { display: "flex", gap: 1, mt: 1, flexWrap: "wrap", alignItems: "center", children: [_jsx(TextInput, { size: "small", value: name, placeholder: SESSION_RULES_WORDS.namePlaceholder, "aria-label": "The rule's name", onChange: event => setName(event.target.value) }), _jsx(Select, { size: "small", value: kind, "aria-label": "What it covers", onChange: event => setKind(event.target.value), children: SESSION_RULE_CLASSES.map(item => (_jsx(Select.Option, { value: item, children: SESSION_RULES_WORDS.classes[item] }, item))) }), _jsx(Select, { size: "small", value: decision, "aria-label": "What it says", onChange: event => setDecision(event.target.value), children: SESSION_RULE_DECISIONS.map(item => (_jsx(Select.Option, { value: item, children: SESSION_RULES_WORDS.decisions[item] }, item))) }), _jsx(Button, { size: "small", disabled: busy, onClick: add, children: SESSION_RULES_WORDS.add })] }), _jsx(Box, { display: "flex", gap: 1, mt: 1, flexWrap: "wrap", "data-testid": "session-rules-file-and-shell", children: FILE_AND_SHELL_RULES.map(rule => (_jsx(Button, { size: "small", variant: "invisible", disabled: busy, title: sessionRuleInWords(rule), onClick: () => void act(() => addSessionRule(agentServerUrl, token, sessionUid, rule)), children: rule.name }, rule.name))) }), problem ? (_jsx(Text, { as: "p", role: "alert", sx: { fontSize: 0, m: 0, mt: 1 }, children: problem })) : null, _jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', mt: 1, mb: 0 }, children: SESSION_RULES_WORDS.goneWithSession })] }));
109
109
  }
110
110
  export function AppRulesCard({ app, workspace, }) {
111
111
  const token = useIAMStore(state => state.token) ?? '';
@@ -15,9 +15,17 @@ type PartsReader = {
15
15
  }>;
16
16
  };
17
17
  /** The short ids an organization turns off, and the plugin each names. */
18
- export declare const CANVAS_PART_SOURCES: readonly ["connections", "skills", "models", "rules", "tests", "events", "protocols", "ui", "characters"];
18
+ export declare const CANVAS_PART_SOURCES: readonly ["agents", "connections", "skills", "models", "rules", "tests", "events", "protocols", "ui", "characters"];
19
19
  export type CanvasPartSource = (typeof CANVAS_PART_SOURCES)[number];
20
20
  export declare const canvasPartsPluginName: (source: CanvasPartSource) => string;
21
+ /**
22
+ * The catalogue's harnesses as the agent (SHIELD SH-43): each harness an
23
+ * agent program over ACP behind the Shield, offered by the catalogue agent
24
+ * that runs on it — dropped on the surface, it is the application's agent.
25
+ * A harness no agent of the catalogue runs yet is offered off, and says so;
26
+ * `custom` is an organization's own and never offered here.
27
+ */
28
+ export declare const AGENT_PARTS: CanvasPartContribution[];
21
29
  /** A connection to each server of the catalogue: read-only, as its owner, every tool. */
22
30
  export declare const CONNECTION_PARTS: CanvasPartContribution[];
23
31
  /** Each skill of the catalogue. */
@@ -13,6 +13,8 @@
13
13
  */
14
14
  import { buildReactorFromPlugins, contribution, definePlugin, } from '@datalayer/reactor';
15
15
  import { MCP_SERVER_LIBRARY } from '../../../specs/mcpServers';
16
+ import { listAgentspecs } from '../../../specs/agents';
17
+ import { listHarnesses } from '../../../specs/harnesses';
16
18
  import { listChatModels } from '../../../specs/models';
17
19
  import { getSkillSpecs } from '../../../specs/skills';
18
20
  import { listProtocolSpecs } from '../../../specs/protocols';
@@ -21,6 +23,7 @@ import { ASSISTANT_CHARACTERS } from '../../../chat/assistant/characters';
21
23
  import { LoopCanvasPart, } from '../../core/canvasParts';
22
24
  /** The short ids an organization turns off, and the plugin each names. */
23
25
  export const CANVAS_PART_SOURCES = [
26
+ 'agents',
24
27
  'connections',
25
28
  'skills',
26
29
  'models',
@@ -39,6 +42,30 @@ const partsPlugin = (source, displayName, parts) => definePlugin({
39
42
  octicon: 'package',
40
43
  contributes: parts.map(part => contribution(LoopCanvasPart, part, { id: part.id })),
41
44
  });
45
+ /**
46
+ * The catalogue's harnesses as the agent (SHIELD SH-43): each harness an
47
+ * agent program over ACP behind the Shield, offered by the catalogue agent
48
+ * that runs on it — dropped on the surface, it is the application's agent.
49
+ * A harness no agent of the catalogue runs yet is offered off, and says so;
50
+ * `custom` is an organization's own and never offered here.
51
+ */
52
+ export const AGENT_PARTS = listHarnesses()
53
+ .filter(harness => harness.id !== 'custom')
54
+ .map(harness => {
55
+ const agent = listAgentspecs().find(spec => spec.harness === harness.id);
56
+ return {
57
+ id: `agent:${harness.id}`,
58
+ kind: 'agent',
59
+ label: harness.name,
60
+ says: agent
61
+ ? `${agent.name}: ${harness.description}`
62
+ : `${harness.description} No agent of the catalogue runs on it yet.`,
63
+ emoji: '🛡️',
64
+ harness: harness.id,
65
+ example: agent?.id ?? '',
66
+ enabled: Boolean(agent) && harness.enabled,
67
+ };
68
+ });
42
69
  /** A connection to each server of the catalogue: read-only, as its owner, every tool. */
43
70
  export const CONNECTION_PARTS = Object.values(MCP_SERVER_LIBRARY).map(server => ({
44
71
  id: `connection:${server.id}`,
@@ -210,6 +237,7 @@ export const CHARACTER_PARTS = ASSISTANT_CHARACTERS.map(character => ({
210
237
  enabled: true,
211
238
  }));
212
239
  export const CANVAS_PART_PLUGINS = [
240
+ partsPlugin('agents', 'Agents', AGENT_PARTS),
213
241
  partsPlugin('connections', 'Connections', CONNECTION_PARTS),
214
242
  partsPlugin('skills', 'Skills', SKILL_PARTS),
215
243
  partsPlugin('models', 'Models', MODEL_PARTS),
@@ -6,7 +6,18 @@ import { CopyIcon, ArrowLeftIcon, SyncIcon, TrashIcon, CheckIcon, PlusIcon, Penc
6
6
  import { SparklesIcon } from '@datalayer/icons-react';
7
7
  import { AGUIAdapter } from '../protocols';
8
8
  import { generateMessageId } from '../types/messages';
9
- import { sanitizeAssistantContent } from '../utils';
9
+ import { BROWSER_LIMITS, emptyConversation, reduceConversation, } from '../portable/conversation';
10
+ import { conversationEventsOf } from '../portable/adapterEvents';
11
+ /** The answer so far: the newest assistant message of the conversation. */
12
+ function answerOf(state) {
13
+ for (let index = state.items.length - 1; index >= 0; index -= 1) {
14
+ const item = state.items[index];
15
+ if (item.kind === 'message' && item.role === 'assistant') {
16
+ return item.text;
17
+ }
18
+ }
19
+ return '';
20
+ }
10
21
  /**
11
22
  * Pre-defined AI action groups
12
23
  */
@@ -79,8 +90,15 @@ export function ChatInline({ selectedText, protocol, onReplaceSelection, onInser
79
90
  const [input, setInput] = useState('');
80
91
  // AI state
81
92
  const [aiState, setAiState] = useState('initial');
82
- // AI response content
83
- const [aiResponse, setAiResponse] = useState('');
93
+ // The one-shot conversation (MOBILE M-030): `reduceConversation`'s, as in
94
+ // ChatBase and on the phone; the response is its newest assistant message.
95
+ const [conversation, setConversation] = useState(() => emptyConversation(BROWSER_LIMITS));
96
+ const conversationRef = useRef(conversation);
97
+ const changeConversation = useCallback((next) => {
98
+ conversationRef.current = next;
99
+ setConversation(next);
100
+ }, []);
101
+ const aiResponse = useMemo(() => answerOf(conversation), [conversation]);
84
102
  // Previous prompt for regeneration
85
103
  const [previousPrompt, setPreviousPrompt] = useState('');
86
104
  // Current page in options menu (for sub-menus like translate)
@@ -112,20 +130,23 @@ export function ChatInline({ selectedText, protocol, onReplaceSelection, onInser
112
130
  adapterRef.current = adapter;
113
131
  // Subscribe to events
114
132
  unsubscribeRef.current = adapter.subscribe((event) => {
115
- switch (event.type) {
116
- case 'message':
117
- if (event.message?.content) {
118
- const content = typeof event.message.content === 'string'
119
- ? sanitizeAssistantContent(event.message.content)
120
- : '';
121
- setAiResponse(content);
122
- }
123
- break;
124
- case 'error':
125
- console.error('[ChatInline] Protocol error:', event.error);
126
- setError(event.error?.message || 'Unknown error');
127
- setAiState('initial');
128
- break;
133
+ if (event.type === 'error') {
134
+ console.error('[ChatInline] Protocol error:', event.error);
135
+ setError(event.error?.message || 'Unknown error');
136
+ setAiState('initial');
137
+ return;
138
+ }
139
+ // The event, as the conversation reads it: one reducer whatever the protocol.
140
+ try {
141
+ const inputs = conversationEventsOf(event, conversationRef.current);
142
+ if (inputs.length) {
143
+ changeConversation(inputs.reduce((kept, input) => reduceConversation(kept, input), conversationRef.current));
144
+ }
145
+ }
146
+ catch (failure) {
147
+ console.error('[ChatInline] Protocol event not read:', failure);
148
+ setError(failure instanceof Error ? failure.message : String(failure));
149
+ setAiState('initial');
129
150
  }
130
151
  });
131
152
  // Connect
@@ -134,7 +155,7 @@ export function ChatInline({ selectedText, protocol, onReplaceSelection, onInser
134
155
  unsubscribeRef.current?.();
135
156
  adapterRef.current?.disconnect();
136
157
  };
137
- }, [protocol]);
158
+ }, [protocol, changeConversation]);
138
159
  // Submit prompt to AI
139
160
  const submitPrompt = useCallback(async (prompt) => {
140
161
  if (!adapterRef.current) {
@@ -145,7 +166,7 @@ export function ChatInline({ selectedText, protocol, onReplaceSelection, onInser
145
166
  setInput('');
146
167
  setPreviousPrompt(prompt);
147
168
  setError(null);
148
- setAiResponse('');
169
+ changeConversation(emptyConversation(BROWSER_LIMITS));
149
170
  // Build system message with selected text context
150
171
  const systemContext = `Do not surround your answer in quote marks. Only return the answer, nothing else. The user is selecting this text:
151
172
 
@@ -178,7 +199,7 @@ ${selectedText || ''}
178
199
  setError(err.message);
179
200
  setAiState('initial');
180
201
  }
181
- }, [selectedText]);
202
+ }, [selectedText, changeConversation]);
182
203
  // Handle pending prompt from toolbar dropdown
183
204
  useEffect(() => {
184
205
  if (pendingPrompt && aiState === 'initial') {
@@ -235,8 +256,8 @@ ${selectedText || ''}
235
256
  // Handle back to options
236
257
  const handleBackToOptions = useCallback(() => {
237
258
  setAiState('initial');
238
- setAiResponse('');
239
- }, []);
259
+ changeConversation(emptyConversation(BROWSER_LIMITS));
260
+ }, [changeConversation]);
240
261
  // Handle discard
241
262
  const handleDiscard = useCallback(() => {
242
263
  onClose?.();
@@ -25,7 +25,7 @@ import { SkeletonText } from '@primer/react/experimental';
25
25
  import { SidebarExpandIcon } from '@primer/octicons-react';
26
26
  import { notebookStore, JupyterReactTheme } from '@datalayer/jupyter-react';
27
27
  import { Box, setupPrimerPortals, useThemeStore, getColorPalette, getThemeConfig, useSystemColorMode, } from '@datalayer/primer-addons';
28
- import { AlertIcon, PersonIcon } from '@primer/octicons-react';
28
+ import { AlertIcon, PersonIcon, UnmuteIcon } from '@primer/octicons-react';
29
29
  import { AiAgentIcon } from '@datalayer/icons-react';
30
30
  import { QueryClientProvider, QueryClientContext } from '@tanstack/react-query';
31
31
  import { useCoreStore } from '@datalayer/core';
@@ -57,6 +57,7 @@ import { settledVoice } from '../../voice/chatVoice';
57
57
  import { useVoiceSettings } from '../../voice/useVoiceSettings';
58
58
  import { useVoiceSpeaker } from '../../voice/useVoiceSpeaker';
59
59
  import { useSpokenAnswers } from '../../voice/useSpokenAnswers';
60
+ import { lineToSay } from '../../voice/sayAloud';
60
61
  import { isFloatingChatViewMode } from '../viewModes';
61
62
  import { ToolApprovalBanner, ToolApprovalDialog, } from '../tools';
62
63
  import { initialModelId, isOffered, offeredModels as offeredModelsFor, } from './modelChoice';
@@ -1819,6 +1820,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
1819
1820
  recorderRef.current = null;
1820
1821
  }
1821
1822
  const inspectAnswerRef = useRef('');
1823
+ // A line the agent says aloud, from a run's `voice.say` (VOICE.md VO-35).
1824
+ const voiceSayRef = useRef(undefined);
1822
1825
  const handleRespondRef = useRef(null);
1823
1826
  // A decision made outside the chat — a host page's own approval banner —
1824
1827
  // is answered exactly as the tool card's buttons answer it: through the
@@ -2801,6 +2804,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2801
2804
  // What the server says besides the conversation's text comes on
2802
2805
  // the ui socket (REACTOR_UI RU-W); the run's own — `loop.session`
2803
2806
  // among them — are the conversation's.
2807
+ if (event.activity) {
2808
+ voiceSayRef.current?.(event.activity);
2809
+ }
2804
2810
  read();
2805
2811
  break;
2806
2812
  case 'done':
@@ -2968,6 +2974,31 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2968
2974
  const ownVoice = useVoiceSpeaker(settled, !hostSpeaks);
2969
2975
  useSpokenAnswers(displayItems, isLoading, !hostSpeaks && settled?.output === 'always', ownVoice.speaker);
2970
2976
  const speakingNow = voiceSpeaking || ownVoice.state.speaking;
2977
+ /*
2978
+ * A line the agent wants heard, not written (VO-35): a caption while the
2979
+ * run goes on, said by the speaker where answers are said by themselves.
2980
+ */
2981
+ const [voiceSaid, setVoiceSaid] = useState('');
2982
+ voiceSayRef.current = activity => {
2983
+ const line = lineToSay(activity);
2984
+ if (!line) {
2985
+ return;
2986
+ }
2987
+ setVoiceSaid(line);
2988
+ if (settled?.output === 'always') {
2989
+ if (onReadAloud) {
2990
+ onReadAloud(line);
2991
+ }
2992
+ else {
2993
+ ownVoice.speaker?.say(line);
2994
+ }
2995
+ }
2996
+ };
2997
+ useEffect(() => {
2998
+ if (!isLoading) {
2999
+ setVoiceSaid('');
3000
+ }
3001
+ }, [isLoading]);
2971
3002
  // Answers spoken over, hands-free (VOICE.md VO-13): marked *interrupted*.
2972
3003
  const [interruptedIds, setInterruptedIds] = useState(() => new Set());
2973
3004
  const stopSpeaking = onStopSpeaking ?? (ownVoice.speaker ? ownVoice.stop : undefined);
@@ -3762,7 +3793,17 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3762
3793
  }, send: (text, metadata) => void handleSend(text, undefined, metadata) })) : undefined;
3763
3794
  // Why an answer could not be heard, in a sentence, by the composer.
3764
3795
  const voiceRefused = !hostSpeaks && ownVoice.state.refused ? (_jsx(Text, { role: "alert", sx: { fontSize: 0, color: 'danger.fg', maxWidth: 260 }, "data-voice-refused": "", children: ownVoice.state.refused })) : null;
3765
- const inputToolbar = showInput ? (_jsx(InputPrompt, { footerExtras: voiceRefused ? (_jsxs(_Fragment, { children: [voiceRefused, voiceControl] })) : (voiceControl), interim: voiceInterim, input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
3796
+ // The line said aloud, written as it is said: captions always (VO-65).
3797
+ const voiceSayCaption = voiceSaid ? (_jsxs(Text, { "aria-live": "polite", sx: {
3798
+ fontSize: 0,
3799
+ fontStyle: 'italic',
3800
+ color: 'fg.muted',
3801
+ maxWidth: 260,
3802
+ display: 'inline-flex',
3803
+ alignItems: 'center',
3804
+ gap: 1,
3805
+ }, "data-voice-say": "", children: [_jsx(UnmuteIcon, { size: 12, "aria-label": "Said aloud" }), voiceSaid] })) : null;
3806
+ const inputToolbar = showInput ? (_jsx(InputPrompt, { footerExtras: voiceRefused || voiceSayCaption ? (_jsxs(_Fragment, { children: [voiceSayCaption, voiceRefused, voiceControl] })) : (voiceControl), interim: voiceInterim, input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
3766
3807
  /*
3767
3808
  The same openers the empty state offers, typed into the box they would
3768
3809
  be typed into.
@@ -1,28 +1,6 @@
1
1
  import React from 'react';
2
- export interface AgentSummaryData {
3
- agentName: string;
4
- location: string;
5
- specId?: string;
6
- /**
7
- * The framework turning this agent's loop, as its spec declares it —
8
- * `pydantic-ai` server-side, `vercel-ai` in the page.
9
- */
10
- harness?: string;
11
- /**
12
- * The runtime variant the host chose: `<location>-<harness>`.
13
- *
14
- * Shown next to the harness rather than instead of it, because they answer
15
- * different questions and can disagree: the harness is what the spec asked
16
- * for, the variant is what it actually got.
17
- */
18
- variant?: string;
19
- status?: string;
20
- baseUrl?: string;
21
- sandboxBaseUrl?: string;
22
- agentId?: string;
23
- isReady?: boolean;
24
- error?: string;
25
- }
2
+ import { type AgentSummaryData } from '../../portable/runtimes';
3
+ export type { AgentSummaryData } from '../../portable/runtimes';
26
4
  export interface AgentSummaryProps {
27
5
  summary: AgentSummaryData | null;
28
6
  title?: string;
@@ -7,19 +7,7 @@ import { useEffect, useRef, useState } from 'react';
7
7
  import { Spinner } from '@primer/react';
8
8
  import { Box } from '@datalayer/primer-addons';
9
9
  import { strip } from '@datalayer/core/lib/utils';
10
- /**
11
- * Statuses that represent an agent actively being created/started. The spinner
12
- * only shows for these; app-navigation states ("selected", "switching") and
13
- * settled states ("ready", "running", ...) never spin.
14
- */
15
- const CREATING_STATUSES = new Set([
16
- 'creating',
17
- 'starting',
18
- 'launching',
19
- 'connecting',
20
- 'pending',
21
- 'resuming',
22
- ]);
10
+ import { isCreatingAgent, } from '../../portable/runtimes';
23
11
  const MAX_AGENT_NAME_LENGTH = 32;
24
12
  const toSandboxStatusUrl = (value) => {
25
13
  const raw = String(value || '').trim();
@@ -95,12 +83,8 @@ export const AgentSummary = ({ summary, title = 'Active Agent', }) => {
95
83
  if (!summary) {
96
84
  return null;
97
85
  }
98
- const normalizedStatus = (summary.status || '').toLowerCase();
99
- // Only show the spinner while the agent is genuinely being created: it is not
100
- // ready, has no id yet, and its status is an active creation/startup state.
101
- const isCreating = !summary.isReady &&
102
- !summary.agentId &&
103
- CREATING_STATUSES.has(normalizedStatus);
86
+ // Only show the spinner while the agent is genuinely being created.
87
+ const isCreating = isCreatingAgent(summary);
104
88
  const hasError = Boolean(summary.error);
105
89
  // The agent is optional: some examples run only a code sandbox. Show the
106
90
  // agent status when there is an agent (or one is being created).