@datalayer/agent-runtimes 1.3.34 → 1.3.35

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.
@@ -33,8 +33,15 @@ export declare const APP_KINDS: AppKind[];
33
33
  export declare const APP_ACCENTS: AppAccent[];
34
34
  /** The four ways an application sits in another product's page (LOOP D-07). */
35
35
  export declare const APP_EMBED_MODES: AppEmbedMode[];
36
- /** The characters an Appspec may name for its floating assistant (LOOP T-24). */
37
- export declare const APP_ASSISTANT_CHARACTERS: AppAssistantCharacter[];
36
+ /**
37
+ * How an Appspec names the character of its floating assistant (LOOP T-24):
38
+ * the id a plugin contributes it under, `paperclip` or `acme-owl` — as
39
+ * agentspecs' `ASSISTANT_CHARACTER_ID`. Which ids exist is what the enabled
40
+ * plugins contribute (`assistantCharacterNamed`), not the spec's to say.
41
+ */
42
+ export declare const APP_ASSISTANT_CHARACTER_ID: RegExp;
43
+ /** Whether a value is shaped as a character id an Appspec may name. */
44
+ export declare function isAssistantCharacterId(value: unknown): value is AppAssistantCharacter;
38
45
  /** The layout a kind starts with. */
39
46
  export declare const DEFAULT_LAYOUTS: Record<AppKind, AppLayout>;
40
47
  /** What an application keeps of what it did, when it does not say. */
@@ -23,13 +23,19 @@ export const APP_EMBED_MODES = [
23
23
  'panel',
24
24
  'assistant',
25
25
  ];
26
- /** The characters an Appspec may name for its floating assistant (LOOP T-24). */
27
- export const APP_ASSISTANT_CHARACTERS = [
28
- 'paperclip',
29
- 'wizard',
30
- 'cat',
31
- 'eyes',
32
- ];
26
+ /**
27
+ * How an Appspec names the character of its floating assistant (LOOP T-24):
28
+ * the id a plugin contributes it under, `paperclip` or `acme-owl` — as
29
+ * agentspecs' `ASSISTANT_CHARACTER_ID`. Which ids exist is what the enabled
30
+ * plugins contribute (`assistantCharacterNamed`), not the spec's to say.
31
+ */
32
+ export const APP_ASSISTANT_CHARACTER_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
33
+ /** Whether a value is shaped as a character id an Appspec may name. */
34
+ export function isAssistantCharacterId(value) {
35
+ return (typeof value === 'string' &&
36
+ value.length <= 64 &&
37
+ APP_ASSISTANT_CHARACTER_ID.test(value));
38
+ }
33
39
  const ACTION_CLASS_NAMES = [
34
40
  'read',
35
41
  'write',
@@ -218,7 +224,7 @@ function parseInterface(data, kind) {
218
224
  if (isData(data.surface)) {
219
225
  parsed.surface = parseSurface(data.surface);
220
226
  }
221
- if (APP_ASSISTANT_CHARACTERS.includes(data.assistant)) {
227
+ if (isAssistantCharacterId(data.assistant)) {
222
228
  parsed.assistant = data.assistant;
223
229
  }
224
230
  return parsed;
@@ -39,7 +39,7 @@ import { getTeamSpec } from '../../specs/teams';
39
39
  import { getToolSpec } from '../../specs/tools';
40
40
  import { getTrack } from '../../specs/tracks';
41
41
  import { pluginsOffSetupNotes } from '../plugins/canvas-blocks';
42
- import { parseAppspec } from './appspec';
42
+ import { isAssistantCharacterId, parseAppspec } from './appspec';
43
43
  import { classesOf, splitRef, toolBehaviours } from './rules';
44
44
  export const NOT_READY = 'Not ready';
45
45
  export const NEEDS_ATTENTION = 'Needs attention';
@@ -378,7 +378,6 @@ const ENUMS = {
378
378
  ],
379
379
  visibility: ['private', 'invited', 'organization', 'link', 'public'],
380
380
  mode: ['inline', 'bubble', 'panel', 'assistant'],
381
- character: ['paperclip', 'wizard', 'cat', 'eyes'],
382
381
  trigger: ['schedule', 'event', 'once'],
383
382
  criterion: ['metric', 'noul', 'choice', 'score'],
384
383
  direction: ['higher', 'lower'],
@@ -506,7 +505,9 @@ export function documentShapeProblems(document) {
506
505
  mapping(d.interface, 'interface', ui => {
507
506
  oneOf(ui.layout, ENUMS.layout, 'interface.layout');
508
507
  oneOf(ui.accent, ENUMS.accent, 'interface.accent');
509
- oneOf(ui.assistant, ENUMS.character, 'interface.assistant');
508
+ if (ui.assistant !== undefined && !isAssistantCharacterId(ui.assistant)) {
509
+ at('interface.assistant', 'names a character by the id a plugin contributes it under, lowercase words joined by a hyphen: `paperclip`, `acme-owl`');
510
+ }
510
511
  text(ui.welcome, 'interface.welcome');
511
512
  texts(ui.components, 'interface.components');
512
513
  records(ui.starters, 'interface.starters', (item, where) => {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The floating assistant in the LOOP workspace (LOOP T-24): the character a
3
+ * page draws over its workspace, chosen from what the enabled plugins
4
+ * contribute to `loop.assistant.character` — the application's own
5
+ * (`interface.assistant`) when its Appspec names one, else the person's
6
+ * choice from their settings, else the paper clip. It acts out the chat's
7
+ * turn (T-22), says the agent's newest words in its balloon while the
8
+ * conversation is out of sight (T-23), opens the conversation when clicked,
9
+ * is dragged about and sent away as the chat's own assistant is (T-27).
10
+ *
11
+ * The workspace's chat is the conversation: on a page layout, its side panel
12
+ * is opened and closed; where the conversation is always on screen, a click
13
+ * takes the person to its composer.
14
+ *
15
+ * @module loop/plugins/assistant
16
+ */
17
+ import type { JSX } from 'react';
18
+ import { type AssistantState } from '../../../chat/assistant/state';
19
+ import { type ChatTurnSnapshot } from '../../core';
20
+ export declare const LOOP_ASSISTANT_PLUGIN_NAME = "@datalayer/loop-plugin-assistant";
21
+ export type LoopAssistantConfig = {
22
+ /** The character the application's Appspec names; it wins. */
23
+ app?: string;
24
+ /** The character the person chose in their settings. */
25
+ person?: string;
26
+ };
27
+ /**
28
+ * What the character acts out, from the workspace's chat turn (T-22): it
29
+ * thinks while the agent thinks, works while a tool runs (the turn's
30
+ * `activity`), speaks while the answer is written, and idles otherwise.
31
+ */
32
+ export declare function assistantStateOfTurn(turn: ChatTurnSnapshot, moment?: {
33
+ arriving?: boolean;
34
+ leaving?: boolean;
35
+ }): AssistantState;
36
+ export declare function LoopAssistant({ app, person, }: LoopAssistantConfig): JSX.Element | null;
37
+ export declare const LoopAssistantPlugin: import("@datalayer/reactor").ReactorPlugin<LoopAssistantConfig, unknown, unknown>;
38
+ export default LoopAssistantPlugin;
@@ -0,0 +1,140 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { definePlugin, signal } from '@datalayer/reactor';
4
+ import { useContributions, useSignalValue } from '@datalayer/reactor/react';
5
+ import { Text } from '@primer/react';
6
+ import { Box } from '@datalayer/primer-addons';
7
+ import { pageLayoutPanelOpen, togglePagePanel, } from '@datalayer/primer-addons/lib/reactor';
8
+ import { AssistantStage } from '../../../chat/assistant/AssistantStage';
9
+ import { assistantStateOf, keepAway, keptAway, latestSaying, } from '../../../chat/assistant/state';
10
+ import { useViewportDrag } from '../../../chat/useViewportDrag';
11
+ import { LoopAssistantCharacter, LoopChatLayout, LoopChatTurn, LoopSlots, } from '../../core';
12
+ import { assistantCharacterFor } from '../assistant-characters';
13
+ export const LOOP_ASSISTANT_PLUGIN_NAME = '@datalayer/loop-plugin-assistant';
14
+ /** The character's size, in pixels, as the chat's own assistant. */
15
+ const SIZE = 88;
16
+ /* A signal to read when no chat contributed a turn: the hook needs one. */
17
+ const NO_TURN = signal({ id: 0, status: 'idle' });
18
+ /**
19
+ * What the character acts out, from the workspace's chat turn (T-22): it
20
+ * thinks while the agent thinks, works while a tool runs (the turn's
21
+ * `activity`), speaks while the answer is written, and idles otherwise.
22
+ */
23
+ export function assistantStateOfTurn(turn, moment = {}) {
24
+ const busy = turn.status === 'thinking' || turn.status === 'streaming';
25
+ const presence = !busy
26
+ ? 'idle'
27
+ : turn.activity
28
+ ? 'working'
29
+ : 'thinking';
30
+ return assistantStateOf(presence, {
31
+ ...moment,
32
+ speaking: turn.status === 'streaming' && !turn.activity,
33
+ });
34
+ }
35
+ /** Take the person to the workspace's composer. */
36
+ function focusComposer() {
37
+ const composer = document.querySelector('[data-chat-composer] textarea, [data-chat-composer] [contenteditable="true"]');
38
+ composer?.focus();
39
+ }
40
+ export function LoopAssistant({ app, person, }) {
41
+ const contributed = useContributions(LoopAssistantCharacter).map(entry => entry.value);
42
+ const chosen = assistantCharacterFor(contributed, { app, person });
43
+ const turnEntries = useContributions(LoopChatTurn);
44
+ const turn = useSignalValue(turnEntries[0]?.value.turn ?? NO_TURN);
45
+ // On a page layout the conversation is its side panel; elsewhere it is
46
+ // always on screen.
47
+ const onPageLayout = useContributions(LoopChatLayout).some(entry => entry.value.id === 'page-layout');
48
+ const panelOpen = useSignalValue(pageLayoutPanelOpen);
49
+ const open = onPageLayout && panelOpen;
50
+ const [away, setAway] = useState(keptAway);
51
+ const [arriving, setArriving] = useState(true);
52
+ const [leaving, setLeaving] = useState(false);
53
+ useEffect(() => {
54
+ const timer = setTimeout(() => setArriving(false), 1800);
55
+ return () => clearTimeout(timer);
56
+ }, []);
57
+ /* The agent's newest words, said in the balloon until heard (T-23). */
58
+ const saying = turn.assistant
59
+ ? latestSaying([
60
+ { id: String(turn.id), role: 'assistant', content: turn.assistant },
61
+ ])
62
+ : undefined;
63
+ const [heardId, setHeardId] = useState();
64
+ const [fresh, setFresh] = useState(false);
65
+ useEffect(() => {
66
+ if (!saying || open) {
67
+ return;
68
+ }
69
+ setFresh(true);
70
+ const timer = setTimeout(() => setFresh(false), 12000);
71
+ return () => clearTimeout(timer);
72
+ }, [saying?.id, saying?.text, open]);
73
+ useEffect(() => {
74
+ if (open && saying) {
75
+ setHeardId(saying.id);
76
+ }
77
+ }, [open, saying?.id]);
78
+ const stageRef = useRef(null);
79
+ const drag = useViewportDrag(stageRef, { whole: true });
80
+ if (away !== 'none') {
81
+ return null;
82
+ }
83
+ if ('problem' in chosen) {
84
+ // Not replaced by another: what was named is said, where it would stand.
85
+ return (_jsx(Box, { role: "status", "data-assistant-problem": chosen.saidBy, sx: {
86
+ position: 'fixed',
87
+ right: 24,
88
+ bottom: 24,
89
+ zIndex: 1000,
90
+ maxWidth: 320,
91
+ p: 2,
92
+ borderRadius: 2,
93
+ border: '1px solid',
94
+ borderColor: 'attention.muted',
95
+ bg: 'attention.subtle',
96
+ boxShadow: 'shadow.medium',
97
+ }, children: _jsx(Text, { sx: { fontSize: 1 }, children: chosen.problem }) }));
98
+ }
99
+ const state = assistantStateOfTurn(turn, { arriving, leaving });
100
+ const unheard = !!saying && saying.id !== heardId;
101
+ const balloon = unheard && saying
102
+ ? { text: saying.text, more: saying.more }
103
+ : { text: 'Click me to open the conversation.' };
104
+ const insist = state === 'greeting' || (unheard && (state === 'speaking' || fresh));
105
+ const onToggle = () => {
106
+ if (onPageLayout) {
107
+ togglePagePanel();
108
+ }
109
+ else {
110
+ focusComposer();
111
+ }
112
+ };
113
+ const onDismiss = (next) => {
114
+ keepAway(next);
115
+ setLeaving(true);
116
+ setTimeout(() => {
117
+ setLeaving(false);
118
+ setAway(next);
119
+ }, 600);
120
+ };
121
+ return (_jsx(AssistantStage, { character: chosen.character, state: state, size: SIZE, place: drag.position
122
+ ? { left: `${drag.position.left}px`, top: `${drag.position.top}px` }
123
+ : { right: '24px', bottom: '24px' }, stageRef: stageRef, onDragStart: drag.onHandlePointerDown, open: open, onToggle: onToggle, balloon: balloon, insist: insist, onDismiss: onDismiss }));
124
+ }
125
+ export const LoopAssistantPlugin = definePlugin({
126
+ name: LOOP_ASSISTANT_PLUGIN_NAME,
127
+ config: { app: undefined, person: undefined },
128
+ displayName: 'Floating assistant',
129
+ description: 'The chat as a character on the page: the application’s own, or the one the person chose, from the characters the enabled plugins contribute.',
130
+ octicon: 'paperclip',
131
+ build: ({ config }) => {
132
+ const Configured = () => (_jsx(LoopAssistant, { app: config.app, person: config.person }));
133
+ return {
134
+ components: [
135
+ { id: 'loop-assistant', slot: LoopSlots.root, Component: Configured },
136
+ ],
137
+ };
138
+ },
139
+ });
140
+ export default LoopAssistantPlugin;
@@ -27,4 +27,29 @@ export declare function assistantCharactersOf(reactor: ContributionsReader): {
27
27
  * enabled plugin contributes is an error that says which ids there are.
28
28
  */
29
29
  export declare function assistantCharacterNamed(reactor: ContributionsReader, id: string): AssistantCharacter | AssistantCharacterData;
30
+ /** Whose choice the character drawn is (T-24). */
31
+ export type AssistantCharacterSaidBy = 'app' | 'person' | 'default';
32
+ /** The character drawn, or why none can be. */
33
+ export type AssistantCharacterChosen = {
34
+ id: string;
35
+ character: AssistantCharacter | AssistantCharacterData;
36
+ saidBy: AssistantCharacterSaidBy;
37
+ } | {
38
+ problem: string;
39
+ saidBy: AssistantCharacterSaidBy;
40
+ };
41
+ /**
42
+ * The character the floating assistant draws on a page (T-24): the one the
43
+ * application's Appspec names (`interface.assistant`), which wins; else the
44
+ * one the person chose in their settings; else the paper clip — each looked
45
+ * up in what the enabled plugins contribute. An id no enabled plugin gives
46
+ * is not replaced by another: it is said, with the ids there are.
47
+ */
48
+ export declare function assistantCharacterFor(contributed: ReadonlyArray<{
49
+ id: string;
50
+ character: AssistantCharacter | AssistantCharacterData;
51
+ }>, choice: {
52
+ app?: string;
53
+ person?: string;
54
+ }): AssistantCharacterChosen;
30
55
  export default AssistantCharactersPlugin;
@@ -10,7 +10,7 @@
10
10
  * @module loop/plugins/assistant-characters
11
11
  */
12
12
  import { contribution, definePlugin, } from '@datalayer/reactor';
13
- import { ASSISTANT_CHARACTERS, } from '../../../chat/assistant/characters';
13
+ import { ASSISTANT_CHARACTERS, DEFAULT_ASSISTANT_CHARACTER, } from '../../../chat/assistant/characters';
14
14
  import { LoopAssistantCharacter, } from '../../core';
15
15
  export const ASSISTANT_CHARACTERS_PLUGIN_NAME = '@datalayer/loop-plugin-assistant-characters';
16
16
  export const AssistantCharactersPlugin = definePlugin({
@@ -38,4 +38,32 @@ export function assistantCharacterNamed(reactor, id) {
38
38
  }
39
39
  return found.character;
40
40
  }
41
+ /**
42
+ * The character the floating assistant draws on a page (T-24): the one the
43
+ * application's Appspec names (`interface.assistant`), which wins; else the
44
+ * one the person chose in their settings; else the paper clip — each looked
45
+ * up in what the enabled plugins contribute. An id no enabled plugin gives
46
+ * is not replaced by another: it is said, with the ids there are.
47
+ */
48
+ export function assistantCharacterFor(contributed, choice) {
49
+ const saidBy = choice.app
50
+ ? 'app'
51
+ : choice.person
52
+ ? 'person'
53
+ : 'default';
54
+ const id = choice.app ?? choice.person ?? DEFAULT_ASSISTANT_CHARACTER;
55
+ const found = contributed.find(entry => entry.id === id);
56
+ if (found) {
57
+ return { id, character: found.character, saidBy };
58
+ }
59
+ const whose = saidBy === 'app'
60
+ ? 'This application names'
61
+ : saidBy === 'person'
62
+ ? 'Your settings name'
63
+ : 'The default is';
64
+ return {
65
+ saidBy,
66
+ problem: `${whose} the character “${id}”, which nothing enabled here draws; the characters are ${contributed.map(entry => entry.id).join(', ') || 'none'}.`,
67
+ };
68
+ }
41
69
  export default AssistantCharactersPlugin;
@@ -314,14 +314,17 @@ export const APPSPEC_SCHEMA = {
314
314
  assistant: {
315
315
  anyOf: [
316
316
  {
317
- $ref: '#/$defs/AssistantCharacter',
317
+ maxLength: 64,
318
+ pattern: '^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$',
319
+ type: 'string',
318
320
  },
319
321
  {
320
322
  type: 'null',
321
323
  },
322
324
  ],
323
325
  default: null,
324
- description: 'The character its floating assistant shows: `paperclip`, `wizard`, `cat` or `eyes`. The paper clip when unsaid; a person may choose another in their settings',
326
+ description: "The character its floating assistant shows, by the id a plugin contributes it under (lowercase letters and digits, words joined by a hyphen): Datalayer's are `paperclip`, `wizard`, `cat` and `eyes`. The paper clip when unsaid; an id no enabled plugin contributes is refused where the plugins are known, the runtime and the page. Said here, it wins over a person's own choice in their settings",
327
+ title: 'Assistant',
325
328
  },
326
329
  },
327
330
  title: 'AppInterface',
@@ -677,12 +680,6 @@ export const APPSPEC_SCHEMA = {
677
680
  title: 'AppTrigger',
678
681
  type: 'object',
679
682
  },
680
- AssistantCharacter: {
681
- description: "The character an application's floating assistant shows (LOOP T-24):\none of those a UI plugin contributes. Datalayer's are these four.",
682
- enum: ['paperclip', 'wizard', 'cat', 'eyes'],
683
- title: 'AssistantCharacter',
684
- type: 'string',
685
- },
686
683
  Behaviour: {
687
684
  description: 'What an application does when it meets an action: the four a person chooses from.',
688
685
  enum: ['do_it', 'if_asked', 'ask_first', 'leave_to_me'],
@@ -384,10 +384,13 @@ export type AppLayout = 'chat' | 'page' | 'split';
384
384
  /** The one colour of an application; everything else is neutral. */
385
385
  export type AppAccent = 'green' | 'rose' | 'sky' | 'lime' | 'sun' | 'violet';
386
386
  /**
387
- * The character an application's floating assistant shows (LOOP T-24): one of
388
- * those Datalayer's plugin contributes.
387
+ * The character an application's floating assistant shows (LOOP T-24): the id
388
+ * an enabled plugin contributes it under to `loop.assistant.character` —
389
+ * Datalayer's are `paperclip`, `wizard`, `cat` and `eyes`. The spec checks
390
+ * its shape (`APP_ASSISTANT_CHARACTER_ID`); whether a plugin gives it is
391
+ * known only where the plugins are.
389
392
  */
390
- export type AppAssistantCharacter = 'paperclip' | 'wizard' | 'cat' | 'eyes';
393
+ export type AppAssistantCharacter = string;
391
394
  /** How an application sits in another product's page (LOOP D-07). */
392
395
  export type AppEmbedMode = 'inline' | 'bubble' | 'panel' | 'assistant';
393
396
  /** Something an application reaches. */
@@ -456,7 +459,10 @@ export interface AppInterfaceSpec {
456
459
  /** The components of the catalog the surface may use. */
457
460
  components: string[];
458
461
  surface?: AppSurfaceSpec;
459
- /** The character its floating assistant shows; the paper clip when unsaid. */
462
+ /**
463
+ * The character its floating assistant shows; the paper clip when unsaid.
464
+ * Said, it wins over the one the person chose in their settings.
465
+ */
460
466
  assistant?: AppAssistantCharacter;
461
467
  }
462
468
  export interface AppTestCaseSpec {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.3.34",
3
+ "version": "1.3.35",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",