@datalayer/agent-runtimes 1.3.84 → 1.3.85

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 (141) hide show
  1. package/lib/chat/ChatFloating.d.ts +5 -4
  2. package/lib/chat/ChatFloating.js +8 -6
  3. package/lib/chat/ChatLanguage.d.ts +17 -0
  4. package/lib/chat/ChatLanguage.js +29 -0
  5. package/lib/chat/assistant/AssistantStage.d.ts +4 -4
  6. package/lib/chat/assistant/AssistantStage.js +4 -2
  7. package/lib/chat/assistant/BalloonParts.d.ts +2 -1
  8. package/lib/chat/assistant/BalloonParts.js +12 -5
  9. package/lib/chat/assistant/BalloonVisual.js +3 -1
  10. package/lib/chat/assistant/ConversationBalloon.d.ts +1 -1
  11. package/lib/chat/assistant/ConversationBalloon.js +3 -2
  12. package/lib/chat/assistant/SpeechBalloon.d.ts +1 -1
  13. package/lib/chat/assistant/SpeechBalloon.js +10 -7
  14. package/lib/chat/assistant/toolLine.d.ts +8 -4
  15. package/lib/chat/assistant/toolLine.js +13 -9
  16. package/lib/chat/base/ChatBase.js +14 -11
  17. package/lib/chat/display/EmptyState.js +3 -1
  18. package/lib/chat/index.d.ts +2 -0
  19. package/lib/chat/index.js +3 -0
  20. package/lib/chat/messages/ChatMessages.js +3 -1
  21. package/lib/chat/messages/TurnFooter.js +3 -1
  22. package/lib/chat/prompt/InputPrompt.js +3 -1
  23. package/lib/chat/prompt/InputPromptLexical.js +3 -1
  24. package/lib/chat/prompt/footer/InPromptFooter.js +3 -1
  25. package/lib/chat/tools/ToolApprovalBanner.js +5 -3
  26. package/lib/chat/tools/ToolApprovalDialog.js +5 -3
  27. package/lib/chat/tools/ToolCallDisplay.js +29 -25
  28. package/lib/chat/words.d.ts +102 -0
  29. package/lib/chat/words.js +460 -0
  30. package/lib/components/a2ui/custom/index.d.ts +39 -0
  31. package/lib/components/a2ui/custom/index.js +95 -0
  32. package/lib/components/a2ui/custom/sandbox.d.ts +73 -0
  33. package/lib/components/a2ui/custom/sandbox.js +188 -0
  34. package/lib/components/a2ui/datalayer/index.d.ts +1 -1
  35. package/lib/components/a2ui/datalayer/index.js +1 -1
  36. package/lib/components/a2ui/datalayer/schema.d.ts +10 -0
  37. package/lib/components/a2ui/datalayer/schema.js +12 -1
  38. package/lib/components/a2ui/index.d.ts +1 -0
  39. package/lib/components/a2ui/index.js +1 -0
  40. package/lib/components/a2ui/visibleWhen.d.ts +12 -1
  41. package/lib/components/a2ui/visibleWhen.js +24 -3
  42. package/lib/components/inspector/AgentInspector.d.ts +25 -44
  43. package/lib/components/inspector/AgentInspector.js +27 -215
  44. package/lib/components/inspector/AgentInspectorDialog.d.ts +6 -6
  45. package/lib/components/inspector/AgentInspectorDialog.js +2 -2
  46. package/lib/components/inspector/a2aSpans.d.ts +51 -0
  47. package/lib/components/inspector/a2aSpans.js +373 -0
  48. package/lib/components/inspector/agentSpans.d.ts +120 -0
  49. package/lib/components/inspector/agentSpans.js +253 -0
  50. package/lib/components/inspector/chatSpans.d.ts +36 -0
  51. package/lib/components/inspector/chatSpans.js +50 -0
  52. package/lib/components/inspector/classify.d.ts +11 -8
  53. package/lib/components/inspector/classify.js +23 -24
  54. package/lib/components/inspector/index.d.ts +7 -7
  55. package/lib/components/inspector/index.js +7 -7
  56. package/lib/components/teams/A2ATeamGraph.d.ts +4 -4
  57. package/lib/components/teams/SceneTranscript.d.ts +29 -0
  58. package/lib/components/teams/SceneTranscript.js +54 -0
  59. package/lib/components/teams/SceneView.d.ts +41 -0
  60. package/lib/components/teams/SceneView.js +28 -0
  61. package/lib/components/teams/index.d.ts +3 -0
  62. package/lib/components/teams/index.js +3 -0
  63. package/lib/components/teams/sceneTranscript.d.ts +74 -0
  64. package/lib/components/teams/sceneTranscript.js +294 -0
  65. package/lib/components/teams/teamConnections.d.ts +2 -2
  66. package/lib/components/teams/teamConnections.js +3 -3
  67. package/lib/components/teams/useA2ATeam.d.ts +4 -4
  68. package/lib/components/teams/useA2ATeam.js +50 -46
  69. package/lib/examples/AgentA2ATeamExample.d.ts +10 -9
  70. package/lib/examples/AgentA2ATeamExample.js +70 -60
  71. package/lib/examples/AssistantExample.js +2 -2
  72. package/lib/loop/apps/AppComposer.d.ts +2 -1
  73. package/lib/loop/apps/AppComposer.js +6 -3
  74. package/lib/loop/apps/AppRenderer.d.ts +2 -1
  75. package/lib/loop/apps/AppRenderer.js +34 -25
  76. package/lib/loop/apps/appspec.d.ts +11 -1
  77. package/lib/loop/apps/appspec.js +115 -1
  78. package/lib/loop/apps/checks.d.ts +8 -0
  79. package/lib/loop/apps/checks.js +106 -4
  80. package/lib/loop/apps/customComponents.d.ts +41 -0
  81. package/lib/loop/apps/customComponents.js +260 -0
  82. package/lib/loop/apps/hostTools.d.ts +8 -1
  83. package/lib/loop/apps/hostTools.js +9 -0
  84. package/lib/loop/apps/index.d.ts +1 -0
  85. package/lib/loop/apps/index.js +1 -0
  86. package/lib/loop/core/a2uiComponents.d.ts +10 -0
  87. package/lib/loop/core/a2uiComponents.js +15 -0
  88. package/lib/loop/core/index.d.ts +1 -0
  89. package/lib/loop/core/index.js +3 -0
  90. package/lib/loop/embed/AppEmbed.d.ts +11 -3
  91. package/lib/loop/embed/AppEmbed.js +42 -16
  92. package/lib/loop/embed/element.d.ts +5 -0
  93. package/lib/loop/embed/element.js +61 -5
  94. package/lib/loop/embed/embedConfig.d.ts +8 -0
  95. package/lib/loop/embed/embedConfig.js +18 -0
  96. package/lib/loop/embed/hostBridge.d.ts +28 -2
  97. package/lib/loop/embed/hostBridge.js +22 -2
  98. package/lib/loop/index.d.ts +4 -3
  99. package/lib/loop/index.js +4 -3
  100. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +7 -1
  101. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +7 -2
  102. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +4 -1
  103. package/lib/loop/plugins/a2ui-surface/ownComponents.d.ts +6 -0
  104. package/lib/loop/plugins/a2ui-surface/ownComponents.js +30 -0
  105. package/lib/loop/plugins/app-components/index.d.ts +26 -0
  106. package/lib/loop/plugins/app-components/index.js +56 -0
  107. package/lib/loop/plugins/app-elements/AppElements.js +4 -1
  108. package/lib/loop/plugins/app-page/AppPage.d.ts +9 -0
  109. package/lib/loop/plugins/app-page/AppPage.js +106 -8
  110. package/lib/loop/plugins/app-page/appPageModel.d.ts +17 -1
  111. package/lib/loop/plugins/app-page/appPageModel.js +120 -4
  112. package/lib/loop/plugins/app-page/index.d.ts +1 -0
  113. package/lib/loop/plugins/app-page/index.js +1 -0
  114. package/lib/loop/plugins/app-page/pageRun.d.ts +46 -0
  115. package/lib/loop/plugins/app-page/pageRun.js +111 -0
  116. package/lib/loop/plugins/assistant/index.js +8 -6
  117. package/lib/loop/plugins/chat/ChatView.js +6 -4
  118. package/lib/loop/plugins/chat/index.d.ts +1 -1
  119. package/lib/loop/plugins/chat/index.js +2 -1
  120. package/lib/models/PageTag.d.ts +1 -1
  121. package/lib/runtimes/browser/a2aPeer.d.ts +2 -1
  122. package/lib/specs/actions.js +78 -1
  123. package/lib/specs/apps.d.ts +5 -0
  124. package/lib/specs/apps.js +1384 -231
  125. package/lib/specs/appspecSchema.js +191 -1
  126. package/lib/specs/teams/teams.d.ts +3 -0
  127. package/lib/specs/teams/teams.js +249 -0
  128. package/lib/types/agentspecs.d.ts +70 -0
  129. package/lib/types/chat.d.ts +6 -5
  130. package/lib/types/teams.d.ts +13 -2
  131. package/package.json +2 -2
  132. package/scripts/codegen/generate_apps.py +4 -2
  133. package/scripts/codegen/generate_teams.py +15 -2
  134. package/lib/components/inspector/JsonTree.d.ts +0 -24
  135. package/lib/components/inspector/JsonTree.js +0 -89
  136. package/lib/components/inspector/a2aInspect.d.ts +0 -56
  137. package/lib/components/inspector/a2aInspect.js +0 -478
  138. package/lib/components/inspector/agentInspector.d.ts +0 -179
  139. package/lib/components/inspector/agentInspector.js +0 -267
  140. package/lib/components/inspector/chatInspect.d.ts +0 -38
  141. package/lib/components/inspector/chatInspect.js +0 -65
@@ -19,15 +19,16 @@
19
19
  * list_invoices…" while Accounting calls Odoo. Then the report appears. The
20
20
  * page's own state is `useA2ATeam`'s, which the landing's home page runs too.
21
21
  *
22
- * Under the graph, each member has its own Agent Inspector: Sales' — its
23
- * model turns, its `ask_accounting` call, the A2A messages it sends and
24
- * receives — and Accounting's — what it received over A2A, its Odoo MCP
25
- * calls, `write_notebook`, its status updates and artifacts, as the A2A
26
- * stream tells them. Both read one record (`useAgentInspector`), fed by the
27
- * team (`useA2ATeam`'s `inspector`) and by the fetch the A2A client sends
28
- * with (`inspectA2AFetch`); an A2A message shows in both, sent in one and
29
- * received in the other. Right-click a member for its menu: *Inspect the
30
- * agent…* opens its inspector over the page.
22
+ * Under the graph, each member has its own Agent Inspector, core's OTEL
23
+ * view of the page's OpenTelemetry spans: Sales' — its model turns
24
+ * (`invoke_agent`), its `ask_accounting` call (`execute_tool`), the A2A
25
+ * requests it sends and what came back, event by event — and Accounting's —
26
+ * the A2A requests sent to it (`peer.service`), its Odoo MCP calls,
27
+ * `write_notebook`, as the A2A stream tells them. Both read one tracer
28
+ * (`useAgentInspector`), fed by the team (`useA2ATeam`'s `inspector`) and by
29
+ * the fetch the A2A client sends with (`traceA2AFetch`); an A2A request
30
+ * shows in both. Right-click a member for its menu: *Inspect the agent…*
31
+ * opens its inspector over the page.
31
32
  *
32
33
  * Sales' suggestions — its Appspec's starters — are chips in its balloon
33
34
  * while it waits for a question: one clicked is asked, as if typed.
@@ -10,9 +10,9 @@ import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
10
10
  import { useBrowserInference } from '../hooks/useBrowserInference';
11
11
  import { readJwtClaims, tokenLifetimeMs, useAnonymousSessionStore, } from '../runtimes/browser/anonymousToken';
12
12
  import { connectA2APeer } from '../runtimes/browser/a2aPeer';
13
- import { A2ATeamGraph, NOTEBOOK_AND_WORDS, teamConnectionsOf, teamToolClassifier, useA2ATeam, usePeerSandbox, } from '../components/teams';
13
+ import { A2ATeamGraph, SceneView, NOTEBOOK_AND_WORDS, teamConnectionsOf, teamToolClassifier, useA2ATeam, usePeerSandbox, } from '../components/teams';
14
14
  import { ACCOUNTING_APP_0_0_1, SALES_APP_0_0_1 } from '../specs/apps';
15
- import { AgentInspector, classifyToolCall, inspectA2AFetch, useAgentInspector, } from '../components/inspector';
15
+ import { AgentInspector, classifyToolCall, traceA2AFetch, useAgentInspector, } from '../components/inspector';
16
16
  import { SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1 } from '../specs/teams/teams';
17
17
  const TEAM = SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1;
18
18
  const SALES = SALES_APP_0_0_1;
@@ -46,8 +46,18 @@ function AgentA2ATeam() {
46
46
  }, [accountingKey]);
47
47
  const [accountingKeyEnded, setAccountingKeyEnded] = useState(() => accountingKeyClock !== undefined &&
48
48
  accountingKeyClock.expiresAt <= Date.now());
49
- // One record of what both members do, each inspector showing its own.
50
- const { sink: inspector } = useAgentInspector();
49
+ // One tracer of what both members do, each inspector showing its own.
50
+ const { tracer: inspector } = useAgentInspector();
51
+ // The members as the transcript names them: the spans' services, and the
52
+ // connections a tool call is of (Accounting → Odoo).
53
+ const sceneMembers = useMemo(() => [
54
+ { id: SALES.id, name: SALES.name },
55
+ {
56
+ id: ACCOUNTING.id,
57
+ name: ACCOUNTING.name,
58
+ connections: ACCOUNTING_CONNECTIONS,
59
+ },
60
+ ], []);
51
61
  const [peer, setPeer] = useState(null);
52
62
  const [peerError, setPeerError] = useState(null);
53
63
  const [draft, setDraft] = useState('');
@@ -61,9 +71,9 @@ function AgentA2ATeam() {
61
71
  connectA2APeer({
62
72
  url: accountingUrl,
63
73
  key: accountingKey,
64
- // The A2A traffic, recorded: Sales sends, Accounting answers.
65
- fetch: inspectA2AFetch(globalThis.fetch.bind(globalThis), {
66
- sink: inspector,
74
+ // The A2A traffic, as spans: Sales sends, Accounting answers.
75
+ fetch: traceA2AFetch(globalThis.fetch.bind(globalThis), {
76
+ tracer: inspector,
67
77
  asker: SALES.name,
68
78
  peer: ACCOUNTING.name,
69
79
  classifyTool: teamToolClassifier(ACCOUNTING_CONNECTIONS, classifyToolCall),
@@ -116,66 +126,66 @@ function AgentA2ATeam() {
116
126
  px: 2,
117
127
  pt: 2,
118
128
  mb: 3,
119
- }, children: _jsx(A2ATeamGraph, { entry: {
120
- id: SALES.id,
121
- name: SALES.name,
122
- emoji: SALES.emoji,
123
- character: SALES.interface.assistant ?? 'paperclip',
124
- where: salesMember.runsIn === 'browser'
125
- ? 'in your browser'
126
- : 'on a runtime',
127
- persona: team.entryPersona,
128
- onToggle: () => composer.current?.focus(),
129
- conversationLabel: `Ask ${SALES.name}`,
130
- onAway: team.setEntryAway,
131
- history: team.entryHistory,
132
- expandTarget: notebookArea,
133
- notebookTitle: `${ACCOUNTING.name}\u2019s notebook`,
134
- suggestions: team.ready && !team.busy ? suggestions : undefined,
135
- onSuggestion: suggestion => send(suggestion.prompt),
136
- inspector,
137
- onStop: team.busy ? team.stop : undefined,
138
- about: {
129
+ }, children: _jsx(SceneView, { members: sceneMembers, tracer: inspector, emptyText: "Ask Sales: what it says to Accounting, what Accounting reads in Odoo and answers, line by line, shows here.", children: _jsx(A2ATeamGraph, { entry: {
130
+ id: SALES.id,
139
131
  name: SALES.name,
140
- spec: `${SALES.id}:${SALES.version}`,
141
- model: SALES.model || undefined,
142
- where: 'in your browser',
143
- description: SALES.description || undefined,
144
- },
145
- }, peer: {
146
- id: ACCOUNTING.id,
147
- name: ACCOUNTING.name,
148
- // Its face as its card says it, once it is reached.
149
- emoji: peer?.face?.emoji ?? ACCOUNTING.emoji,
150
- character: ACCOUNTING.interface.assistant ?? 'wizard',
151
- where: accountingWhere,
152
- persona: team.peerPersona,
153
- onAway: team.setPeerAway,
154
- history: team.peerHistory,
155
- connections: ACCOUNTING_CONNECTIONS,
156
- inspector,
157
- sandbox: accountingSandbox,
158
- about: {
132
+ emoji: SALES.emoji,
133
+ character: SALES.interface.assistant ?? 'paperclip',
134
+ where: salesMember.runsIn === 'browser'
135
+ ? 'in your browser'
136
+ : 'on a runtime',
137
+ persona: team.entryPersona,
138
+ onToggle: () => composer.current?.focus(),
139
+ conversationLabel: `Ask ${SALES.name}`,
140
+ onAway: team.setEntryAway,
141
+ history: team.entryHistory,
142
+ expandTarget: notebookArea,
143
+ notebookTitle: `${ACCOUNTING.name}\u2019s notebook`,
144
+ suggestions: team.ready && !team.busy ? suggestions : undefined,
145
+ onSuggestion: suggestion => send(suggestion.prompt),
146
+ inspector,
147
+ onStop: team.busy ? team.stop : undefined,
148
+ about: {
149
+ name: SALES.name,
150
+ spec: `${SALES.id}:${SALES.version}`,
151
+ model: SALES.model || undefined,
152
+ where: 'in your browser',
153
+ description: SALES.description || undefined,
154
+ },
155
+ }, peer: {
156
+ id: ACCOUNTING.id,
159
157
  name: ACCOUNTING.name,
160
- spec: `${ACCOUNTING.id}:${ACCOUNTING.version}`,
161
- model: ACCOUNTING.model || undefined,
158
+ // Its face as its card says it, once it is reached.
159
+ emoji: peer?.face?.emoji ?? ACCOUNTING.emoji,
160
+ character: ACCOUNTING.interface.assistant ?? 'wizard',
162
161
  where: accountingWhere,
163
- // What its A2A card says, once it is reached.
164
- description: peer?.card.description || ACCOUNTING.description || undefined,
165
- skills: peer?.card.skills.map(skill => skill.name),
166
- protocol: peer ? 'a2a' : undefined,
167
- url: peer ? accountingUrl : undefined,
168
- },
169
- }, flow: team.flow, calls: team.calls,
170
- // Room for the balloons: suggestions, *more* and a notebook.
171
- balloonRoom: 320, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }), _jsx(Box, { ref: notebookArea, sx: { mb: team.notebook ? 3 : 0 }, "data-team-notebook-placement": "under-graph" }), _jsx(Box, { sx: {
162
+ persona: team.peerPersona,
163
+ onAway: team.setPeerAway,
164
+ history: team.peerHistory,
165
+ connections: ACCOUNTING_CONNECTIONS,
166
+ inspector,
167
+ sandbox: accountingSandbox,
168
+ about: {
169
+ name: ACCOUNTING.name,
170
+ spec: `${ACCOUNTING.id}:${ACCOUNTING.version}`,
171
+ model: ACCOUNTING.model || undefined,
172
+ where: accountingWhere,
173
+ // What its A2A card says, once it is reached.
174
+ description: peer?.card.description || ACCOUNTING.description || undefined,
175
+ skills: peer?.card.skills.map(skill => skill.name),
176
+ protocol: peer ? 'a2a' : undefined,
177
+ url: peer ? accountingUrl : undefined,
178
+ },
179
+ }, flow: team.flow, calls: team.calls,
180
+ // Room for the balloons: suggestions, *more* and a notebook.
181
+ balloonRoom: 320, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }) }), _jsx(Box, { ref: notebookArea, sx: { mb: team.notebook ? 3 : 0 }, "data-team-notebook-placement": "under-graph" }), _jsx(Box, { sx: {
172
182
  display: 'grid',
173
183
  gridTemplateColumns: ['1fr', '1fr', '1fr 1fr'],
174
184
  gap: 3,
175
185
  mb: 3,
176
- }, "data-team-inspectors": "", children: [SALES, ACCOUNTING].map(member => (_jsxs(Box, { sx: { minWidth: 0 }, "data-team-inspector": member.id, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: [member.emoji, " ", member.name, "\u2019s Agent Inspector"] }), _jsx(AgentInspector, { sink: inspector, agent: member.name, maxHeight: 360, exportName: `agent-inspector-${member.id}`, emptyText: member === SALES
177
- ? 'Its model turns, its call to Accounting and the A2A messages it sends and receives show here.'
178
- : 'What it receives over A2A, its Odoo MCP calls and what it sends back show here.' })] }, member.id))) }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", SALES.name] }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, mt: 0 }, children: ["Or choose one of ", SALES.name, "\u2019s suggestions in its balloon."] }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: "Ask for a financial report\u2026", onChange: event => setDraft(event.target.value), onKeyDown: event => {
186
+ }, "data-team-inspectors": "", children: [SALES, ACCOUNTING].map(member => (_jsxs(Box, { sx: { minWidth: 0 }, "data-team-inspector": member.id, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: [member.emoji, " ", member.name, "\u2019s Agent Inspector"] }), _jsx(AgentInspector, { tracer: inspector, agent: member.name, compact: true, maxHeight: 360, exportName: `agent-inspector-${member.id}`, emptyText: member === SALES
187
+ ? 'Its model turns, its call to Accounting and the A2A requests it sends, with what came back, show here.'
188
+ : 'The A2A requests sent to it, its Odoo MCP calls and what it sends back show here.' })] }, member.id))) }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", SALES.name] }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, mt: 0 }, children: ["Or choose one of ", SALES.name, "\u2019s suggestions in its balloon."] }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: "Ask for a financial report\u2026", onChange: event => setDraft(event.target.value), onKeyDown: event => {
179
189
  if (event.key === 'Enter' && !event.shiftKey) {
180
190
  event.preventDefault();
181
191
  send(draft);
@@ -15,7 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  * and so are the characters clippy.js publishes, fetched when picked.
16
16
  */
17
17
  import { useEffect, useRef, useState } from 'react';
18
- import { useAgentInspector } from '../components/inspector/agentInspector';
18
+ import { useAgentInspector } from '../components/inspector/agentSpans';
19
19
  import { buildReactorFromPlugins } from '@datalayer/reactor';
20
20
  import { Button, Heading, Text, ToggleSwitch } from '@primer/react';
21
21
  import { Box } from '@datalayer/primer-addons';
@@ -96,7 +96,7 @@ const AssistantExample = () => {
96
96
  // How the balloon shows the conversation (T-23): history, or current.
97
97
  const [balloon, setBalloon] = useState('history');
98
98
  // What the agent does, for the menu's *Inspect the agent…*.
99
- const { sink: inspector } = useAgentInspector();
99
+ const { tracer: inspector } = useAgentInspector();
100
100
  // Pixel, the test sprite, read once through the clippy.js reader.
101
101
  const [pixel, setPixel] = useState();
102
102
  useEffect(() => {
@@ -23,7 +23,8 @@
23
23
  * conversation keeps its profile: the picker says so and picks no other.
24
24
  *
25
25
  * Given the application as the person reads it (`translatedAppspec`, LOOP
26
- * P-26), its words are theirs; the composer's own, `language`'s.
26
+ * P-26), its words are theirs; the composer's own, the chat's language's
27
+ * (`useChatLanguage`).
27
28
  *
28
29
  * @module loop/apps/AppComposer
29
30
  */
@@ -28,7 +28,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
28
28
  * conversation keeps its profile: the picker says so and picks no other.
29
29
  *
30
30
  * Given the application as the person reads it (`translatedAppspec`, LOOP
31
- * P-26), its words are theirs; the composer's own, `language`'s.
31
+ * P-26), its words are theirs; the composer's own, the chat's language's
32
+ * (`useChatLanguage`).
32
33
  *
33
34
  * @module loop/apps/AppComposer
34
35
  */
@@ -40,8 +41,8 @@ import { useSignalValue } from '@datalayer/reactor/react';
40
41
  import { readFile, } from '../../components/a2ui/datalayer/FileUpload';
41
42
  import { LoopChatExtras, LoopCommand, LoopRunProps, LoopSlots, } from '../core';
42
43
  import { commandPrompt, modeChoice, modeDefaults, profileChoice, startersFor, } from './composer';
43
- import { DEFAULT_LANGUAGE } from './appspec';
44
44
  import { interfaceWords } from './language';
45
+ import { useChatLanguage } from '../../chat/ChatLanguage';
45
46
  import { uploadsAccept, uploadsInWords, uploadsRefusal } from './uploads';
46
47
  /**
47
48
  * Files chosen in the composer, held for the next message: the refusal the
@@ -89,7 +90,6 @@ export const needsAppComposer = (app) => (app.interface.commands ?? []).length >
89
90
  * Nothing for an application with none: its plugin contributes nothing.
90
91
  */
91
92
  export function defineAppComposerPlugin(app) {
92
- const words = interfaceWords(app.interface.language ?? DEFAULT_LANGUAGE);
93
93
  const profiles = app.interface.profiles ?? [];
94
94
  /** The profile picked, and whether the conversation has started with it. */
95
95
  const profile = signal(profileChoice(app)?.id);
@@ -98,6 +98,8 @@ export function defineAppComposerPlugin(app) {
98
98
  openers: startersAsOpeners(startersFor(app, profile.value)),
99
99
  }));
100
100
  function ProfilePicker() {
101
+ // In the chat's language, as the chat's own words (P-26).
102
+ const words = interfaceWords(useChatLanguage());
101
103
  const now = profileChoice(app, useSignalValue(profile));
102
104
  const kept = useSignalValue(started);
103
105
  if (!now) {
@@ -131,6 +133,7 @@ export function defineAppComposerPlugin(app) {
131
133
  /** The files held for the next message, and the last refusal (P-21). */
132
134
  const held = signal({ files: [], refused: null });
133
135
  function Attach() {
136
+ const words = interfaceWords(useChatLanguage());
134
137
  const now = useSignalValue(held);
135
138
  const input = useRef(null);
136
139
  if (!uploads) {
@@ -176,7 +176,8 @@ export type AppPreset = {
176
176
  * whose contributions are the blocks it may draw (R-01b); a thumb and a
177
177
  * comment on each answer when its record keeps feedback (V-18); its
178
178
  * commands in the composer's `/` menu and its modes beside it (P-19); the
179
- * side panel and the pages its code opens (`app-elements`, P-18); what it
179
+ * side panel and the pages its code opens (`app-elements`, P-18); the
180
+ * components its developer wrote (`app-components`, P-17); what it
180
181
  * keeps and for how long, under its prompt before the first message, when
181
182
  * `kept` says it (R-31); with
182
183
  * `sidebar`, its rules and approvals card, its activity feed and its
@@ -58,6 +58,7 @@ import { LoopEmbed } from '../embed/LoopEmbed';
58
58
  import { dumpAppspec } from './appspec';
59
59
  import { defineAppActivityPlugin } from '../plugins/app-activity';
60
60
  import { AppElementsPlugin } from '../plugins/app-elements';
61
+ import { defineAppComponentsPlugin, hasCustomComponents, } from '../plugins/app-components';
61
62
  import { defineAppComputerPlugin } from '../plugins/app-computer';
62
63
  import { defineAppRulesPlugin } from '../plugins/app-rules';
63
64
  import { defineAppComposerPlugin, needsAppComposer, startersAsOpeners, } from './AppComposer';
@@ -67,6 +68,7 @@ import { defineAppFeedbackPlugin } from './AppFeedback';
67
68
  import { defineAppKeptPlugin } from './AppKept';
68
69
  import { keepsFeedback } from './feedback';
69
70
  import { keptBeforeFirstMessage } from './kept';
71
+ import { ChatLanguage } from '../../chat/ChatLanguage';
70
72
  /** The id of an agent or a Cog, without its version. */
71
73
  export const agentIdOf = (app) => {
72
74
  const reference = app.agent || app.team;
@@ -167,7 +169,8 @@ export function appLayoutOptions(app) {
167
169
  * whose contributions are the blocks it may draw (R-01b); a thumb and a
168
170
  * comment on each answer when its record keeps feedback (V-18); its
169
171
  * commands in the composer's `/` menu and its modes beside it (P-19); the
170
- * side panel and the pages its code opens (`app-elements`, P-18); what it
172
+ * side panel and the pages its code opens (`app-elements`, P-18); the
173
+ * components its developer wrote (`app-components`, P-17); what it
171
174
  * keeps and for how long, under its prompt before the first message, when
172
175
  * `kept` says it (R-31); with
173
176
  * `sidebar`, its rules and approvals card, its activity feed and its
@@ -203,6 +206,9 @@ export function appPreset(app, options = {}) {
203
206
  plugins: [
204
207
  defineAppPlugin(app, shown),
205
208
  ...(withPage ? [defineAppPagePlugin(shown), ...blocks] : []),
209
+ // The components its developer wrote (LOOP P-17): blocks of its page
210
+ // and renderers of its page, its elements and its answers — its own.
211
+ ...(hasCustomComponents(app) ? [defineAppComponentsPlugin(app)] : []),
206
212
  // Its commands in the composer's `/` menu, its modes and its profiles
207
213
  // beside the composer (LOOP P-19, P-20) and what a person may attach
208
214
  // there (P-21): only for an application that has some.
@@ -385,31 +391,34 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
385
391
  if ('problem' in preset) {
386
392
  return (_jsx("div", { role: "status", style: { padding: 16 }, children: preset.problem }));
387
393
  }
388
- return (_jsx(LoopEmbed
389
- // The application's own agent: created under its id, so that its
390
- // spec is applied to an agent of its own, never to the one it extends.
391
- , {
394
+ return (
395
+ // The chat's own words in the person's language too (P-26): the host's,
396
+ // else the browser's.
397
+ _jsx(ChatLanguage, { language: language, children: _jsx(LoopEmbed
392
398
  // The application's own agent: created under its id, so that its
393
399
  // spec is applied to an agent of its own, never to the one it extends.
394
- agentId: app.id, ...preset.workspace, ...(frame
395
- ? {
396
- frameTitle: [app.emoji, shown.name].filter(Boolean).join(' '),
397
- }
398
- : {}),
399
- // Its rules, activity and computer on a rail, one at a time (T-07).
400
- sidebarRail: sidebar || computer, teamPicker: false, showAgentVariants: false, graph: false, pluginsPanel: false, datalayerAgentSpecId: DATALAYER_BOOTSTRAP_AGENTSPEC,
401
- // Where an application runs is decided by its host — the Studio's
402
- // Preview, the hosted page — not offered to its user.
403
- targetFixed: true, ...(named ? { themeVariant: named.variant } : {}), ...(namedMode ? { colorMode: namedMode } : {}), themeOverrides: themeOverrides,
404
- // Its own face, name and welcome in the chat (T-08); no counters: a
405
- // person using an application is not asking about tokens.
406
- presence: {
407
- name: shown.name,
408
- face: app.emoji,
409
- welcome: shown.interface.welcome || shown.description,
410
- paused,
411
- onPresence,
412
- onSaying,
413
- }, showTokenUsage: false, datalayerCreatePayload: datalayerCreatePayload, ...embed, plugins: allPlugins }));
400
+ , {
401
+ // The application's own agent: created under its id, so that its
402
+ // spec is applied to an agent of its own, never to the one it extends.
403
+ agentId: app.id, ...preset.workspace, ...(frame
404
+ ? {
405
+ frameTitle: [app.emoji, shown.name].filter(Boolean).join(' '),
406
+ }
407
+ : {}),
408
+ // Its rules, activity and computer on a rail, one at a time (T-07).
409
+ sidebarRail: sidebar || computer, teamPicker: false, showAgentVariants: false, graph: false, pluginsPanel: false, datalayerAgentSpecId: DATALAYER_BOOTSTRAP_AGENTSPEC,
410
+ // Where an application runs is decided by its host — the Studio's
411
+ // Preview, the hosted page — not offered to its user.
412
+ targetFixed: true, ...(named ? { themeVariant: named.variant } : {}), ...(namedMode ? { colorMode: namedMode } : {}), themeOverrides: themeOverrides,
413
+ // Its own face, name and welcome in the chat (T-08); no counters: a
414
+ // person using an application is not asking about tokens.
415
+ presence: {
416
+ name: shown.name,
417
+ face: app.emoji,
418
+ welcome: shown.interface.welcome || shown.description,
419
+ paused,
420
+ onPresence,
421
+ onSaying,
422
+ }, showTokenUsage: false, datalayerCreatePayload: datalayerCreatePayload, ...embed, plugins: allPlugins }) }));
414
423
  }
415
424
  export default AppRenderer;
@@ -24,7 +24,7 @@
24
24
  *
25
25
  * @module loop/apps/appspec
26
26
  */
27
- import type { AppAccent, AppThemeMode, AppThemeVariant, AppAssistantCharacter, AppEmbedMode, AppVoiceSpec, AppKind, AppLayout, AppSpec } from '../../types/agentspecs';
27
+ import type { AppAccent, AppThemeMode, AppThemeVariant, AppAssistantCharacter, AppEmbedMode, AppCustomComponentSpec, AppVoiceSpec, AppKind, AppLayout, AppPageOutputComponent, AppSpec } from '../../types/agentspecs';
28
28
  /** The version of the spec itself. */
29
29
  export declare const APP_SCHEMA = "loop.app/v1";
30
30
  /** The language an application's own words are in, unless it says (LOOP P-26). */
@@ -62,6 +62,16 @@ export declare function retentionDays(keepFor: string): number;
62
62
  export declare function emptyAppspec(kind?: AppKind): AppSpec;
63
63
  /** An application's voice (VOICE.md VO-41): off unless said. */
64
64
  export declare const DEFAULT_VOICE: AppVoiceSpec;
65
+ /** What a page's output may be drawn with (LOOP P-05), and the property of each that shows its value. */
66
+ export declare const PAGE_OUTPUT_SHOWS: Record<AppPageOutputComponent, string>;
67
+ /** The height of a component its developer wrote (P-17), unless it says. */
68
+ export declare const DEFAULT_CUSTOM_HEIGHT = 240;
69
+ /**
70
+ * The property a page's output fills with its value (P-05): a catalog
71
+ * component's (`PAGE_OUTPUT_SHOWS`), or what a component of the
72
+ * application's own shows first (P-17). Undefined for any other.
73
+ */
74
+ export declare function pageOutputShows(component: string, own?: readonly AppCustomComponentSpec[]): string | undefined;
65
75
  /** What reading a document gives: the application, and what the document got wrong. */
66
76
  export interface ParsedAppspec {
67
77
  app: AppSpec;
@@ -363,6 +363,103 @@ function parseUploads(data) {
363
363
  maxFiles: numberOf(data.max_files, DEFAULT_MAX_FILES),
364
364
  };
365
365
  }
366
+ /** What a page's output may be drawn with (LOOP P-05), and the property of each that shows its value. */
367
+ export const PAGE_OUTPUT_SHOWS = {
368
+ Text: 'text',
369
+ Image: 'url',
370
+ Table: 'rows',
371
+ Chart: 'points',
372
+ };
373
+ /** The height of a component its developer wrote (P-17), unless it says. */
374
+ export const DEFAULT_CUSTOM_HEIGHT = 240;
375
+ /**
376
+ * A component its developer wrote (LOOP P-17), as written: its schema and
377
+ * example kept whole, so that the checks say what is wrong with them.
378
+ */
379
+ function parseCustomComponent(data) {
380
+ const component = {
381
+ name: text(data.name),
382
+ description: text(data.description),
383
+ props: isData(data.props)
384
+ ? JSON.parse(JSON.stringify(data.props))
385
+ : { type: 'object', properties: {} },
386
+ shows: texts(data.shows),
387
+ sends: texts(data.sends),
388
+ source: text(data.source),
389
+ integrity: text(data.integrity),
390
+ height: numberOf(data.height, DEFAULT_CUSTOM_HEIGHT),
391
+ };
392
+ if (isData(data.example)) {
393
+ component.example = JSON.parse(JSON.stringify(data.example));
394
+ }
395
+ return component;
396
+ }
397
+ function dumpCustomComponent(component) {
398
+ const written = new Writer()
399
+ .text('name', component.name, '\u0000')
400
+ .text('description', component.description, '\u0000');
401
+ written.data.props = JSON.parse(JSON.stringify(component.props));
402
+ written
403
+ .list('shows', component.shows)
404
+ .list('sends', component.sends)
405
+ .text('source', component.source, '\u0000')
406
+ .text('integrity', component.integrity)
407
+ .value('height', component.height, DEFAULT_CUSTOM_HEIGHT);
408
+ if (component.example) {
409
+ written.data.example = JSON.parse(JSON.stringify(component.example));
410
+ }
411
+ return written.data;
412
+ }
413
+ /**
414
+ * The property a page's output fills with its value (P-05): a catalog
415
+ * component's (`PAGE_OUTPUT_SHOWS`), or what a component of the
416
+ * application's own shows first (P-17). Undefined for any other.
417
+ */
418
+ export function pageOutputShows(component, own = []) {
419
+ if (component in PAGE_OUTPUT_SHOWS) {
420
+ return PAGE_OUTPUT_SHOWS[component];
421
+ }
422
+ return own.find(each => each.name === component)?.shows[0];
423
+ }
424
+ /**
425
+ * A widget's page written in its code (P-05), as written: its inputs a form
426
+ * kept whole, an output drawn with another component kept as written so that
427
+ * the checks say it.
428
+ */
429
+ function parsePage(data) {
430
+ const page = {
431
+ function: text(data.function),
432
+ inputs: (isData(data.inputs)
433
+ ? JSON.parse(JSON.stringify(data.inputs))
434
+ : {}),
435
+ outputs: records(data.outputs).map(output => ({
436
+ name: text(output.name),
437
+ title: text(output.title),
438
+ component: text(output.component, 'Text'),
439
+ props: isData(output.props)
440
+ ? JSON.parse(JSON.stringify(output.props))
441
+ : {},
442
+ })),
443
+ live: data.live !== false,
444
+ };
445
+ if (isData(data.inputs_ui)) {
446
+ page.inputsUi = JSON.parse(JSON.stringify(data.inputs_ui));
447
+ }
448
+ return page;
449
+ }
450
+ function dumpPage(page) {
451
+ const written = new Writer().text('function', page.function, '\u0000');
452
+ written.data.inputs = JSON.parse(JSON.stringify(page.inputs));
453
+ if (page.inputsUi) {
454
+ written.data.inputs_ui = { ...page.inputsUi };
455
+ }
456
+ written.list('outputs', page.outputs.map(output => new Writer()
457
+ .text('name', output.name, '\u0000')
458
+ .text('title', output.title)
459
+ .text('component', output.component, 'Text')
460
+ .part('props', { ...output.props }).data));
461
+ return written.value('live', page.live, true).data;
462
+ }
366
463
  function parseInterface(data, kind) {
367
464
  const parsed = {
368
465
  layout: oneOf(data.layout, ['chat', 'page', 'split'], DEFAULT_LAYOUTS[kind]),
@@ -392,9 +489,16 @@ function parseInterface(data, kind) {
392
489
  if (isData(data.uploads)) {
393
490
  parsed.uploads = parseUploads(data.uploads);
394
491
  }
492
+ // Components its developer wrote (P-17): absent when it has none.
493
+ if (Array.isArray(data.custom_components)) {
494
+ parsed.customComponents = records(data.custom_components).map(parseCustomComponent);
495
+ }
395
496
  if (isData(data.surface)) {
396
497
  parsed.surface = parseSurface(data.surface);
397
498
  }
499
+ if (isData(data.page)) {
500
+ parsed.page = parsePage(data.page);
501
+ }
398
502
  if (isAssistantCharacterId(data.assistant)) {
399
503
  parsed.assistant = data.assistant;
400
504
  }
@@ -452,6 +556,11 @@ function parseDeployment(data) {
452
556
  ? {
453
557
  host: {
454
558
  context: texts(data.embedded.host.context),
559
+ // Kept as written when said: a value that is none is refused
560
+ // by the checks, never read as `claimed` (D-21).
561
+ ...(data.embedded.host.user !== undefined
562
+ ? { user: text(data.embedded.host.user) }
563
+ : {}),
455
564
  functions: records(data.embedded.host.functions).map(fn => ({
456
565
  name: text(fn.name),
457
566
  description: text(fn.description),
@@ -752,9 +861,13 @@ function dumpInterface(spec, kind) {
752
861
  .value('max_files', spec.uploads.maxFiles, DEFAULT_MAX_FILES).data;
753
862
  }
754
863
  writer.list('components', spec.components);
864
+ writer.list('custom_components', (spec.customComponents ?? []).map(dumpCustomComponent));
755
865
  if (spec.surface) {
756
866
  writer.data.surface = dumpSurface(spec.surface);
757
867
  }
868
+ if (spec.page) {
869
+ writer.data.page = dumpPage(spec.page);
870
+ }
758
871
  if (spec.assistant) {
759
872
  writer.data.assistant = spec.assistant;
760
873
  }
@@ -905,7 +1018,8 @@ export function dumpAppspec(app) {
905
1018
  JSON.stringify(NO_HOST_PARAMETERS)
906
1019
  ? {}
907
1020
  : { parameters: fn.parameters }),
908
- }))).data).data).data)
1021
+ })))
1022
+ .value('user', app.deployment.embedded.host.user ?? 'claimed', 'claimed').data).data).data)
909
1023
  .text('goal', app.goal)
910
1024
  .list('triggers', app.triggers.map(trigger => new Writer()
911
1025
  .text('type', trigger.type, '\u0000')
@@ -20,6 +20,14 @@ export declare const componentNamed: (name: string) => ComponentSpec | undefined
20
20
  * once; a tool says what it does, by class, each class once.
21
21
  */
22
22
  export declare function ownCodeProblems(app: AppSpec): string[];
23
+ /**
24
+ * What agentspecs refuses of a widget's page written in its code (LOOP
25
+ * P-05): a page that is not a widget's, inputs that are not a form, outputs
26
+ * of one name twice, drawn with another component, saying in their `props`
27
+ * what their value fills or lacking what their component needs, and an input
28
+ * named as a setting — both are on the page at `/inputs/<name>`.
29
+ */
30
+ export declare function pageProblems(app: AppSpec): string[];
23
31
  /** How an organization's own context is named: never a catalogue id (agentspecs' `ORGANIZATION_FRAME_PREFIX`). */
24
32
  /**
25
33
  * What stops a Form block from asking (LOOP C-16), in agentspecs'