@datalayer/agent-runtimes 1.3.80 → 1.3.81

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 (43) hide show
  1. package/lib/chat/assistant/AssistantStage.d.ts +9 -4
  2. package/lib/chat/assistant/AssistantStage.js +18 -6
  3. package/lib/chat/assistant/SpriteCharacter.d.ts +25 -3
  4. package/lib/chat/assistant/SpriteCharacter.js +68 -3
  5. package/lib/chat/assistant/formats/acf.d.ts +56 -0
  6. package/lib/chat/assistant/formats/acf.js +317 -0
  7. package/lib/chat/assistant/formats/acs.d.ts +85 -7
  8. package/lib/chat/assistant/formats/acs.js +111 -39
  9. package/lib/chat/assistant/formats/files.d.ts +6 -2
  10. package/lib/chat/assistant/formats/files.js +18 -4
  11. package/lib/chat/assistant/formats/index.d.ts +6 -3
  12. package/lib/chat/assistant/formats/index.js +4 -2
  13. package/lib/chat/assistant/formats/types.d.ts +14 -0
  14. package/lib/chat/assistant/state.d.ts +2 -0
  15. package/lib/chat/assistant/state.js +9 -0
  16. package/lib/chat/base/ChatBase.js +7 -0
  17. package/lib/chat/base/loopStep.d.ts +40 -0
  18. package/lib/chat/base/loopStep.js +73 -0
  19. package/lib/loop/apps/appspec.js +32 -2
  20. package/lib/loop/apps/checks.js +53 -0
  21. package/lib/loop/apps/hostTools.d.ts +17 -0
  22. package/lib/loop/apps/hostTools.js +20 -0
  23. package/lib/loop/apps/rules.d.ts +22 -1
  24. package/lib/loop/apps/rules.js +49 -1
  25. package/lib/loop/embed/AppEmbed.d.ts +57 -2
  26. package/lib/loop/embed/AppEmbed.js +71 -8
  27. package/lib/loop/embed/element.js +40 -1
  28. package/lib/loop/embed/hostBridge.d.ts +79 -0
  29. package/lib/loop/embed/hostBridge.js +104 -0
  30. package/lib/loop/index.d.ts +2 -1
  31. package/lib/loop/index.js +3 -1
  32. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.d.ts +5 -2
  33. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +7 -1
  34. package/lib/loop/plugins/a2ui-surface/index.d.ts +1 -1
  35. package/lib/loop/plugins/a2ui-surface/index.js +4 -4
  36. package/lib/loop/plugins/a2ui-surface/toolResult.d.ts +27 -0
  37. package/lib/loop/plugins/a2ui-surface/toolResult.js +22 -0
  38. package/lib/specs/agents/agents.js +6 -25
  39. package/lib/specs/apps.js +22 -8
  40. package/lib/specs/appspecSchema.js +67 -0
  41. package/lib/types/agentspecs.d.ts +28 -0
  42. package/package.json +1 -1
  43. package/scripts/codegen/generate_apps.py +7 -0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A step of an application's code, shown in the conversation as it runs
3
+ * (LOOP P-16): the chain of thought.
4
+ *
5
+ * AG-UI's `STEP_STARTED` and `STEP_FINISHED` carry a step's name only; the
6
+ * session API says the step whole beside them, with a `CUSTOM` event named
7
+ * `loop.step` — `{id, name, kind, parent_id, input, output, error,
8
+ * started_at, ended_at}` — which the AG-UI adapter hands on as an
9
+ * `activity`. A step is drawn as the chat draws a tool call: one row,
10
+ * collapsed, its input and output inside, running until it ends; a nested
11
+ * step says the step it is in.
12
+ */
13
+ import type { DisplayItem, ToolCallMessage } from '../../types/chat';
14
+ /** The `CUSTOM` event's name. */
15
+ export declare const LOOP_STEP = "loop.step";
16
+ /** A step, as the event says it. */
17
+ export type LoopStep = {
18
+ id: string;
19
+ name: string;
20
+ kind: string;
21
+ parentId: string | null;
22
+ input: unknown;
23
+ output: unknown;
24
+ error: string;
25
+ ended: boolean;
26
+ };
27
+ /** The step an activity says, or `null` when it says none. */
28
+ export declare function loopStepOf(activity: {
29
+ type: string;
30
+ data: unknown;
31
+ } | undefined): LoopStep | null;
32
+ /** The id of the row a step is drawn as. */
33
+ export declare const stepCallId: (id: string) => string;
34
+ /** A step as the row the chat draws a tool call as. */
35
+ export declare function stepItem(step: LoopStep, inside?: string): ToolCallMessage;
36
+ /**
37
+ * The conversation with the step drawn: its row changed in place when it is
38
+ * there (a step ending), else added after the rest.
39
+ */
40
+ export declare function withLoopStep(items: DisplayItem[], step: LoopStep): DisplayItem[];
@@ -0,0 +1,73 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** The `CUSTOM` event's name. */
6
+ export const LOOP_STEP = 'loop.step';
7
+ /** The step an activity says, or `null` when it says none. */
8
+ export function loopStepOf(activity) {
9
+ if (!activity || activity.type !== LOOP_STEP)
10
+ return null;
11
+ const data = activity.data;
12
+ if (!data ||
13
+ typeof data.id !== 'string' ||
14
+ !data.id ||
15
+ typeof data.name !== 'string') {
16
+ return null;
17
+ }
18
+ return {
19
+ id: data.id,
20
+ name: data.name,
21
+ kind: typeof data.kind === 'string' ? data.kind : 'run',
22
+ parentId: typeof data.parent_id === 'string' && data.parent_id
23
+ ? data.parent_id
24
+ : null,
25
+ input: data.input ?? null,
26
+ output: data.output ?? null,
27
+ error: typeof data.error === 'string' ? data.error : '',
28
+ ended: typeof data.ended_at === 'string' && data.ended_at !== '',
29
+ };
30
+ }
31
+ /** The id of the row a step is drawn as. */
32
+ export const stepCallId = (id) => `step-${id}`;
33
+ /** A step as the row the chat draws a tool call as. */
34
+ export function stepItem(step, inside) {
35
+ const callId = stepCallId(step.id);
36
+ const kind = `${step.kind} step`;
37
+ return {
38
+ id: callId,
39
+ type: 'tool-call',
40
+ toolCallId: callId,
41
+ toolName: step.name,
42
+ args: {
43
+ kind: step.kind,
44
+ ...(step.input !== null ? { input: step.input } : {}),
45
+ },
46
+ ...(step.ended && !step.error && step.output !== null
47
+ ? { result: step.output }
48
+ : {}),
49
+ status: !step.ended ? 'executing' : step.error ? 'error' : 'complete',
50
+ ...(step.error ? { error: step.error } : {}),
51
+ summary: inside ? `${kind}, inside ${inside}` : kind,
52
+ };
53
+ }
54
+ /**
55
+ * The conversation with the step drawn: its row changed in place when it is
56
+ * there (a step ending), else added after the rest.
57
+ */
58
+ export function withLoopStep(items, step) {
59
+ const parent = step.parentId
60
+ ? items.find(item => 'toolCallId' in item &&
61
+ item.toolCallId === stepCallId(step.parentId))
62
+ : undefined;
63
+ const row = stepItem(step, parent && 'toolName' in parent ? parent.toolName : undefined);
64
+ let found = false;
65
+ const changed = items.map(item => {
66
+ if ('toolCallId' in item && item.toolCallId === row.toolCallId) {
67
+ found = true;
68
+ return row;
69
+ }
70
+ return item;
71
+ });
72
+ return found ? changed : [...items, row];
73
+ }
@@ -58,6 +58,8 @@ const DEFAULT_READY_AT = 0.8;
58
58
  const DEFAULT_PROTOCOL = 'a2ui/v0.9';
59
59
  const DEFAULT_ICON = 'apps';
60
60
  const DEFAULT_VERSION = '0.0.1';
61
+ /** A host function's arguments when it names none: an empty object. */
62
+ const NO_HOST_PARAMETERS = { type: 'object', properties: {} };
61
63
  const isData = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
62
64
  const text = (value, otherwise = '') => typeof value === 'string' ? value : otherwise;
63
65
  const flag = (value, otherwise) => typeof value === 'boolean' ? value : otherwise;
@@ -290,12 +292,27 @@ function parseDeployment(data) {
290
292
  deployment.hosted = {
291
293
  visibility: oneOf(data.hosted.visibility, ['private', 'invited', 'organization', 'link', 'public'], 'private'),
292
294
  slug: text(data.hosted.slug),
295
+ ...(data.hosted.character_alone === true ? { characterAlone: true } : {}),
293
296
  };
294
297
  }
295
298
  if (isData(data.embedded)) {
296
299
  deployment.embedded = {
297
300
  mode: oneOf(data.embedded.mode, APP_EMBED_MODES, 'inline'),
298
301
  origins: texts(data.embedded.origins),
302
+ ...(isData(data.embedded.host)
303
+ ? {
304
+ host: {
305
+ context: texts(data.embedded.host.context),
306
+ functions: records(data.embedded.host.functions).map(fn => ({
307
+ name: text(fn.name),
308
+ description: text(fn.description),
309
+ parameters: isData(fn.parameters)
310
+ ? fn.parameters
311
+ : { ...NO_HOST_PARAMETERS },
312
+ })),
313
+ },
314
+ }
315
+ : {}),
299
316
  };
300
317
  }
301
318
  return deployment;
@@ -628,11 +645,24 @@ export function dumpAppspec(app) {
628
645
  .present('hosted', app.deployment.hosted &&
629
646
  new Writer()
630
647
  .value('visibility', app.deployment.hosted.visibility, 'private')
631
- .text('slug', app.deployment.hosted.slug).data)
648
+ .text('slug', app.deployment.hosted.slug)
649
+ .value('character_alone', app.deployment.hosted.characterAlone === true, false).data)
632
650
  .present('embedded', app.deployment.embedded &&
633
651
  new Writer()
634
652
  .value('mode', app.deployment.embedded.mode, 'inline')
635
- .list('origins', app.deployment.embedded.origins).data).data)
653
+ .list('origins', app.deployment.embedded.origins)
654
+ .present('host', app.deployment.embedded.host &&
655
+ new Writer()
656
+ .list('context', app.deployment.embedded.host.context)
657
+ .list('functions', app.deployment.embedded.host.functions.map(fn => ({
658
+ name: fn.name,
659
+ description: fn.description,
660
+ // No arguments is what it is unless said.
661
+ ...(JSON.stringify(fn.parameters) ===
662
+ JSON.stringify(NO_HOST_PARAMETERS)
663
+ ? {}
664
+ : { parameters: fn.parameters }),
665
+ }))).data).data).data)
636
666
  .text('goal', app.goal)
637
667
  .list('triggers', app.triggers.map(trigger => new Writer()
638
668
  .text('type', trigger.type, '\u0000')
@@ -41,6 +41,7 @@ import { getTrack } from '../../specs/tracks';
41
41
  import { pluginsOffSetupNotes } from '../plugins/canvas-blocks';
42
42
  import { isAssistantCharacterId, parseAppspec } from './appspec';
43
43
  import { classesOf, splitRef, toolBehaviours } from './rules';
44
+ import { HOST_NAME, hostToolsOf } from './hostTools';
44
45
  export const NOT_READY = 'Not ready';
45
46
  export const NEEDS_ATTENTION = 'Needs attention';
46
47
  export const PASSES = 'Passes the instant checks';
@@ -110,6 +111,40 @@ function shapeProblems(app) {
110
111
  else if (app.triggers.length > 0) {
111
112
  problems.push(`A ${app.kind} application starts when somebody opens it: \`triggers\` are a worker's.`);
112
113
  }
114
+ // What the host page passes and offers (D-10): named once each, in words
115
+ // its agent's tools can carry, each function saying what it does.
116
+ const host = app.deployment.embedded?.host;
117
+ if (host) {
118
+ for (const name of host.context) {
119
+ if (!HOST_NAME.test(name)) {
120
+ problems.push(`Cannot use “${name}” as a value of the host: lower-case letters, digits and \`_\`.`);
121
+ }
122
+ }
123
+ if (new Set(host.context).size !== host.context.length) {
124
+ problems.push('The host’s values are named once each.');
125
+ }
126
+ for (const fn of host.functions) {
127
+ if (!HOST_NAME.test(fn.name)) {
128
+ problems.push(`Cannot use “${fn.name}” as a host function: lower-case letters, digits and \`_\`.`);
129
+ }
130
+ if (!fn.description.trim()) {
131
+ problems.push(`The host function “${fn.name}” says what it does.`);
132
+ }
133
+ if (fn.parameters.type !== 'object') {
134
+ problems.push(`The host function “${fn.name}” takes its arguments as a JSON Schema of \`type: object\`.`);
135
+ }
136
+ }
137
+ const names = host.functions.map(fn => fn.name);
138
+ if (new Set(names).size !== names.length) {
139
+ problems.push('The host’s functions are named once each.');
140
+ }
141
+ const named = new Set(app.rules.flatMap(rule => rule.appliesTo));
142
+ for (const tool of hostToolsOf(host)) {
143
+ if (!named.has(tool)) {
144
+ problems.push(`No rule names “${tool}”, which the host page offers it: it is left to the person until a rule decides it.`);
145
+ }
146
+ }
147
+ }
113
148
  const servers = app.connections.map(connection => idOf(connection.server));
114
149
  if (new Set(servers).size !== servers.length) {
115
150
  problems.push('The application connects to the same server twice.');
@@ -274,12 +309,16 @@ function referenceProblems(app, organizationFrames) {
274
309
  problems.push(`The application may write through “${connection.server}”, whose tools nobody has classed: every one of them is left to the person until they are.`);
275
310
  }
276
311
  }
312
+ // The tools of the host page are the application's own (D-10).
313
+ const hostTools = hostToolsOf(app.deployment.embedded?.host);
277
314
  for (const rule of app.rules) {
278
315
  for (const target of rule.appliesTo) {
279
316
  if (CLASS_NAMES.has(target))
280
317
  continue;
281
318
  const [server, name] = splitRef(target);
282
319
  if (server === undefined) {
320
+ if (hostTools.includes(target))
321
+ continue;
283
322
  if (!getBackendToolSpec(name)) {
284
323
  problems.push(`The rule “${rule.action}” names the tool “${target}”, which the catalogue does not have.`);
285
324
  }
@@ -628,6 +667,10 @@ export function documentShapeProblems(document) {
628
667
  !/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(hosted.slug)))) {
629
668
  at('deployment.hosted.slug', 'is lower-case letters, digits and hyphens');
630
669
  }
670
+ if (hosted.character_alone !== undefined &&
671
+ typeof hosted.character_alone !== 'boolean') {
672
+ at('deployment.hosted.character_alone', 'is true or false');
673
+ }
631
674
  });
632
675
  mapping(deployment.embedded, 'deployment.embedded', embedded => {
633
676
  oneOf(embedded.mode, ENUMS.mode, 'deployment.embedded.mode');
@@ -640,6 +683,16 @@ export function documentShapeProblems(document) {
640
683
  }
641
684
  });
642
685
  }
686
+ mapping(embedded.host, 'deployment.embedded.host', host => {
687
+ texts(host.context, 'deployment.embedded.host.context');
688
+ records(host.functions, 'deployment.embedded.host.functions', (item, where) => {
689
+ required(item, 'name', where);
690
+ required(item, 'description', where);
691
+ text(item.name, `${where}.name`);
692
+ text(item.description, `${where}.description`);
693
+ mapping(item.parameters, `${where}.parameters`, () => undefined);
694
+ });
695
+ });
643
696
  });
644
697
  });
645
698
  records(d.triggers, 'triggers', (item, where) => {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The tools an embedded application is given for its host page (LOOP D-10),
3
+ * by name: `host_context` for the values the page passes, `host_<name>` for
4
+ * each function it offers. Apart from `embed/hostBridge`, which calls the
5
+ * page, so that the checks read them as a pure module.
6
+ *
7
+ * @module loop/apps/hostTools
8
+ */
9
+ import type { AppHostBridgeSpec } from '../../types/agentspecs';
10
+ /** The tool its agent reads what the page passes it with. */
11
+ export declare const HOST_CONTEXT_TOOL = "host_context";
12
+ /** The tool its agent calls a function of the page with. */
13
+ export declare const hostTool: (name: string) => string;
14
+ /** How a value or a function of the host is named. */
15
+ export declare const HOST_NAME: RegExp;
16
+ /** The tools its agent is given for the host. */
17
+ export declare function hostToolsOf(bridge: AppHostBridgeSpec | undefined): string[];
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** The tool its agent reads what the page passes it with. */
6
+ export const HOST_CONTEXT_TOOL = 'host_context';
7
+ /** The tool its agent calls a function of the page with. */
8
+ export const hostTool = (name) => `host_${name}`;
9
+ /** How a value or a function of the host is named. */
10
+ export const HOST_NAME = /^[a-z][a-z0-9_]{0,62}$/;
11
+ /** The tools its agent is given for the host. */
12
+ export function hostToolsOf(bridge) {
13
+ if (!bridge) {
14
+ return [];
15
+ }
16
+ return [
17
+ ...(bridge.context.length > 0 ? [HOST_CONTEXT_TOOL] : []),
18
+ ...bridge.functions.map(fn => hostTool(fn.name)),
19
+ ];
20
+ }
@@ -28,11 +28,32 @@ export declare function matchesPattern(name: string, pattern: string): boolean;
28
28
  export declare const isComparable: (value: unknown) => boolean;
29
29
  /** Whether the arguments of a call make a condition true. */
30
30
  export declare function conditionHolds(condition: ActionConditionSpec, args: ToolArguments): boolean;
31
+ /** Where a call of a tool that forwards mail says what it forwards, from where, and to whom. */
32
+ export type Forwarding = {
33
+ /** The argument naming the message forwarded; a call without it does not forward. */
34
+ message: string;
35
+ /** The argument holding the address of the mailbox it sends from. */
36
+ mailbox: string;
37
+ /** The arguments holding its recipients. */
38
+ recipients: string[];
39
+ };
40
+ /** The tools that forward a message, by `server.tool` — as `agentspecs.actions.FORWARDING`. */
41
+ export declare const FORWARDING: Record<string, Forwarding>;
42
+ /** The mail addresses an argument holds — one, several separated by commas, or a list — in lower case. */
43
+ export declare function addresses(value: unknown): string[];
44
+ /**
45
+ * Whether a call forwards a message to somebody outside the organization
46
+ * (LOOP W-04): the domain of the mailbox it sends from. Fails closed: a
47
+ * forward that does not say its mailbox is outside. A reply is not a forward.
48
+ */
49
+ export declare function forwardsOutside(server: string, name: string, args: ToolArguments): boolean;
31
50
  /**
32
51
  * The classes of a tool, by reference; empty when nobody classed it.
33
52
  *
34
53
  * With the arguments of a call, the classes of that call. Without them,
35
- * everything the tool can do: nobody said what it is asked.
54
+ * everything the tool can do: nobody said what it is asked. A forward
55
+ * outside the organization is told from the call (`forwardsOutside`): it
56
+ * publishes besides sending.
36
57
  */
37
58
  export declare function classesOf(ref: string, args?: ToolArguments): ActionClass[];
38
59
  /** Whether a tool only reads. An unknown tool — no class — does not. */
@@ -129,11 +129,53 @@ function entryOf(server, name) {
129
129
  }
130
130
  return [[...actions.default], []];
131
131
  }
132
+ /** The tools that forward a message, by `server.tool` — as `agentspecs.actions.FORWARDING`. */
133
+ export const FORWARDING = {
134
+ 'google-workspace.send_gmail_message': {
135
+ message: 'forward_message_id',
136
+ mailbox: 'user_google_email',
137
+ recipients: ['to', 'cc', 'bcc'],
138
+ },
139
+ };
140
+ /** The mail addresses an argument holds — one, several separated by commas, or a list — in lower case. */
141
+ export function addresses(value) {
142
+ const items = Array.isArray(value) ? value : [value];
143
+ return items
144
+ .filter((item) => typeof item === 'string')
145
+ .flatMap(item => item.split(','))
146
+ .map(part => {
147
+ const angled = /<([^>]*)>/.exec(part);
148
+ return (angled ? angled[1] : part).trim().toLowerCase();
149
+ })
150
+ .filter(address => address.includes('@'));
151
+ }
152
+ const domainOf = (address) => address.slice(address.lastIndexOf('@') + 1).trim();
153
+ /**
154
+ * Whether a call forwards a message to somebody outside the organization
155
+ * (LOOP W-04): the domain of the mailbox it sends from. Fails closed: a
156
+ * forward that does not say its mailbox is outside. A reply is not a forward.
157
+ */
158
+ export function forwardsOutside(server, name, args) {
159
+ const forwarding = own(FORWARDING, `${server}.${name}`);
160
+ const message = forwarding ? args[forwarding.message] : undefined;
161
+ if (!forwarding || typeof message !== 'string' || !message.trim()) {
162
+ return false;
163
+ }
164
+ const [home] = addresses(args[forwarding.mailbox]);
165
+ if (!home) {
166
+ return true;
167
+ }
168
+ return forwarding.recipients
169
+ .flatMap(field => addresses(args[field]))
170
+ .some(address => domainOf(address) !== domainOf(home));
171
+ }
132
172
  /**
133
173
  * The classes of a tool, by reference; empty when nobody classed it.
134
174
  *
135
175
  * With the arguments of a call, the classes of that call. Without them,
136
- * everything the tool can do: nobody said what it is asked.
176
+ * everything the tool can do: nobody said what it is asked. A forward
177
+ * outside the organization is told from the call (`forwardsOutside`): it
178
+ * publishes besides sending.
137
179
  */
138
180
  export function classesOf(ref, args) {
139
181
  const [server, name] = splitRef(ref);
@@ -150,6 +192,12 @@ export function classesOf(ref, args) {
150
192
  }
151
193
  }
152
194
  }
195
+ if (args !== undefined &&
196
+ classes.length > 0 &&
197
+ !classes.includes('publish') &&
198
+ forwardsOutside(server, name, args)) {
199
+ classes.push('publish');
200
+ }
153
201
  return classes;
154
202
  }
155
203
  /** Whether a tool only reads. An unknown tool — no class — does not. */
@@ -1,9 +1,15 @@
1
1
  import type { CSSProperties, JSX, ReactNode } from 'react';
2
2
  import type { PluginRef } from '@datalayer/reactor';
3
3
  import type { AppAccent, AppEmbedMode, AppSpec } from '../../types/agentspecs';
4
- import { type AppInstance } from '../apps/AppRenderer';
4
+ import type { ThemeOverrides } from '../../types/chat';
5
+ import type { AssistantCharacter } from '../../chat/assistant/characters';
6
+ import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
7
+ import type { PresenceState } from '../../chat/presence/presenceStatus';
8
+ import { type AppInstance, type AppRendererProps } from '../apps/AppRenderer';
9
+ import type { ChatSaid } from '../plugins/chat';
5
10
  import { type AssistantCharacterChosen } from '../plugins/assistant-characters';
6
11
  import { type EmbedColorMode } from './embedConfig';
12
+ import { type AppEmbedHost } from './hostBridge';
7
13
  export type AppEmbedProps = {
8
14
  /** The application. */
9
15
  app: AppSpec;
@@ -44,6 +50,23 @@ export type AppEmbedProps = {
44
50
  * element passes none.
45
51
  */
46
52
  plugins?: PluginRef[];
53
+ /**
54
+ * What the page gives the application (LOOP D-10): the values it passes,
55
+ * its functions, and what it is told — read for the names the Appspec
56
+ * gives under `deployment.embedded.host`.
57
+ */
58
+ host?: AppEmbedHost;
59
+ };
60
+ /**
61
+ * The page and the application talking (LOOP D-10): the tools of the host
62
+ * the Appspec names, given to its agent through a chat-extras plugin made
63
+ * once — the page's newest values and functions read at each call — and
64
+ * `message` said to the page when an answer is done.
65
+ */
66
+ export declare function useHostBridge(app: AppSpec, host: AppEmbedHost | undefined): {
67
+ plugins: PluginRef[];
68
+ onPresence: (state: PresenceState) => void;
69
+ onSaying: (said: ChatSaid) => void;
47
70
  };
48
71
  /**
49
72
  * The character the assistant mode draws (T-24, D-07): the application's own
@@ -54,6 +77,38 @@ export type AppEmbedProps = {
54
77
  export declare function embedAssistantCharacter(app: AppSpec, plugins?: PluginRef[]): AssistantCharacterChosen;
55
78
  /** What a floating application says to a visitor Datalayer does not know. */
56
79
  export declare const signedOutSentence: (app: Pick<AppSpec, "name">) => string;
80
+ export type AppFloatingProps = {
81
+ app: AppSpec;
82
+ view: 'floating-small' | 'panel' | 'assistant';
83
+ colorMode: 'light' | 'dark';
84
+ /** The accent and face inside the conversation. */
85
+ themeOverrides: ThemeOverrides;
86
+ /** The assistant's character, in the assistant mode. */
87
+ character?: AssistantCharacter | AssistantCharacterData;
88
+ /** The host's agent-runtimes server, when it names one. */
89
+ serverUrl?: string;
90
+ instance?: AppInstance;
91
+ /** The visit's embed token, on the host's server (R-20). */
92
+ embedToken?: string;
93
+ /**
94
+ * What else the host gives the application's `AppRenderer`: Datalayer's
95
+ * own page at an application's address (T-21) passes the UI plugins its
96
+ * organization turned off, the visitors' runtime and the one kept on.
97
+ */
98
+ renderer?: Partial<Pick<AppRendererProps, 'pluginsOff' | 'datalayerVisitors' | 'datalayerKept'>>;
99
+ /** What the page gives the application (D-10). */
100
+ host?: AppEmbedHost;
101
+ };
102
+ /**
103
+ * The bubble, the panel and the assistant: `ChatFloating`'s chrome in that
104
+ * mode — the button, its blink and its balloon, the panel at the edge, the
105
+ * character dragged about and sent away — holding the application as
106
+ * `AppRenderer` draws it inline (R-01): its kind's preset, its layout, its
107
+ * agent created with its spec and spoken to at its session endpoint (R-04).
108
+ * The workspace tells the chrome what the application is doing and what it
109
+ * last said (`onPresence`, `onSaying`).
110
+ */
111
+ export declare function AppFloating({ app, view, colorMode, themeOverrides, character, serverUrl, instance, embedToken, renderer, host, }: AppFloatingProps): JSX.Element;
57
112
  /** The `loop` theme, with the application's (or the host's) accent and face. */
58
113
  export declare function EmbedThemed({ accent, colorMode, font, children, style, }: {
59
114
  accent: AppAccent;
@@ -62,5 +117,5 @@ export declare function EmbedThemed({ accent, colorMode, font, children, style,
62
117
  children: ReactNode;
63
118
  style?: CSSProperties;
64
119
  }): JSX.Element;
65
- export declare function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, embedToken, height, ownPortal, plugins, }: AppEmbedProps): JSX.Element;
120
+ export declare function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, embedToken, height, ownPortal, plugins, host, }: AppEmbedProps): JSX.Element;
66
121
  export default AppEmbed;
@@ -35,16 +35,66 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
35
35
  *
36
36
  * @module loop/embed/AppEmbed
37
37
  */
38
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
38
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
39
39
  import { Text, registerPortalRoot } from '@primer/react';
40
40
  import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
41
41
  import { FluentEmoji } from '@datalayer/core/lib/components/emoji';
42
42
  import { DatalayerThemeProvider, loopTheme } from '@datalayer/primer-addons';
43
43
  import { ChatFloating } from '../../chat/ChatFloating';
44
- import { AppRenderer } from '../apps/AppRenderer';
44
+ import { AppRenderer, } from '../apps/AppRenderer';
45
45
  import { AssistantCharactersPlugin, assistantCharacterFor, assistantCharactersFrom, } from '../plugins/assistant-characters';
46
46
  import { floatingViewOf } from './embedConfig';
47
+ import { createChatExtrasPlugin } from '../plugins/chat-extras';
48
+ import { hostFrontendTools } from './hostBridge';
47
49
  import { embedThemeOverrides, embedThemeStyles } from './embedTheme';
50
+ /** No plugins for the renderer: one array, so that it never reads as a change. */
51
+ const NO_HOST_PLUGINS = [];
52
+ /**
53
+ * The page and the application talking (LOOP D-10): the tools of the host
54
+ * the Appspec names, given to its agent through a chat-extras plugin made
55
+ * once — the page's newest values and functions read at each call — and
56
+ * `message` said to the page when an answer is done.
57
+ */
58
+ export function useHostBridge(app, host) {
59
+ const hostRef = useRef(host);
60
+ hostRef.current = host;
61
+ const handle = useMemo(() => createChatExtrasPlugin(), []);
62
+ const bridge = app.deployment.embedded?.host;
63
+ const key = JSON.stringify([bridge ?? null, app.rules, app.connections]);
64
+ useEffect(() => {
65
+ handle.setExtras({
66
+ frontendTools: hostFrontendTools(app, () => hostRef.current ?? {}),
67
+ });
68
+ // The tools change with what the Appspec names, and the rules that decide them.
69
+ // eslint-disable-next-line react-hooks/exhaustive-deps
70
+ }, [handle, key]);
71
+ const plugins = useMemo(() => (bridge ? [handle.plugin] : NO_HOST_PLUGINS), [bridge, handle]);
72
+ const said = useRef({ answering: false });
73
+ const before = useRef('idle');
74
+ const told = useRef('');
75
+ const onSaying = useCallback((next) => {
76
+ said.current = next;
77
+ }, []);
78
+ const onPresence = useCallback((state) => {
79
+ const was = before.current;
80
+ before.current = state;
81
+ const done = state === 'idle' || state === 'waiting';
82
+ const saying = said.current.saying;
83
+ if (done &&
84
+ was !== 'idle' &&
85
+ was !== 'waiting' &&
86
+ said.current.answering &&
87
+ saying &&
88
+ saying.id !== told.current) {
89
+ told.current = saying.id;
90
+ hostRef.current?.onEvent?.({
91
+ type: 'message',
92
+ detail: { id: saying.id, text: saying.markdown || saying.text },
93
+ });
94
+ }
95
+ }, []);
96
+ return { plugins, onPresence, onSaying };
97
+ }
48
98
  /** No host plugins: one array, so that it never reads as a change. */
49
99
  const NO_PLUGINS = [];
50
100
  /**
@@ -72,14 +122,24 @@ function Face({ emoji }) {
72
122
  * The workspace tells the chrome what the application is doing and what it
73
123
  * last said (`onPresence`, `onSaying`).
74
124
  */
75
- function FloatingApp({ app, view, colorMode, themeOverrides, character, serverUrl, instance, embedToken, }) {
125
+ export function AppFloating({ app, view, colorMode, themeOverrides, character, serverUrl, instance, embedToken, renderer, host, }) {
76
126
  const [presence, setPresence] = useState('idle');
77
127
  const [said, setSaid] = useState({ answering: false });
128
+ const bridge = useHostBridge(app, host);
129
+ const onPresence = useCallback((state) => {
130
+ setPresence(state);
131
+ bridge.onPresence(state);
132
+ }, [bridge.onPresence]);
133
+ const onSaying = useCallback((next) => {
134
+ setSaid(next);
135
+ bridge.onSaying(next);
136
+ }, [bridge.onSaying]);
78
137
  const welcome = app.interface.welcome || app.description;
79
138
  // A runtime is somebody's to pay for: on Datalayer, only for a visitor
80
139
  // Datalayer knows — said, and nothing launched, for one it does not.
81
140
  const token = useIAMStore(state => state.token);
82
- const signedOut = !serverUrl && !token;
141
+ // Signed out on the visitors' runtime (R-30) is somebody's too.
142
+ const signedOut = !serverUrl && !token && !renderer?.datalayerVisitors;
83
143
  return (_jsx(ChatFloating, { defaultViewMode: view, ...(character ? { assistantCharacter: character } : {}), ...(app.interface.balloon
84
144
  ? { balloonDisplay: app.interface.balloon }
85
145
  : {}), title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, colorMode: colorMode, useStore: false, conversation: {
@@ -90,7 +150,7 @@ function FloatingApp({ app, view, colorMode, themeOverrides, character, serverUr
90
150
  // The window draws its face, its name and close.
91
151
  hideChatHeader: true,
92
152
  // Mounted closed: the caret goes in when the window opens.
93
- autoFocusPrompt: false, onPresence: setPresence, onSaying: setSaid })),
153
+ autoFocusPrompt: false, onPresence: onPresence, onSaying: onSaying, plugins: bridge.plugins, ...renderer })),
94
154
  presence,
95
155
  saying: said.saying,
96
156
  answering: said.answering,
@@ -139,7 +199,8 @@ export function EmbedThemed({ accent, colorMode, font, children, style, }) {
139
199
  */
140
200
  _jsx("div", { "data-color-mode": colorMode, style: { display: 'contents' }, children: _jsx(DatalayerThemeProvider, { colorMode: colorMode, theme: loopTheme, themeStyles: themeStyles, baseStyles: style, children: children }) }));
141
201
  }
142
- export function AppEmbed({ app, mode, accent, colorMode = 'auto', font, serverUrl, instance, embedToken, height = 640, ownPortal = false, plugins = NO_PLUGINS, }) {
202
+ export function AppEmbed({ app, mode, accent, colorMode = 'auto', font, serverUrl, instance, embedToken, height = 640, ownPortal = false, plugins = NO_PLUGINS, host, }) {
203
+ const bridge = useHostBridge(app, host);
143
204
  const system = useSystemMode();
144
205
  const resolvedMode = colorMode === 'auto' ? system : colorMode;
145
206
  const shownAs = mode ?? app.deployment.embedded?.mode ?? 'inline';
@@ -157,10 +218,12 @@ export function AppEmbed({ app, mode, accent, colorMode = 'auto', font, serverUr
157
218
  return (_jsxs(EmbedThemed, { accent: worn, colorMode: resolvedMode, font: font, style: view
158
219
  ? // Floating: nothing in the page's flow, nothing painted behind.
159
220
  { backgroundColor: 'transparent' }
160
- : { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view ? (_jsx(FloatingApp, { app: app, view: view, serverUrl: serverUrl, instance: instance, embedToken: embedToken, colorMode: resolvedMode, themeOverrides: themeOverrides, character: character })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), ...(serverUrl && embedToken ? { embedToken } : {}), instance: instance,
221
+ : { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view ? (_jsx(AppFloating, { app: app, view: view, serverUrl: serverUrl, instance: instance, embedToken: embedToken, colorMode: resolvedMode, themeOverrides: themeOverrides, character: character, host: host })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), ...(serverUrl && embedToken ? { embedToken } : {}), instance: instance,
161
222
  // The host's accent and face over the application's own, inside
162
223
  // its conversation too, in the embed's mode: the host's or its
163
224
  // visitor's system's, not a Datalayer setting.
164
- themeOverrides: themeOverrides, colorMode: resolvedMode }))] }));
225
+ themeOverrides: themeOverrides, colorMode: resolvedMode,
226
+ // What the page gives it, and what it tells the page (D-10).
227
+ plugins: bridge.plugins, onPresence: bridge.onPresence, onSaying: bridge.onSaying }))] }));
165
228
  }
166
229
  export default AppEmbed;