@datalayer/agent-runtimes 1.3.79 → 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.
- package/lib/chat/assistant/AssistantStage.d.ts +9 -4
- package/lib/chat/assistant/AssistantStage.js +18 -6
- package/lib/chat/assistant/SpriteCharacter.d.ts +25 -3
- package/lib/chat/assistant/SpriteCharacter.js +68 -3
- package/lib/chat/assistant/formats/acf.d.ts +56 -0
- package/lib/chat/assistant/formats/acf.js +317 -0
- package/lib/chat/assistant/formats/acs.d.ts +85 -7
- package/lib/chat/assistant/formats/acs.js +111 -39
- package/lib/chat/assistant/formats/files.d.ts +6 -2
- package/lib/chat/assistant/formats/files.js +18 -4
- package/lib/chat/assistant/formats/index.d.ts +6 -3
- package/lib/chat/assistant/formats/index.js +4 -2
- package/lib/chat/assistant/formats/types.d.ts +14 -0
- package/lib/chat/assistant/state.d.ts +2 -0
- package/lib/chat/assistant/state.js +9 -0
- package/lib/chat/base/ChatBase.js +7 -0
- package/lib/chat/base/loopStep.d.ts +40 -0
- package/lib/chat/base/loopStep.js +73 -0
- package/lib/loop/apps/appspec.js +32 -2
- package/lib/loop/apps/checks.js +53 -0
- package/lib/loop/apps/hostTools.d.ts +17 -0
- package/lib/loop/apps/hostTools.js +20 -0
- package/lib/loop/apps/rules.d.ts +22 -1
- package/lib/loop/apps/rules.js +49 -1
- package/lib/loop/embed/AppEmbed.d.ts +57 -2
- package/lib/loop/embed/AppEmbed.js +71 -8
- package/lib/loop/embed/element.js +40 -1
- package/lib/loop/embed/hostBridge.d.ts +79 -0
- package/lib/loop/embed/hostBridge.js +104 -0
- package/lib/loop/index.d.ts +2 -1
- package/lib/loop/index.js +3 -1
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.d.ts +5 -2
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +7 -1
- package/lib/loop/plugins/a2ui-surface/index.d.ts +1 -1
- package/lib/loop/plugins/a2ui-surface/index.js +4 -4
- package/lib/loop/plugins/a2ui-surface/toolResult.d.ts +27 -0
- package/lib/loop/plugins/a2ui-surface/toolResult.js +22 -0
- package/lib/specs/agents/agents.js +6 -25
- package/lib/specs/apps.js +22 -8
- package/lib/specs/appspecSchema.js +67 -0
- package/lib/types/agentspecs.d.ts +28 -0
- package/package.json +1 -1
- 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
|
+
}
|
package/lib/loop/apps/appspec.js
CHANGED
|
@@ -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)
|
|
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)
|
|
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')
|
package/lib/loop/apps/checks.js
CHANGED
|
@@ -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
|
+
}
|
package/lib/loop/apps/rules.d.ts
CHANGED
|
@@ -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. */
|
package/lib/loop/apps/rules.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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;
|