@datalayer/agent-runtimes 1.3.29 → 1.3.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/lib/AgentNode.js +14 -33
  2. package/lib/chat/ChatFloating.js +40 -8
  3. package/lib/chat/assistant/AssistantStage.d.ts +26 -7
  4. package/lib/chat/assistant/AssistantStage.js +115 -8
  5. package/lib/chat/assistant/SpeechBalloon.js +7 -3
  6. package/lib/chat/assistant/characters.d.ts +32 -1
  7. package/lib/chat/assistant/characters.js +180 -15
  8. package/lib/chat/assistant/state.d.ts +28 -0
  9. package/lib/chat/assistant/state.js +31 -0
  10. package/lib/chat/base/ChatBase.js +74 -50
  11. package/lib/chat/base/modelChoice.d.ts +40 -1
  12. package/lib/chat/base/modelChoice.js +42 -15
  13. package/lib/chat/prompt/InputPrompt.d.ts +7 -1
  14. package/lib/chat/prompt/InputPrompt.js +2 -2
  15. package/lib/chat/prompt/InputPromptBase.js +1 -1
  16. package/lib/chat/prompt/menus/ModelSelector.d.ts +13 -1
  17. package/lib/chat/prompt/menus/ModelSelector.js +17 -5
  18. package/lib/chat/useViewportDrag.d.ts +9 -1
  19. package/lib/chat/useViewportDrag.js +4 -4
  20. package/lib/components/a2ui/index.d.ts +2 -0
  21. package/lib/components/a2ui/index.js +2 -0
  22. package/lib/components/a2ui/visibility.d.ts +15 -0
  23. package/lib/components/a2ui/visibility.js +24 -0
  24. package/lib/components/a2ui/visibleWhen.d.ts +17 -0
  25. package/lib/components/a2ui/visibleWhen.js +54 -0
  26. package/lib/examples/ChatAssistantExample.d.ts +4 -0
  27. package/lib/examples/ChatAssistantExample.js +26 -5
  28. package/lib/examples/utils/owlCharacterPlugin.d.ts +7 -0
  29. package/lib/examples/utils/owlCharacterPlugin.js +44 -0
  30. package/lib/loop/apps/AppFeedback.d.ts +27 -0
  31. package/lib/loop/apps/AppFeedback.js +78 -0
  32. package/lib/loop/apps/AppRenderer.d.ts +33 -1
  33. package/lib/loop/apps/AppRenderer.js +111 -29
  34. package/lib/loop/apps/agent.d.ts +15 -0
  35. package/lib/loop/apps/agent.js +42 -0
  36. package/lib/loop/apps/appspec.js +6 -3
  37. package/lib/loop/apps/checks.d.ts +10 -2
  38. package/lib/loop/apps/checks.js +21 -10
  39. package/lib/loop/apps/feedback.d.ts +51 -0
  40. package/lib/loop/apps/feedback.js +61 -0
  41. package/lib/loop/apps/index.d.ts +3 -0
  42. package/lib/loop/apps/index.js +3 -0
  43. package/lib/loop/apps/rules.d.ts +14 -0
  44. package/lib/loop/apps/rules.js +24 -0
  45. package/lib/loop/core/canvasBlocks.d.ts +16 -0
  46. package/lib/loop/core/canvasBlocks.js +13 -0
  47. package/lib/loop/core/index.d.ts +15 -0
  48. package/lib/loop/core/index.js +3 -0
  49. package/lib/loop/embed/AppEmbed.d.ts +42 -0
  50. package/lib/loop/embed/AppEmbed.js +205 -0
  51. package/lib/loop/embed/datalayer-app.d.ts +1 -0
  52. package/lib/loop/embed/datalayer-app.js +16 -0
  53. package/lib/loop/embed/element.d.ts +57 -0
  54. package/lib/loop/embed/element.js +405 -0
  55. package/lib/loop/embed/embedConfig.d.ts +120 -0
  56. package/lib/loop/embed/embedConfig.js +148 -0
  57. package/lib/loop/embed/embedTheme.d.ts +24 -0
  58. package/lib/loop/embed/embedTheme.js +83 -0
  59. package/lib/loop/index.d.ts +6 -2
  60. package/lib/loop/index.js +8 -2
  61. package/lib/loop/plugins/a2ui/A2uiView.js +2 -3
  62. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +23 -3
  63. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +36 -9
  64. package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
  65. package/lib/loop/plugins/agent-capacity/index.js +3 -1
  66. package/lib/loop/plugins/agents/switchable.d.ts +7 -0
  67. package/lib/loop/plugins/agents/switchable.js +91 -88
  68. package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
  69. package/lib/loop/plugins/app-page/AppPage.js +53 -0
  70. package/lib/loop/plugins/app-page/appPageModel.d.ts +180 -0
  71. package/lib/loop/plugins/app-page/appPageModel.js +443 -0
  72. package/lib/loop/plugins/app-page/index.d.ts +8 -0
  73. package/lib/loop/plugins/app-page/index.js +34 -0
  74. package/lib/loop/plugins/canvas-blocks/index.d.ts +63 -0
  75. package/lib/loop/plugins/canvas-blocks/index.js +118 -0
  76. package/lib/loop/plugins/chat/ChatView.js +52 -26
  77. package/lib/loop/plugins/chat/turnState.d.ts +5 -2
  78. package/lib/loop/plugins/chat/turnState.js +2 -1
  79. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
  80. package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
  81. package/lib/loop/plugins/page-layout/index.d.ts +13 -0
  82. package/lib/loop/plugins/page-layout/index.js +10 -1
  83. package/lib/loop/presets.d.ts +12 -0
  84. package/lib/loop/presets.js +5 -3
  85. package/lib/specs/actions.js +9 -0
  86. package/lib/specs/agents/agents.js +20 -4
  87. package/lib/specs/apps.d.ts +10 -1
  88. package/lib/specs/apps.js +1779 -154
  89. package/lib/specs/appspecSchema.js +14 -8
  90. package/lib/specs/models.d.ts +1 -1
  91. package/lib/specs/models.js +8 -8
  92. package/lib/specs/tools.d.ts +1 -0
  93. package/lib/specs/tools.js +19 -0
  94. package/lib/stories/loop/LoopReference.stories.d.ts +21 -0
  95. package/lib/stories/loop/LoopReference.stories.js +31 -0
  96. package/lib/stories/loop/ReferenceScreens.d.ts +53 -0
  97. package/lib/stories/loop/ReferenceScreens.js +256 -0
  98. package/lib/stories/loop/pictures-main.d.ts +1 -0
  99. package/lib/stories/loop/pictures-main.js +64 -0
  100. package/lib/types/agentspecs.d.ts +17 -1
  101. package/lib/types/chat.d.ts +5 -0
  102. package/lib/types/config.d.ts +20 -0
  103. package/package.json +10 -6
  104. package/scripts/check-cloudflare-models.py +2 -2
  105. package/scripts/codegen/generate_agents.py +50 -2
  106. package/scripts/codegen/generate_apps.py +38 -7
  107. package/scripts/codegen/generate_models.py +4 -4
@@ -16,15 +16,30 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
16
16
  * created with the application in its payload — so the runtime reaches only
17
17
  * what the application connects to and enforces its rules before every tool
18
18
  * call — and the application's starters as the openers of the empty chat;
19
- * - the **layout of its kind**: a chat is the conversation alone, with the
20
- * A2UI surfaces its answers draw; the other layouts keep the editors.
19
+ * - its **layout**, as `interface.layout` says (`appLayoutOptions`, LOOP
20
+ * T-07), on the workspace's own `page-layout` plugin: `chat` is the
21
+ * conversation alone, with the A2UI surfaces its answers draw; `page` is
22
+ * the page on a sheet with the composer over it and the conversation in a
23
+ * panel the composer's display modes open; `split` is the conversation and
24
+ * the page side by side, a hairline between them to drag;
25
+ * - its **page**, when it has one (`hasAppPage`: a chat, a widget or a worker
26
+ * with a `page` or `split` layout): its A2UI surface drawn by the
27
+ * `app-page` plugin, in place of the notebook and the document, fed from
28
+ * the conversation and answered through it (`APP_KIND_PATHS`). A
29
+ * decision's page stays the Studio's run page (R-02); its layout keeps the
30
+ * editors;
31
+ * - its **frame**, when the host asks (`frame`): the `window-frame` plugin's
32
+ * window, its title the application's face and name.
21
33
  *
22
34
  * @module loop/apps/AppRenderer
23
35
  */
24
36
  import { useMemo } from 'react';
25
37
  import { defineAgentCapacityPlugin } from '../plugins/agent-capacity';
38
+ import { APP_PAGE_SURFACE, defineAppPagePlugin, hasAppPage, } from '../plugins/app-page';
26
39
  import { LoopEmbed } from '../embed/LoopEmbed';
27
40
  import { dumpAppspec } from './appspec';
41
+ import { defineAppFeedbackPlugin } from './AppFeedback';
42
+ import { keepsFeedback } from './feedback';
28
43
  /** The id of an agent or a Cog, without its version. */
29
44
  export const agentIdOf = (app) => {
30
45
  const reference = app.agent || app.team;
@@ -64,8 +79,75 @@ export function defineAppPlugin(app) {
64
79
  text: starter.label,
65
80
  message: starter.message,
66
81
  })),
82
+ // In the page, the agent is told what a runtime tells it: its own
83
+ // spec's prompt, then the application's instructions; on the
84
+ // application's model when it names one (`loop/apps/agent`).
85
+ instructions: app.instructions,
86
+ model: app.model,
67
87
  });
68
88
  }
89
+ /**
90
+ * The workspace an application's layout asks for (LOOP T-07).
91
+ *
92
+ * - `chat`: the conversation alone — no editor, no page, no strip.
93
+ * - `page`: the page layout — its page (or, without one, the editors) on the
94
+ * sheet; the composer a card over it, starting at the bottom, whose display
95
+ * modes put it in the conversation panel beside, over or in the corner of
96
+ * the page; the current turn under the composer, its copy and dismiss only.
97
+ * - `split`: the page layout's split — the conversation on the left with its
98
+ * composer, the page on the right, a hairline between them to drag.
99
+ *
100
+ * With a page, the page is the one editor, opened, without a strip to swap
101
+ * it; without one (a decision, whose page is the Studio's), the editors stay.
102
+ */
103
+ export function appLayoutOptions(app) {
104
+ const layout = app.interface.layout;
105
+ if (layout === 'chat') {
106
+ return { editors: false, showViewSelector: false };
107
+ }
108
+ const withPage = hasAppPage(app);
109
+ return {
110
+ editors: !withPage,
111
+ showViewSelector: !withPage,
112
+ ...(withPage ? { defaultEditor: APP_PAGE_SURFACE } : {}),
113
+ pageLayout: true,
114
+ pageLayoutArrangement: layout === 'split' ? 'split' : 'page',
115
+ pageLayoutPrompt: 'floating',
116
+ pageLayoutPromptAnchor: 'bottom',
117
+ pageLayoutTurnPanelFooter: 'actions',
118
+ };
119
+ }
120
+ /**
121
+ * What an application's agent is created with on a Datalayer runtime: under
122
+ * the application's id, over AG-UI, with the application's own document —
123
+ * so that the runtime registers it and decides every tool call by its rules
124
+ * (LOOP R-03, R-05), as `loop apps run --cloud` does. One payload for every
125
+ * place it runs: the workspace (`AppRenderer`) and the embed's floating
126
+ * modes (`AppEmbed`).
127
+ */
128
+ export function appDatalayerCreatePayload(app, instance) {
129
+ return {
130
+ // Under the application's id, over AG-UI: what the workspace's
131
+ // chat addresses (`AppRenderer` gives it as `agentId`).
132
+ name: app.id,
133
+ transport: 'ag-ui',
134
+ agent_spec_id: agentIdOf(app),
135
+ app_spec: dumpAppspec(app),
136
+ // Code Mode calls every tool through `execute_code`, which an
137
+ // application without a shell is refused: its tools are called
138
+ // one by one instead, each decided by its rules.
139
+ enable_codemode: Boolean(app.permissions?.computer?.shell),
140
+ ...(instance
141
+ ? {
142
+ app_instance: {
143
+ app_uid: instance.appUid ?? '',
144
+ deployment_uid: instance.deploymentUid ?? '',
145
+ version: instance.version ?? 0,
146
+ },
147
+ }
148
+ : {}),
149
+ };
150
+ }
69
151
  /**
70
152
  * An application in a LOOP workspace: its agent, its starters, its layout.
71
153
  *
@@ -76,8 +158,7 @@ export function defineAppPlugin(app) {
76
158
  /** The agentspec a runtime is allocated with before an application's agent is created on it. */
77
159
  export const DATALAYER_BOOTSTRAP_AGENTSPEC = 'example-simple';
78
160
  const NO_PLUGINS = [];
79
- export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, ...embed }) {
80
- const chatOnly = app.interface.layout === 'chat';
161
+ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, frame = false, ...embed }) {
81
162
  /*
82
163
  * The application plugin, made once per application. `LoopEmbed` rebuilds
83
164
  * its whole reactor when its plugins change, so a new plugin on every
@@ -88,36 +169,33 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, .
88
169
  const appPlugin = useMemo(() => (app.agent ? defineAppPlugin(app) : null),
89
170
  // eslint-disable-next-line react-hooks/exhaustive-deps
90
171
  [source]);
91
- const allPlugins = useMemo(() => (appPlugin ? [appPlugin, ...plugins] : plugins), [appPlugin, plugins]);
172
+ const withPage = hasAppPage(app);
173
+ const pagePlugin = useMemo(() => (withPage ? defineAppPagePlugin(app) : null),
174
+ // eslint-disable-next-line react-hooks/exhaustive-deps
175
+ [source, withPage]);
176
+ /*
177
+ * A thumb and a comment on each answer, kept in its record (LOOP V-18):
178
+ * only for an application whose record keeps feedback.
179
+ */
180
+ const keeps = keepsFeedback(app);
181
+ const feedbackPlugin = useMemo(() => (keeps ? defineAppFeedbackPlugin(app) : null),
182
+ // eslint-disable-next-line react-hooks/exhaustive-deps
183
+ [source, keeps]);
184
+ const allPlugins = useMemo(() => appPlugin
185
+ ? [
186
+ appPlugin,
187
+ ...(pagePlugin ? [pagePlugin] : []),
188
+ ...(feedbackPlugin ? [feedbackPlugin] : []),
189
+ ...plugins,
190
+ ]
191
+ : plugins, [appPlugin, pagePlugin, feedbackPlugin, plugins]);
92
192
  /*
93
193
  * On Datalayer, the application runs on a runtime: allocated with a plain
94
194
  * agentspec, its agent created there with the application's spec — so that
95
195
  * the runtime registers it and decides every tool call by its rules (LOOP
96
196
  * R-03, R-05), as `loop apps run --cloud` does.
97
197
  */
98
- const datalayerCreatePayload = useMemo(() => app.agent
99
- ? {
100
- // Under the application's id, over AG-UI: what the workspace's
101
- // chat addresses (`agentId` below).
102
- name: app.id,
103
- transport: 'ag-ui',
104
- agent_spec_id: agentIdOf(app),
105
- app_spec: dumpAppspec(app),
106
- // Code Mode calls every tool through `execute_code`, which an
107
- // application without a shell is refused: its tools are called
108
- // one by one instead, each decided by its rules.
109
- enable_codemode: Boolean(app.permissions?.computer?.shell),
110
- ...(instance
111
- ? {
112
- app_instance: {
113
- app_uid: instance.appUid ?? '',
114
- deployment_uid: instance.deploymentUid ?? '',
115
- version: instance.version ?? 0,
116
- },
117
- }
118
- : {}),
119
- }
120
- : undefined,
198
+ const datalayerCreatePayload = useMemo(() => (app.agent ? appDatalayerCreatePayload(app, instance) : undefined),
121
199
  // eslint-disable-next-line react-hooks/exhaustive-deps
122
200
  [source, instance?.appUid, instance?.deploymentUid, instance?.version]);
123
201
  if (!appPlugin) {
@@ -129,7 +207,11 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, .
129
207
  , {
130
208
  // The application's own agent: created under its id, so that its
131
209
  // spec is applied to an agent of its own, never to the one it extends.
132
- agentId: app.id, editors: !chatOnly, showViewSelector: !chatOnly, teamPicker: false, showAgentVariants: false, graph: false, pluginsPanel: false, datalayerAgentSpecId: DATALAYER_BOOTSTRAP_AGENTSPEC,
210
+ agentId: app.id, ...appLayoutOptions(app), ...(frame
211
+ ? {
212
+ frameTitle: [app.emoji, app.name].filter(Boolean).join(' '),
213
+ }
214
+ : {}), teamPicker: false, showAgentVariants: false, graph: false, pluginsPanel: false, datalayerAgentSpecId: DATALAYER_BOOTSTRAP_AGENTSPEC,
133
215
  // Where an application runs is decided by its host — the Studio's
134
216
  // Preview, the hosted page — not offered to its user.
135
217
  targetFixed: true,
@@ -0,0 +1,15 @@
1
+ import type { Agentspec } from '../../types/agentspecs';
2
+ /** An agent of the catalogue, or a Cog's agent, by `id` or `id:version`. */
3
+ export declare function resolveAgentspec(reference: string): Agentspec | undefined;
4
+ /**
5
+ * What the agent is told: its spec's system prompt, then what the
6
+ * application tells it on top — as the runtime joins them.
7
+ */
8
+ export declare function agentInstructions(spec: Pick<Agentspec, 'systemPrompt'> | undefined, extra?: string): string | undefined;
9
+ /**
10
+ * Why an agent turned in the page cannot run, or undefined when it can.
11
+ *
12
+ * An agent named by a reference that resolves to nothing would answer with
13
+ * no instructions — bare — which is not the agent that was asked for.
14
+ */
15
+ export declare function inPageAgentRefusal(reference: string | undefined, spec: Agentspec | undefined): string | undefined;
@@ -0,0 +1,42 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The agent an application runs, as the page turns it (LOOP H-01, E-11).
7
+ *
8
+ * On a runtime, an application's agent is its agent's spec — an agent of the
9
+ * catalogue, or a Cog's, which is an agent equipped with Frames, their
10
+ * context already in its system prompt — and the application's own
11
+ * instructions on top of it (`get_library_agent_spec` and the create route
12
+ * in `agent_runtimes/routes/agents.py`). The page turns the same agent in the
13
+ * browser, so it resolves it the same way: an application on a Cog that the
14
+ * page looked up among the agentspecs alone ran with no instructions at all.
15
+ *
16
+ * @module loop/apps/agent
17
+ */
18
+ import { getAgentspecs } from '../../specs/agents';
19
+ import { getCog } from '../../specs/cogs';
20
+ /** An agent of the catalogue, or a Cog's agent, by `id` or `id:version`. */
21
+ export function resolveAgentspec(reference) {
22
+ return getAgentspecs(reference) ?? getCog(reference)?.spec;
23
+ }
24
+ /**
25
+ * What the agent is told: its spec's system prompt, then what the
26
+ * application tells it on top — as the runtime joins them.
27
+ */
28
+ export function agentInstructions(spec, extra) {
29
+ const parts = [spec?.systemPrompt?.trim(), extra?.trim()].filter((part) => Boolean(part));
30
+ return parts.length > 0 ? parts.join('\n\n') : undefined;
31
+ }
32
+ /**
33
+ * Why an agent turned in the page cannot run, or undefined when it can.
34
+ *
35
+ * An agent named by a reference that resolves to nothing would answer with
36
+ * no instructions — bare — which is not the agent that was asked for.
37
+ */
38
+ export function inPageAgentRefusal(reference, spec) {
39
+ return reference && !spec
40
+ ? `There is no agent or Cog named “${reference}” in this page, so it cannot answer here.`
41
+ : undefined;
42
+ }
@@ -105,6 +105,7 @@ export function emptyAppspec(kind = 'chat') {
105
105
  keepFor: DEFAULT_KEEP_FOR,
106
106
  retentionDays: retentionDays(DEFAULT_KEEP_FOR),
107
107
  include: [...DEFAULT_RECORD_INCLUDE],
108
+ suggestTests: false,
108
109
  },
109
110
  checks: { guards: [], gates: [], track: '' },
110
111
  deployment: {},
@@ -243,7 +244,7 @@ function parseDecision(data) {
243
244
  name: text(scenario.name),
244
245
  weights: Object.fromEntries(Object.entries(isData(scenario.weights) ? scenario.weights : {}).filter((entry) => typeof entry[1] === 'number')),
245
246
  })),
246
- judgmentModel: text(data.judgment_model),
247
+ decisionModel: text(data.decision_model),
247
248
  };
248
249
  }
249
250
  function parseDeployment(data) {
@@ -359,6 +360,7 @@ export function parseAppspec(document) {
359
360
  include: Array.isArray(record.include)
360
361
  ? texts(record.include)
361
362
  : [...DEFAULT_RECORD_INCLUDE],
363
+ suggestTests: record.suggest_tests === true,
362
364
  },
363
365
  checks: {
364
366
  guards: texts(checks.guards),
@@ -493,7 +495,7 @@ function dumpDecision(decision) {
493
495
  .text('name', scenario.name, '\u0000')
494
496
  // By criterion name, in alphabetical order: the spec declares no order.
495
497
  .part('weights', sorted(scenario.weights)).data))
496
- .text('judgment_model', decision.judgmentModel).data;
498
+ .text('decision_model', decision.decisionModel).data;
497
499
  }
498
500
  /**
499
501
  * An application as the document its file holds.
@@ -554,7 +556,8 @@ export function dumpAppspec(app) {
554
556
  }))).data)
555
557
  .part('record', new Writer()
556
558
  .text('keep_for', app.record.keepFor, DEFAULT_KEEP_FOR)
557
- .list('include', app.record.include, DEFAULT_RECORD_INCLUDE).data)
559
+ .list('include', app.record.include, DEFAULT_RECORD_INCLUDE)
560
+ .value('suggest_tests', app.record.suggestTests, false).data)
558
561
  .part('checks', new Writer()
559
562
  .list('guards', app.checks.guards)
560
563
  .list('gates', app.checks.gates)
@@ -16,8 +16,16 @@ export interface AppCheck {
16
16
  export declare const componentNamed: (name: string) => ComponentSpec | undefined;
17
17
  /** The Guards the runtime executes (agent-runtimes `loop/apps/guards.py`). */
18
18
  export declare const EXECUTED_GUARDS: readonly string[];
19
+ /**
20
+ * Where an application is checked: the plugins its organization has turned
21
+ * off (catalogue ids, `plugins_off` in IAM), none when it has decided nothing
22
+ * or the checks run with no organization.
23
+ */
24
+ export interface CheckContext {
25
+ pluginsOff: readonly string[];
26
+ }
19
27
  /** The instant checks of an application as an editor holds it. */
20
- export declare function checkApp(app: AppSpec, read?: string[]): AppCheck;
28
+ export declare function checkApp(app: AppSpec, read?: string[], context?: CheckContext): AppCheck;
21
29
  /**
22
30
  * What the document says in a shape the spec refuses — a list where a list is
23
31
  * not, a word where a choice is — which the tolerant reader replaces with a
@@ -25,4 +33,4 @@ export declare function checkApp(app: AppSpec, read?: string[]): AppCheck;
25
33
  */
26
34
  export declare function documentShapeProblems(document: unknown): string[];
27
35
  /** The instant checks of an application's document, what reading it found included. */
28
- export declare function checkAppspec(document: unknown): AppCheck;
36
+ export declare function checkAppspec(document: unknown, context?: CheckContext): AppCheck;
@@ -38,6 +38,7 @@ import { getSkillSpec } from '../../specs/skills';
38
38
  import { getTeamSpec } from '../../specs/teams';
39
39
  import { getToolSpec } from '../../specs/tools';
40
40
  import { getTrack } from '../../specs/tracks';
41
+ import { pluginsOffSetupNotes } from '../plugins/canvas-blocks';
41
42
  import { parseAppspec } from './appspec';
42
43
  import { classesOf, splitRef, toolBehaviours } from './rules';
43
44
  export const NOT_READY = 'Not ready';
@@ -192,14 +193,14 @@ function referenceProblems(app) {
192
193
  problems.push(`The surface's “${String(node.id)}” is a “${String(node.component)}”, which the catalog does not have.`);
193
194
  }
194
195
  }
195
- const judge = app.decision?.judgmentModel;
196
- if (judge) {
197
- const model = getModel(judge);
196
+ const decider = app.decision?.decisionModel;
197
+ if (decider) {
198
+ const model = getModel(decider);
198
199
  if (!model) {
199
- problems.push(`There is no model named “${judge}” to judge with.`);
200
+ problems.push(`There is no model named “${decider}” to decide with.`);
200
201
  }
201
- else if (!(model.capabilities ?? []).includes('judgments')) {
202
- problems.push(`The model “${judge}” does not answer typed judgments.`);
202
+ else if (!(model.capabilities ?? []).includes('decisions')) {
203
+ problems.push(`The model “${decider}” does not answer typed decisions.`);
203
204
  }
204
205
  }
205
206
  const run = new Set(app.checks.guards.map(idOf));
@@ -336,10 +337,16 @@ function setupNotes(app) {
336
337
  note('notification', ref, getNotificationSpec(idOf(ref)));
337
338
  return setup;
338
339
  }
340
+ const NO_ORGANIZATION = { pluginsOff: [] };
339
341
  /** The instant checks of an application as an editor holds it. */
340
- export function checkApp(app, read = []) {
342
+ export function checkApp(app, read = [], context = NO_ORGANIZATION) {
341
343
  const problems = [...read, ...shapeProblems(app), ...referenceProblems(app)];
342
- const setup = setupNotes(app);
344
+ // A block its page uses from a plugin its organization turned off is said
345
+ // with the rest of what is not enabled today (C-12).
346
+ const setup = [
347
+ ...setupNotes(app),
348
+ ...pluginsOffSetupNotes(app, context.pluginsOff),
349
+ ];
343
350
  if (problems.length > 0) {
344
351
  return { verdict: NOT_READY, problems, attention: [], setup };
345
352
  }
@@ -540,6 +547,10 @@ export function documentShapeProblems(document) {
540
547
  else
541
548
  record.include.forEach((item, index) => oneOf(item, ENUMS.record, `record.include.${index}`));
542
549
  }
550
+ if (record.suggest_tests !== undefined &&
551
+ typeof record.suggest_tests !== 'boolean') {
552
+ at('record.suggest_tests', 'is true or false');
553
+ }
543
554
  });
544
555
  mapping(d.checks, 'checks', checks => {
545
556
  texts(checks.guards, 'checks.guards');
@@ -596,7 +607,7 @@ export function documentShapeProblems(document) {
596
607
  return problems;
597
608
  }
598
609
  /** The instant checks of an application's document, what reading it found included. */
599
- export function checkAppspec(document) {
610
+ export function checkAppspec(document, context = NO_ORGANIZATION) {
600
611
  const { app, problems } = parseAppspec(document);
601
- return checkApp(app, [...problems, ...documentShapeProblems(document)]);
612
+ return checkApp(app, [...problems, ...documentShapeProblems(document)], context);
602
613
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Feedback from the people who use an application (LOOP V-18): a thumb up or
3
+ * down on an answer, and a comment, kept in the application's record.
4
+ *
5
+ * Kept only when the application's `record.include` names `feedback` — its
6
+ * builder says what its record keeps, and the chat offers nothing it would
7
+ * not keep. Sent to the runtime the conversation runs on
8
+ * (`POST /api/v1/apps/feedback`), which writes it through the recorder of
9
+ * that conversation, under its session: the conversation's AG-UI thread.
10
+ *
11
+ * Pure: no React; the network is the `fetch` it is handed.
12
+ *
13
+ * @module loop/apps/feedback
14
+ */
15
+ import type { AppSpec } from '../../types/agentspecs';
16
+ /** The longest comment kept, as the runtime holds it (`COMMENT_LIMIT`). */
17
+ export declare const FEEDBACK_COMMENT_LIMIT = 2000;
18
+ /** What a person says of an answer. */
19
+ export type Feedback = {
20
+ /** The conversation, as the runtime knows it. */
21
+ session: string;
22
+ liked: boolean;
23
+ comment: string;
24
+ };
25
+ /** What the feedback strip says, in a person's words. */
26
+ export declare const FEEDBACK_WORDS: {
27
+ readonly ask: "Was this answer useful?";
28
+ readonly liked: "Useful";
29
+ readonly disliked: "Not useful";
30
+ readonly comment: "Anything to add? (optional)";
31
+ readonly send: "Send";
32
+ readonly kept: (app: string) => string;
33
+ readonly where: (app: string) => string;
34
+ readonly tooLong: "A comment is at most 2000 characters.";
35
+ readonly noSession: "This conversation has no name the application knows yet: send a message first.";
36
+ };
37
+ /** Whether an application keeps feedback: its record names it. */
38
+ export declare const keepsFeedback: (app: Pick<AppSpec, "record">) => boolean;
39
+ /** Where feedback is sent, on the runtime an application runs on. */
40
+ export declare const feedbackEndpoint: (agentBaseUrl: string) => string;
41
+ /** Why feedback cannot be sent as it is; nothing when it can. */
42
+ export declare function feedbackProblems(feedback: Feedback): string[];
43
+ /**
44
+ * Send feedback to the runtime, and say what it kept: the entry, in a
45
+ * sentence. A refusal throws the runtime's own sentence.
46
+ */
47
+ export declare function sendFeedback(feedback: Feedback, { agentBaseUrl, token, fetcher, }: {
48
+ agentBaseUrl: string;
49
+ token?: string | null;
50
+ fetcher?: typeof fetch;
51
+ }): Promise<string>;
@@ -0,0 +1,61 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** The longest comment kept, as the runtime holds it (`COMMENT_LIMIT`). */
6
+ export const FEEDBACK_COMMENT_LIMIT = 2000;
7
+ /** What the feedback strip says, in a person's words. */
8
+ export const FEEDBACK_WORDS = {
9
+ ask: 'Was this answer useful?',
10
+ liked: 'Useful',
11
+ disliked: 'Not useful',
12
+ comment: 'Anything to add? (optional)',
13
+ send: 'Send',
14
+ kept: (app) => `Thank you. Kept in ${app}’s record.`,
15
+ where: (app) => `What you say is kept with this conversation in ${app}’s record, for its builder to read.`,
16
+ tooLong: `A comment is at most ${FEEDBACK_COMMENT_LIMIT} characters.`,
17
+ noSession: 'This conversation has no name the application knows yet: send a message first.',
18
+ };
19
+ /** Whether an application keeps feedback: its record names it. */
20
+ export const keepsFeedback = (app) => (app.record?.include ?? []).includes('feedback');
21
+ /** Where feedback is sent, on the runtime an application runs on. */
22
+ export const feedbackEndpoint = (agentBaseUrl) => `${agentBaseUrl.replace(/\/+$/, '')}/api/v1/apps/feedback`;
23
+ /** Why feedback cannot be sent as it is; nothing when it can. */
24
+ export function feedbackProblems(feedback) {
25
+ const problems = [];
26
+ if (!feedback.session.trim()) {
27
+ problems.push(FEEDBACK_WORDS.noSession);
28
+ }
29
+ if (feedback.comment.trim().length > FEEDBACK_COMMENT_LIMIT) {
30
+ problems.push(FEEDBACK_WORDS.tooLong);
31
+ }
32
+ return problems;
33
+ }
34
+ /**
35
+ * Send feedback to the runtime, and say what it kept: the entry, in a
36
+ * sentence. A refusal throws the runtime's own sentence.
37
+ */
38
+ export async function sendFeedback(feedback, { agentBaseUrl, token, fetcher = fetch, }) {
39
+ const problems = feedbackProblems(feedback);
40
+ if (problems.length) {
41
+ throw new Error(problems.join(' '));
42
+ }
43
+ const response = await fetcher(feedbackEndpoint(agentBaseUrl), {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
48
+ },
49
+ body: JSON.stringify({
50
+ session: feedback.session,
51
+ liked: feedback.liked,
52
+ comment: feedback.comment.trim(),
53
+ }),
54
+ });
55
+ const body = (await response.json().catch(() => ({})));
56
+ if (!response.ok) {
57
+ const detail = typeof body.detail === 'string' ? body.detail : `${response.status}`;
58
+ throw new Error(`The feedback was not kept: ${detail}`);
59
+ }
60
+ return String(body.summary ?? '');
61
+ }
@@ -3,8 +3,11 @@
3
3
  *
4
4
  * @module loop/apps
5
5
  */
6
+ export * from './agent';
7
+ export * from './AppFeedback';
6
8
  export * from './AppRenderer';
7
9
  export * from './appspec';
8
10
  export * from './checks';
11
+ export * from './feedback';
9
12
  export * from './rules';
10
13
  export * from './yaml';
@@ -7,8 +7,11 @@
7
7
  *
8
8
  * @module loop/apps
9
9
  */
10
+ export * from './agent';
11
+ export * from './AppFeedback';
10
12
  export * from './AppRenderer';
11
13
  export * from './appspec';
12
14
  export * from './checks';
15
+ export * from './feedback';
13
16
  export * from './rules';
14
17
  export * from './yaml';
@@ -39,6 +39,20 @@ export declare function classesOf(ref: string, args?: ToolArguments): ActionClas
39
39
  export declare const isReadOnly: (classes: ActionClass[]) => boolean;
40
40
  /** The most restricted of several behaviours. */
41
41
  export declare const strictest: (behaviours: AppBehaviour[]) => AppBehaviour;
42
+ /**
43
+ * Whether the application is given a tool of a server: its connection's
44
+ * level (LOOP U-15). A connection gives the tools of its server it reaches
45
+ * (`only`); one that only reads gives only the tools that only read — a tool
46
+ * nobody classed is taken to write, so a read connection does not give it.
47
+ * A tool of a server the application is not connected to is not given.
48
+ *
49
+ * The runtime gives its agent only these (`AppRulesCapability.prepare_tools`,
50
+ * through `agent_runtimes.loop.apps.rules.gives`, its Python twin).
51
+ *
52
+ * @throws When `tool` names no server: a catalogue tool is given by the
53
+ * spec's `tools`, not by a connection.
54
+ */
55
+ export declare function gives(app: Pick<AppSpec, 'connections'>, tool: string): boolean;
42
56
  /** What `behaviourFor` may be told about the call. */
43
57
  export interface BehaviourOptions {
44
58
  /** The arguments of the call; without them the decision is for the worst the tool can do. */
@@ -161,6 +161,30 @@ export const strictest = (behaviours) => behaviours.reduce((worst, behaviour) =>
161
161
  const connectionTo = (app, server) => app.connections.find(connection => idOf(connection.server) === server);
162
162
  const reaches = (connection, tool) => connection.only.length === 0 ||
163
163
  connection.only.some(pattern => matchesPattern(tool, pattern));
164
+ /**
165
+ * Whether the application is given a tool of a server: its connection's
166
+ * level (LOOP U-15). A connection gives the tools of its server it reaches
167
+ * (`only`); one that only reads gives only the tools that only read — a tool
168
+ * nobody classed is taken to write, so a read connection does not give it.
169
+ * A tool of a server the application is not connected to is not given.
170
+ *
171
+ * The runtime gives its agent only these (`AppRulesCapability.prepare_tools`,
172
+ * through `agent_runtimes.loop.apps.rules.gives`, its Python twin).
173
+ *
174
+ * @throws When `tool` names no server: a catalogue tool is given by the
175
+ * spec's `tools`, not by a connection.
176
+ */
177
+ export function gives(app, tool) {
178
+ const [server, name] = splitRef(tool);
179
+ if (server === undefined) {
180
+ throw new Error(`\`${tool}\` is not a tool of a server (\`server.tool\`).`);
181
+ }
182
+ const connection = connectionTo(app, server);
183
+ if (!connection || !reaches(connection, name)) {
184
+ return false;
185
+ }
186
+ return (connection.access !== 'read' || isReadOnly(classesOf(`${server}.${name}`)));
187
+ }
164
188
  const isClass = (target) => Object.prototype.hasOwnProperty.call(DEFAULT_BEHAVIOURS, target);
165
189
  /** What a rule applies to, versions aside. */
166
190
  const normal = (target) => {
@@ -0,0 +1,16 @@
1
+ import type { ComponentSpec } from '../../types/agentspecs';
2
+ /**
3
+ * A block the Canvas may place (LOOP C-12): one component of the catalog of
4
+ * visual components (C-13), contributed by the plugin of the UI plugin that
5
+ * renders it. The Canvas's palette is what the enabled plugins contribute
6
+ * here, and nothing else: a plugin disabled takes its blocks off it, and an
7
+ * application that uses one of them says so in its setup notes.
8
+ */
9
+ export type CanvasBlockContribution = {
10
+ /** The name a surface gives it, the component's id (e.g. 'Table'). */
11
+ id: string;
12
+ /** The UI plugin of the catalogue that renders it (e.g. 'a2ui'). */
13
+ uiPlugin: string;
14
+ component: ComponentSpec;
15
+ };
16
+ export declare const LoopCanvasBlock: import("@datalayer/reactor").ContributionPoint<CanvasBlockContribution>;
@@ -0,0 +1,13 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The Canvas's palette as a contribution point (LOOP C-12), re-exported by
7
+ * `loop/core`: kept apart so that reading the palette loads the reactor and
8
+ * the catalogue, and nothing of the workspace.
9
+ *
10
+ * @module loop/core/canvasBlocks
11
+ */
12
+ import { defineContributionPoint } from '@datalayer/reactor';
13
+ export const LoopCanvasBlock = defineContributionPoint('loop.canvas.block');
@@ -483,6 +483,14 @@ export type AgentBlueprintContribution = {
483
483
  specId?: string;
484
484
  /** Extra fields merged into the server's create-agent payload. */
485
485
  createPayload?: Record<string, unknown>;
486
+ /**
487
+ * What the agent is told on top of its spec's system prompt — an
488
+ * application's instructions — when the page turns it, as a runtime
489
+ * appends them.
490
+ */
491
+ instructions?: string;
492
+ /** The model it runs on in the page, in place of its spec's. */
493
+ model?: string;
486
494
  };
487
495
  export declare const LoopAgentBlueprint: import("@datalayer/reactor").ContributionPoint<AgentBlueprintContribution>;
488
496
  /** One conversation opener a plugin offers on the empty chat. */
@@ -518,6 +526,7 @@ export type AssistantCharacterContribution = {
518
526
  character: AssistantCharacter | AssistantCharacterData;
519
527
  };
520
528
  export declare const LoopAssistantCharacter: import("@datalayer/reactor").ContributionPoint<AssistantCharacterContribution>;
529
+ export { LoopCanvasBlock, type CanvasBlockContribution } from './canvasBlocks';
521
530
  /**
522
531
  * The per-example chat extras a host feeds the loop's conversation live.
523
532
  *
@@ -927,6 +936,12 @@ export type ChatTurnSnapshot = {
927
936
  user?: string;
928
937
  /** What the agent has said so far. */
929
938
  assistant?: string;
939
+ /**
940
+ * The conversation the turn belongs to, as the runtime knows it (AG-UI's
941
+ * thread) — what an application's record keeps its session under, and
942
+ * what feedback on it names (LOOP V-18). Absent when the chat does not say.
943
+ */
944
+ thread?: string;
930
945
  status: ChatTurnStatus;
931
946
  /**
932
947
  * The context window as the agent last reported it — what a turn footer
@@ -142,6 +142,9 @@ export const LoopChatHeader = defineContributionPoint('loop.chat.header');
142
142
  export const LoopAgentBlueprint = defineContributionPoint('loop.agent.blueprint');
143
143
  export const LoopChatSuggestion = defineContributionPoint('loop.chat.suggestion');
144
144
  export const LoopAssistantCharacter = defineContributionPoint('loop.assistant.character');
145
+ // A block the Canvas may place (LOOP C-12), in a module of its own so that
146
+ // the palette is read without the rest of the contracts.
147
+ export { LoopCanvasBlock } from './canvasBlocks';
145
148
  export const LoopChatExtras = defineContributionPoint('loop.chat.extras');
146
149
  export const LoopOpeningNotebook = defineContributionPoint('loop.notebook.opening');
147
150
  export const LoopOpeningDocument = defineContributionPoint('loop.document.opening');