@datalayer/agent-runtimes 1.3.86 → 1.3.88
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/components/a2ui/datalayer/Download.d.ts +21 -0
- package/lib/components/a2ui/datalayer/Download.js +66 -0
- package/lib/components/a2ui/datalayer/drawn.d.ts +1 -1
- package/lib/components/a2ui/datalayer/drawn.js +1 -0
- package/lib/components/a2ui/datalayer/index.d.ts +2 -1
- package/lib/components/a2ui/datalayer/index.js +3 -0
- package/lib/components/inspector/a2aSpans.d.ts +9 -0
- package/lib/components/inspector/a2aSpans.js +11 -1
- package/lib/components/teams/A2ATeamGraph.d.ts +2 -2
- package/lib/components/teams/A2ATeamGraph.js +39 -22
- package/lib/components/teams/AnswerSurfaces.d.ts +56 -0
- package/lib/components/teams/AnswerSurfaces.js +27 -0
- package/lib/components/teams/AnswerSurfacesView.d.ts +14 -0
- package/lib/components/teams/AnswerSurfacesView.js +30 -0
- package/lib/components/teams/index.d.ts +1 -0
- package/lib/components/teams/index.js +1 -0
- package/lib/components/teams/useA2ATeam.d.ts +56 -2
- package/lib/components/teams/useA2ATeam.js +122 -3
- package/lib/hooks/useAgentRuntimes.d.ts +13 -0
- package/lib/hooks/useAgentRuntimes.js +11 -1
- package/lib/loop/apps/AppFeedback.js +11 -2
- package/lib/loop/apps/AppRenderer.d.ts +13 -1
- package/lib/loop/apps/AppRenderer.js +34 -1
- package/lib/loop/apps/AppRuntimePlugins.d.ts +8 -0
- package/lib/loop/apps/AppRuntimePlugins.js +87 -0
- package/lib/loop/apps/feedback.d.ts +7 -0
- package/lib/loop/apps/feedback.js +3 -0
- package/lib/loop/apps/pluginPair.d.ts +60 -0
- package/lib/loop/apps/pluginPair.js +44 -0
- package/lib/loop/apps/threads.d.ts +35 -0
- package/lib/loop/apps/threads.js +79 -0
- package/lib/loop/embed/LoopEmbed.d.ts +4 -3
- package/lib/loop/embed/hostBridge.d.ts +27 -0
- package/lib/loop/embed/hostBridge.js +47 -0
- package/lib/loop/index.d.ts +1 -1
- package/lib/loop/index.js +1 -1
- package/lib/loop/plugins/agent-capacity/index.d.ts +7 -0
- package/lib/loop/plugins/agent-capacity/index.js +3 -2
- package/lib/loop/plugins/agents/DatalayerAgentBridge.js +11 -0
- package/lib/loop/plugins/agents/appLaunch.d.ts +15 -0
- package/lib/loop/plugins/agents/appLaunch.js +28 -0
- package/lib/loop/plugins/chat/ChatView.js +177 -109
- package/lib/loop/shell/LoopWorkspace.d.ts +3 -3
- package/lib/loop/shell/LoopWorkspace.js +1 -1
- package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +10 -2
- package/lib/runtimes/actions.js +23 -0
- package/lib/runtimes/apis.d.ts +18 -0
- package/lib/runtimes/browser/a2aPeer.d.ts +19 -2
- package/lib/runtimes/browser/a2aPeer.js +13 -7
- package/lib/specs/apps.js +102 -30
- package/lib/specs/envvars.d.ts +2 -0
- package/lib/specs/envvars.js +22 -0
- package/lib/specs/outputs.d.ts +1 -0
- package/lib/specs/outputs.js +12 -0
- package/lib/specs/sceneSchema.d.ts +8 -0
- package/lib/specs/sceneSchema.js +863 -0
- package/lib/specs/scenes.js +238 -121
- package/lib/specs/teams/teams.js +23 -7
- package/lib/specs/uiPlugins.js +55 -0
- package/lib/stores/agentRuntimeStore.js +4 -0
- package/lib/types/chat.d.ts +6 -0
- package/lib/types/scenes.d.ts +7 -1
- package/package.json +1 -1
- package/scripts/codegen/generate_envvars.py +1 -1
- package/scripts/codegen/generate_mcp_servers.py +11 -7
- package/scripts/codegen/generate_scenes.py +43 -1
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
* and the way each link carries a message as its flow ({@link flowAfter}).
|
|
23
23
|
* What a peer gives besides words — the formats the page accepts
|
|
24
24
|
* (`accept`), such as a Jupyter notebook — is kept as its artifacts, and
|
|
25
|
-
* the latest notebook kept until another one comes ({@link A2ATeam.notebook})
|
|
25
|
+
* the latest notebook kept until another one comes ({@link A2ATeam.notebook}),
|
|
26
|
+
* and each surface of components of the catalog a peer shows
|
|
27
|
+
* (`application/json+a2ui`) kept with the peer that gave it, in the order
|
|
28
|
+
* they came ({@link A2ATeam.surfaces}, STUDIO H-02). A button pressed on
|
|
29
|
+
* one is a turn of the conversation ({@link A2ATeam.pressAction}): the
|
|
30
|
+
* person's line to that member, its answer from it, in the entry's history.
|
|
26
31
|
* The tools a peer calls on its connections (its MCP servers), told over
|
|
27
32
|
* A2A as it calls them, are kept as the calls running now ({@link callsAfter}).
|
|
28
33
|
*
|
|
@@ -39,9 +44,10 @@
|
|
|
39
44
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
40
45
|
import { stepCountIs, ToolLoopAgent } from 'ai';
|
|
41
46
|
import { createBrowserModel, } from '../../runtimes/browser/model';
|
|
42
|
-
import { a2aPeerTool, askA2APeer, NOTEBOOK_MEDIA_TYPE, } from '../../runtimes/browser/a2aPeer';
|
|
47
|
+
import { a2aPeerTool, askA2APeer, A2UI_MEDIA_TYPE, NOTEBOOK_MEDIA_TYPE, } from '../../runtimes/browser/a2aPeer';
|
|
43
48
|
import { endAgentTurn, endToolCall, startAgentTurn, startToolCall, } from '../inspector/agentSpans';
|
|
44
49
|
import { callsAfter, flowAfter, toolOwnName, pruneCalls, } from './a2aTeamFlow';
|
|
50
|
+
import { PERSON } from './sceneTranscript';
|
|
45
51
|
import { toolLineOfStep, } from '../../chat/assistant/toolLine';
|
|
46
52
|
const NO_CONNECTIONS = [];
|
|
47
53
|
const NO_PEERS = [];
|
|
@@ -55,11 +61,42 @@ export const NOTEBOOK_AND_WORDS = [
|
|
|
55
61
|
NOTEBOOK_MEDIA_TYPE,
|
|
56
62
|
'text/markdown',
|
|
57
63
|
];
|
|
64
|
+
/**
|
|
65
|
+
* What a page that draws an answer's components accepts besides: the
|
|
66
|
+
* catalog's surfaces, a notebook, and words in Markdown (STUDIO H-02).
|
|
67
|
+
*/
|
|
68
|
+
export const COMPONENTS_NOTEBOOK_AND_WORDS = [
|
|
69
|
+
A2UI_MEDIA_TYPE,
|
|
70
|
+
...NOTEBOOK_AND_WORDS,
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* The surfaces among what a member gave, each once by its surface's id,
|
|
74
|
+
* after those already kept.
|
|
75
|
+
*/
|
|
76
|
+
export function surfacesAfter(kept, giver, artifacts) {
|
|
77
|
+
const found = artifacts
|
|
78
|
+
.filter(artifact => artifact.mediaType === A2UI_MEDIA_TYPE)
|
|
79
|
+
.map(artifact => {
|
|
80
|
+
const data = artifact.data;
|
|
81
|
+
return {
|
|
82
|
+
id: data && typeof data.surfaceId === 'string'
|
|
83
|
+
? data.surfaceId
|
|
84
|
+
: `${giver}-${kept.length}`,
|
|
85
|
+
giver,
|
|
86
|
+
artifact,
|
|
87
|
+
};
|
|
88
|
+
})
|
|
89
|
+
.filter((surface, index, all) => !kept.some(one => one.id === surface.id) &&
|
|
90
|
+
all.findIndex(one => one.id === surface.id) === index);
|
|
91
|
+
return found.length ? [...kept, ...found] : kept;
|
|
92
|
+
}
|
|
58
93
|
/** What the peer gave, in words: its words, and each of its artifacts. */
|
|
59
94
|
export function answeredLine(artifacts) {
|
|
60
95
|
const besides = artifacts.map(artifact => artifact.mediaType === NOTEBOOK_MEDIA_TYPE
|
|
61
96
|
? `a notebook, ${artifact.name}`
|
|
62
|
-
: artifact.
|
|
97
|
+
: artifact.mediaType === A2UI_MEDIA_TYPE
|
|
98
|
+
? `what it shows, ${artifact.name}`
|
|
99
|
+
: artifact.name);
|
|
63
100
|
return besides.length ? `the report and ${besides.join(', ')}` : 'the report';
|
|
64
101
|
}
|
|
65
102
|
/** The latest notebook among what a peer gave, or none. */
|
|
@@ -221,6 +258,7 @@ export function useA2ATeam(options) {
|
|
|
221
258
|
const [artifacts, setArtifacts] = useState([]);
|
|
222
259
|
const [givens, setGivens] = useState({});
|
|
223
260
|
const [notebook, setNotebook] = useState(null);
|
|
261
|
+
const [surfaces, setSurfaces] = useState([]);
|
|
224
262
|
const [flows, setFlows] = useState({});
|
|
225
263
|
const [calls, setCalls] = useState([]);
|
|
226
264
|
const callsNow = useRef([]);
|
|
@@ -291,6 +329,7 @@ export function useA2ATeam(options) {
|
|
|
291
329
|
setReports(prev => ({ ...prev, [id]: event.answer }));
|
|
292
330
|
setArtifacts(event.artifacts);
|
|
293
331
|
setGivens(prev => ({ ...prev, [id]: event.artifacts }));
|
|
332
|
+
setSurfaces(prev => surfacesAfter(prev, id, event.artifacts));
|
|
294
333
|
const given = notebookAmong(event.artifacts);
|
|
295
334
|
if (given) {
|
|
296
335
|
setNotebook(given);
|
|
@@ -575,6 +614,72 @@ export function useA2ATeam(options) {
|
|
|
575
614
|
setPersona,
|
|
576
615
|
peersAtRest,
|
|
577
616
|
]);
|
|
617
|
+
const pressAction = useCallback(async (memberId, pressed) => {
|
|
618
|
+
const words = pressed.message.trim();
|
|
619
|
+
const isEntry = memberId === entry.id;
|
|
620
|
+
const one = isEntry ? undefined : peerOf(memberId);
|
|
621
|
+
const name = isEntry ? entry.name : (one?.app.name ?? memberId);
|
|
622
|
+
const to = isEntry ? entryPeer : (one?.peer ?? null);
|
|
623
|
+
if (!to) {
|
|
624
|
+
throw new Error(`${name} is not reached: it cannot be asked.`);
|
|
625
|
+
}
|
|
626
|
+
if (busy) {
|
|
627
|
+
throw new Error('Wait for the answer under way, then choose.');
|
|
628
|
+
}
|
|
629
|
+
if (!words) {
|
|
630
|
+
throw new Error('This button says nothing to send.');
|
|
631
|
+
}
|
|
632
|
+
// The entry's own button is a turn with it; another member's is
|
|
633
|
+
// addressed to that member, and answered by it.
|
|
634
|
+
const member = isEntry ? undefined : { id: memberId, name };
|
|
635
|
+
const withMember = member ? { member } : {};
|
|
636
|
+
setBusy(true);
|
|
637
|
+
setTurns(prev => [...prev, { role: 'user', text: words, ...withMember }]);
|
|
638
|
+
abort.current = new AbortController();
|
|
639
|
+
try {
|
|
640
|
+
const { answer } = await askA2APeer(to, words, {
|
|
641
|
+
signal: abort.current.signal,
|
|
642
|
+
action: pressed.action,
|
|
643
|
+
accept,
|
|
644
|
+
onEvent: event => onMemberEvent(memberId, event),
|
|
645
|
+
});
|
|
646
|
+
setTurns(prev => [
|
|
647
|
+
...prev,
|
|
648
|
+
{ role: 'assistant', text: answer, ...withMember },
|
|
649
|
+
]);
|
|
650
|
+
if (!entryPeer) {
|
|
651
|
+
// The entry's model reads it at its next turn: what was chosen,
|
|
652
|
+
// and what the member answered.
|
|
653
|
+
history.current = [
|
|
654
|
+
...history.current,
|
|
655
|
+
{ role: 'user', content: words },
|
|
656
|
+
{
|
|
657
|
+
role: 'assistant',
|
|
658
|
+
content: `${name} answered the person directly: ${answer}`,
|
|
659
|
+
},
|
|
660
|
+
];
|
|
661
|
+
}
|
|
662
|
+
// Answered: back at rest, its words still in its balloon.
|
|
663
|
+
setPersona(memberId, prev => ({ ...prev, state: 'idle' }));
|
|
664
|
+
return answer;
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
668
|
+
setTurns(prev => [
|
|
669
|
+
...prev,
|
|
670
|
+
{
|
|
671
|
+
role: 'assistant',
|
|
672
|
+
text: `Could not answer: ${message}`,
|
|
673
|
+
...withMember,
|
|
674
|
+
},
|
|
675
|
+
]);
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
abort.current = null;
|
|
680
|
+
setBusy(false);
|
|
681
|
+
}
|
|
682
|
+
}, [entry, entryPeer, peerOf, busy, accept, onMemberEvent, setPersona]);
|
|
578
683
|
const stop = useCallback(() => abort.current?.abort(), []);
|
|
579
684
|
useEffect(() => () => {
|
|
580
685
|
abort.current?.abort();
|
|
@@ -594,6 +699,18 @@ export function useA2ATeam(options) {
|
|
|
594
699
|
role: turn.role,
|
|
595
700
|
content: turn.text,
|
|
596
701
|
createdAt: new Date(0),
|
|
702
|
+
// A button pressed on another member's surface: the person's line
|
|
703
|
+
// to it, and its answer, said by it.
|
|
704
|
+
...(turn.member
|
|
705
|
+
? turn.role === 'user'
|
|
706
|
+
? {
|
|
707
|
+
speaker: { id: 'person', name: PERSON },
|
|
708
|
+
directedTo: { id: turn.member.id, name: turn.member.name },
|
|
709
|
+
}
|
|
710
|
+
: {
|
|
711
|
+
speaker: { id: turn.member.id, name: turn.member.name },
|
|
712
|
+
}
|
|
713
|
+
: {}),
|
|
597
714
|
})), [turns]);
|
|
598
715
|
const NONE = useMemo(() => [], []);
|
|
599
716
|
const teamPeers = useMemo(() => peerList.map(one => ({
|
|
@@ -623,11 +740,13 @@ export function useA2ATeam(options) {
|
|
|
623
740
|
report,
|
|
624
741
|
artifacts,
|
|
625
742
|
notebook,
|
|
743
|
+
surfaces,
|
|
626
744
|
flow: first?.flow ?? 'still',
|
|
627
745
|
calls,
|
|
628
746
|
busy,
|
|
629
747
|
ready: entryPeer ? true : agent !== null,
|
|
630
748
|
send,
|
|
749
|
+
pressAction,
|
|
631
750
|
stop,
|
|
632
751
|
};
|
|
633
752
|
}
|
|
@@ -49,7 +49,20 @@ export interface UseAgentOptions {
|
|
|
49
49
|
* connection bootstrap as well as agent creation.
|
|
50
50
|
*/
|
|
51
51
|
runtimeConnection?: AgentRuntimeConnectionOptions;
|
|
52
|
+
/**
|
|
53
|
+
* The application the runtime is launched for (STUDIO R-19): its Appspec,
|
|
54
|
+
* and the saved application and deployment it runs for. Sent with the
|
|
55
|
+
* launch, as `RuntimesClient.create` sends it in Python, so the runtime is
|
|
56
|
+
* given the secrets its connections declare before its agent is made.
|
|
57
|
+
*
|
|
58
|
+
* An application's runtime is always its own: a running runtime is never
|
|
59
|
+
* looked up and reused for it, since one not launched for the application
|
|
60
|
+
* was not given its secrets.
|
|
61
|
+
*/
|
|
62
|
+
appLaunch?: AppLaunch;
|
|
52
63
|
}
|
|
64
|
+
/** What an application's launch carries besides its environment (R-19). */
|
|
65
|
+
export type AppLaunch = Required<Pick<IRuntimeOptions, 'appSpec'>> & Pick<IRuntimeOptions, 'appUid' | 'deploymentUid'>;
|
|
53
66
|
/**
|
|
54
67
|
* Return type for the useAgents hook.
|
|
55
68
|
*/
|
|
@@ -362,7 +362,7 @@ function toAgentRuntimeData(raw) {
|
|
|
362
362
|
* ```
|
|
363
363
|
*/
|
|
364
364
|
export function useAgentRuntimes(options = {}) {
|
|
365
|
-
const { agentSpecId, agentConfig, autoCreateAgent = true, autoStart = false, agentSpec, variant: variantOption, runtimeCreationTarget, runtimeCreationBaseUrl, runtimeConnection, } = options;
|
|
365
|
+
const { agentSpecId, agentConfig, autoCreateAgent = true, autoStart = false, agentSpec, variant: variantOption, runtimeCreationTarget, runtimeCreationBaseUrl, runtimeConnection, appLaunch, } = options;
|
|
366
366
|
// Both vocabularies funnel into one value here, so no branch below has to
|
|
367
367
|
// remember that the old names exist.
|
|
368
368
|
const variant = toAgentRuntimeVariant(variantOption ?? runtimeCreationTarget);
|
|
@@ -484,12 +484,14 @@ export function useAgentRuntimes(options = {}) {
|
|
|
484
484
|
const conn = await storeLaunchAgent(runtimeOptions
|
|
485
485
|
? {
|
|
486
486
|
...runtimeOptions,
|
|
487
|
+
...appLaunch,
|
|
487
488
|
runtimesUrl: resolvedRuntimeCreationBaseUrl,
|
|
488
489
|
}
|
|
489
490
|
: {
|
|
490
491
|
environmentName: 'ai-agents-env',
|
|
491
492
|
creditsLimit: 10,
|
|
492
493
|
givenName: safeName,
|
|
494
|
+
...appLaunch,
|
|
493
495
|
runtimesUrl: resolvedRuntimeCreationBaseUrl,
|
|
494
496
|
});
|
|
495
497
|
setLifecycleStatus('ready');
|
|
@@ -515,6 +517,7 @@ export function useAgentRuntimes(options = {}) {
|
|
|
515
517
|
agentConfig?.name,
|
|
516
518
|
agentSpec?.name,
|
|
517
519
|
agentSpecId,
|
|
520
|
+
appLaunch,
|
|
518
521
|
hasSpec,
|
|
519
522
|
resolvedRuntimeCreationBaseUrl,
|
|
520
523
|
storeLaunchAgent,
|
|
@@ -654,6 +657,12 @@ export function useAgentRuntimes(options = {}) {
|
|
|
654
657
|
if (!hasSpec || runtime || lifecycleStatus !== 'idle') {
|
|
655
658
|
return;
|
|
656
659
|
}
|
|
660
|
+
if (appLaunch) {
|
|
661
|
+
// An application's runtime is launched for it (R-19): a running one
|
|
662
|
+
// was not given its connections' secrets, so none is looked for.
|
|
663
|
+
setLookedForExisting(true);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
657
666
|
let cancelled = false;
|
|
658
667
|
const bootstrap = async () => {
|
|
659
668
|
try {
|
|
@@ -736,6 +745,7 @@ export function useAgentRuntimes(options = {}) {
|
|
|
736
745
|
lifecycleStatus,
|
|
737
746
|
getAuthHeaders,
|
|
738
747
|
agentSpecId,
|
|
748
|
+
appLaunch,
|
|
739
749
|
storeConnectAgent,
|
|
740
750
|
]);
|
|
741
751
|
// Reset agent creation tracking on disconnect
|
|
@@ -6,13 +6,17 @@ import { contribution, definePlugin, signal } from '@datalayer/reactor';
|
|
|
6
6
|
import { useContributions, useSignalValue } from '@datalayer/reactor/react';
|
|
7
7
|
import { useIAMStore } from '../../state';
|
|
8
8
|
import { LoopChatTurn, LoopPromptPanel, } from '../core';
|
|
9
|
-
import { FEEDBACK_WORDS, sendFeedback } from './feedback';
|
|
9
|
+
import { answerIdOf, FEEDBACK_WORDS, sendFeedback } from './feedback';
|
|
10
10
|
export const APP_FEEDBACK_PLUGIN_NAME = '@datalayer/loop-plugin-app-feedback';
|
|
11
11
|
/* A signal to read when no chat contributed a turn: the hook needs one. */
|
|
12
12
|
const NO_TURN = signal({ id: 0, status: 'idle' });
|
|
13
|
+
const NO_CONVERSATION = signal([]);
|
|
13
14
|
export function AppFeedback({ app, workspace, }) {
|
|
14
15
|
const turns = useContributions(LoopChatTurn);
|
|
15
16
|
const turn = useSignalValue(turns[0]?.value.turn ?? NO_TURN);
|
|
17
|
+
// Which answer it is about (P-24): its place among the conversation's answers.
|
|
18
|
+
const conversation = useSignalValue(turns[0]?.value.conversation ?? NO_CONVERSATION);
|
|
19
|
+
const answers = conversation.filter(entry => entry.role === 'assistant').length;
|
|
16
20
|
const token = useIAMStore(state => state.token);
|
|
17
21
|
const [liked, setLiked] = useState(null);
|
|
18
22
|
const [comment, setComment] = useState('');
|
|
@@ -31,7 +35,12 @@ export function AppFeedback({ app, workspace, }) {
|
|
|
31
35
|
}
|
|
32
36
|
setSaid({ turn: turn.id, state: 'sending', says: '' });
|
|
33
37
|
try {
|
|
34
|
-
await sendFeedback({
|
|
38
|
+
await sendFeedback({
|
|
39
|
+
session: thread,
|
|
40
|
+
liked: choice,
|
|
41
|
+
comment,
|
|
42
|
+
message: answerIdOf(answers),
|
|
43
|
+
}, { agentBaseUrl, token });
|
|
35
44
|
setSaid({
|
|
36
45
|
turn: turn.id,
|
|
37
46
|
state: 'kept',
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* @module loop/apps/AppRenderer
|
|
46
46
|
*/
|
|
47
47
|
import { type ComponentType } from 'react';
|
|
48
|
-
import type
|
|
48
|
+
import { type PluginRef, type ReactorExtension, type ReactorPlugin } from '@datalayer/reactor';
|
|
49
49
|
import type { AppSpec } from '../../types/agentspecs';
|
|
50
50
|
import type { ThemeOverrides } from '../../types/chat';
|
|
51
51
|
import { type AppHostPageProps } from '../plugins/app-page';
|
|
@@ -58,6 +58,11 @@ export declare const agentIdOf: (app: Pick<AppSpec, "agent" | "team">) => string
|
|
|
58
58
|
/**
|
|
59
59
|
* The plugin that makes a LOOP workspace run an application.
|
|
60
60
|
*
|
|
61
|
+
* Its page plugin of the pair (LOOP F-15): named `loop-app-<id>`, as its
|
|
62
|
+
* runtime's plugin is, and requiring it — so that it stands down while its
|
|
63
|
+
* runtime does not hold the application, and comes back when it does
|
|
64
|
+
* (`AppRuntimePlugins` follows the runtime).
|
|
65
|
+
*
|
|
61
66
|
* The agent is created with the application's own document (`app_spec`) in
|
|
62
67
|
* its payload; a runtime that knows applications configures the agent from
|
|
63
68
|
* it, and one that does not creates the agent alone — the application is
|
|
@@ -68,6 +73,13 @@ export declare const agentIdOf: (app: Pick<AppSpec, "agent" | "team">) => string
|
|
|
68
73
|
* is not an agent spec, and is refused here rather than launched as one.
|
|
69
74
|
*/
|
|
70
75
|
export declare function defineAppPlugin(app: AppSpec, shown?: AppSpec): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
76
|
+
/**
|
|
77
|
+
* The application on the page: its plugins delivered as one extension named
|
|
78
|
+
* as its plugin pair is, `loop-app-<id>` — the extension its runtime's plugin
|
|
79
|
+
* says too, so that Reactor's manager ("Delivered by") and graph show the
|
|
80
|
+
* page's plugins and the runtime's as one application (LOOP F-15).
|
|
81
|
+
*/
|
|
82
|
+
export declare function appExtension(app: AppSpec, shown: AppSpec, plugins: PluginRef[]): ReactorExtension;
|
|
71
83
|
/** Which instance of an application runs, as the platform knows it (LOOP R-07). */
|
|
72
84
|
export type AppInstance = {
|
|
73
85
|
/** Its `app` item. */
|
|
@@ -50,6 +50,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
50
50
|
* @module loop/apps/AppRenderer
|
|
51
51
|
*/
|
|
52
52
|
import { useMemo } from 'react';
|
|
53
|
+
import { defineExtension, } from '@datalayer/reactor';
|
|
53
54
|
import { loopAccentStyles, useSystemColorMode, useThemeStore, } from '@datalayer/primer-addons';
|
|
54
55
|
import { defineAgentCapacityPlugin } from '../plugins/agent-capacity';
|
|
55
56
|
import { APP_PAGE_SURFACE, defineAppHostPagePlugin, defineAppPagePlugin, hasAppPage, } from '../plugins/app-page';
|
|
@@ -69,6 +70,8 @@ import { defineAppKeptPlugin } from './AppKept';
|
|
|
69
70
|
import { keepsFeedback } from './feedback';
|
|
70
71
|
import { keptBeforeFirstMessage } from './kept';
|
|
71
72
|
import { ChatLanguage } from '../../chat/ChatLanguage';
|
|
73
|
+
import { defineAppRuntimePlugin } from './AppRuntimePlugins';
|
|
74
|
+
import { appPluginPair } from './pluginPair';
|
|
72
75
|
/** The id of an agent or a Cog, without its version. */
|
|
73
76
|
export const agentIdOf = (app) => {
|
|
74
77
|
const reference = app.agent || app.team;
|
|
@@ -80,6 +83,11 @@ export const agentIdOf = (app) => {
|
|
|
80
83
|
/**
|
|
81
84
|
* The plugin that makes a LOOP workspace run an application.
|
|
82
85
|
*
|
|
86
|
+
* Its page plugin of the pair (LOOP F-15): named `loop-app-<id>`, as its
|
|
87
|
+
* runtime's plugin is, and requiring it — so that it stands down while its
|
|
88
|
+
* runtime does not hold the application, and comes back when it does
|
|
89
|
+
* (`AppRuntimePlugins` follows the runtime).
|
|
90
|
+
*
|
|
83
91
|
* The agent is created with the application's own document (`app_spec`) in
|
|
84
92
|
* its payload; a runtime that knows applications configures the agent from
|
|
85
93
|
* it, and one that does not creates the agent alone — the application is
|
|
@@ -93,8 +101,11 @@ export function defineAppPlugin(app, shown = app) {
|
|
|
93
101
|
if (!app.agent) {
|
|
94
102
|
throw new Error(`“${app.name}” is run by a team, which the workspace does not run as an application yet.`);
|
|
95
103
|
}
|
|
104
|
+
const pair = appPluginPair(app.id);
|
|
96
105
|
return defineAgentCapacityPlugin({
|
|
97
106
|
key: `app-${app.id}`,
|
|
107
|
+
name: pair.page.name,
|
|
108
|
+
requiredBackendPlugins: pair.page.requiredBackendPlugins,
|
|
98
109
|
// What a person reads is in their language (P-26); what it is created
|
|
99
110
|
// with is its own Appspec.
|
|
100
111
|
displayName: shown.name,
|
|
@@ -128,6 +139,22 @@ export function defineAppPlugin(app, shown = app) {
|
|
|
128
139
|
: {}),
|
|
129
140
|
});
|
|
130
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The application on the page: its plugins delivered as one extension named
|
|
144
|
+
* as its plugin pair is, `loop-app-<id>` — the extension its runtime's plugin
|
|
145
|
+
* says too, so that Reactor's manager ("Delivered by") and graph show the
|
|
146
|
+
* page's plugins and the runtime's as one application (LOOP F-15).
|
|
147
|
+
*/
|
|
148
|
+
export function appExtension(app, shown, plugins) {
|
|
149
|
+
return defineExtension({
|
|
150
|
+
name: appPluginPair(app.id).extension,
|
|
151
|
+
displayName: shown.name,
|
|
152
|
+
description: shown.description || `The ${shown.name} application`,
|
|
153
|
+
emoji: app.emoji,
|
|
154
|
+
version: app.version,
|
|
155
|
+
plugins,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
131
158
|
/**
|
|
132
159
|
* The workspace an application's layout asks for (LOOP T-07).
|
|
133
160
|
*
|
|
@@ -205,6 +232,8 @@ export function appPreset(app, options = {}) {
|
|
|
205
232
|
return {
|
|
206
233
|
plugins: [
|
|
207
234
|
defineAppPlugin(app, shown),
|
|
235
|
+
// What its runtime holds, told to the page (F-15).
|
|
236
|
+
defineAppRuntimePlugin(app.id),
|
|
208
237
|
...(withPage ? [defineAppPagePlugin(shown), ...blocks] : []),
|
|
209
238
|
// The components its developer wrote (LOOP P-17): blocks of its page
|
|
210
239
|
// and renderers of its page, its elements and its answers — its own.
|
|
@@ -353,7 +382,11 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
|
|
|
353
382
|
deployed,
|
|
354
383
|
visitor,
|
|
355
384
|
]);
|
|
356
|
-
const allPlugins = useMemo(() =>
|
|
385
|
+
const allPlugins = useMemo(() => 'problem' in preset
|
|
386
|
+
? plugins
|
|
387
|
+
: [appExtension(app, shown, preset.plugins), ...plugins],
|
|
388
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
389
|
+
[preset, plugins]);
|
|
357
390
|
/*
|
|
358
391
|
* On Datalayer, the application runs on a runtime: allocated with a plain
|
|
359
392
|
* agentspec, its agent created there with the application's spec — so that
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ReactorPlugin } from '@datalayer/reactor';
|
|
2
|
+
export declare const APP_RUNTIME_PLUGIN_NAME = "@datalayer/loop-plugin-app-runtime";
|
|
3
|
+
/** Follows the application's runtime, and tells the page what it holds. */
|
|
4
|
+
export declare function AppRuntimePlugins({ appId }: {
|
|
5
|
+
appId: string;
|
|
6
|
+
}): null;
|
|
7
|
+
/** The plugin that follows an application's runtime for its page. */
|
|
8
|
+
export declare function defineAppRuntimePlugin(appId: string): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The page told which of an application's plugins its runtime holds (LOOP
|
|
8
|
+
* F-15).
|
|
9
|
+
*
|
|
10
|
+
* The application's page plugin requires the runtime's plugin of the same
|
|
11
|
+
* name (`requiredBackendPlugins`). Reactor stands such a plugin down while
|
|
12
|
+
* the plugin it requires is not running, and brings it back when it is — but
|
|
13
|
+
* only a host that says what runs can make that true. Every place an
|
|
14
|
+
* application appears draws it with `AppRenderer`, so this is said once, for
|
|
15
|
+
* all of them — the Studio's Preview, its address, an embed, the browser
|
|
16
|
+
* alone:
|
|
17
|
+
*
|
|
18
|
+
* - on a runtime, Reactor's `useBackendPluginStream` follows the runtime's
|
|
19
|
+
* `/api/v1/apps/<id>/plugins/state` (and its stream): the page's plugin
|
|
20
|
+
* stands down when the runtime no longer holds the application, and comes
|
|
21
|
+
* back when it does;
|
|
22
|
+
* - with no runtime — the browser alone, or one still starting — the page is
|
|
23
|
+
* its own runtime, and the pair is whole: nothing stands down, nothing is
|
|
24
|
+
* blank.
|
|
25
|
+
*
|
|
26
|
+
* A plugin of its own, with no backend requirement, so that it keeps
|
|
27
|
+
* listening while the application's plugin is stood down. It renders nothing.
|
|
28
|
+
*
|
|
29
|
+
* @module loop/apps/AppRuntimePlugins
|
|
30
|
+
*/
|
|
31
|
+
import { useCallback, useEffect } from 'react';
|
|
32
|
+
import { definePlugin } from '@datalayer/reactor';
|
|
33
|
+
import { registerReactor, useBackendPluginStream, useReactorPlatform, useSignalValue, } from '@datalayer/reactor/react';
|
|
34
|
+
import { IDLE_SANDBOX_SNAPSHOT_SIGNAL, IDLE_SANDBOX_TARGET_SIGNAL, LoopSlots, } from '../core';
|
|
35
|
+
import { useOptionalSandboxService } from '../plugins/agents/useSandboxService';
|
|
36
|
+
import { appPluginName, appPluginsBase, appRuntimeOf } from './pluginPair';
|
|
37
|
+
export const APP_RUNTIME_PLUGIN_NAME = '@datalayer/loop-plugin-app-runtime';
|
|
38
|
+
/** A host's answer to "is this runtime plugin running?", from a list. */
|
|
39
|
+
const runningOf = (names) => (name) => names.includes(name);
|
|
40
|
+
/** Follows the application's runtime, and tells the page what it holds. */
|
|
41
|
+
export function AppRuntimePlugins({ appId }) {
|
|
42
|
+
const reactor = useReactorPlatform();
|
|
43
|
+
const service = useOptionalSandboxService();
|
|
44
|
+
const target = useSignalValue(service?.target ?? IDLE_SANDBOX_TARGET_SIGNAL);
|
|
45
|
+
const snapshot = useSignalValue(service?.snapshot ?? IDLE_SANDBOX_SNAPSHOT_SIGNAL);
|
|
46
|
+
const runtime = appRuntimeOf({
|
|
47
|
+
target,
|
|
48
|
+
snapshot,
|
|
49
|
+
serverUrl: service?.serverUrl,
|
|
50
|
+
});
|
|
51
|
+
const name = appPluginName(appId);
|
|
52
|
+
// No runtime: the page is its own, and the pair is whole.
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (runtime) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
registerReactor(reactor, runningOf([name]));
|
|
58
|
+
void reactor.setBackendPlugins([name]);
|
|
59
|
+
}, [reactor, runtime, name]);
|
|
60
|
+
// On one: what it says, for the slots gated on it too.
|
|
61
|
+
const onState = useCallback((state) => registerReactor(reactor, runningOf(state.plugins
|
|
62
|
+
.filter(plugin => plugin.enabled)
|
|
63
|
+
.map(plugin => plugin.name))), [reactor]);
|
|
64
|
+
useBackendPluginStream(runtime ? appPluginsBase(runtime, appId) : undefined, {
|
|
65
|
+
onState,
|
|
66
|
+
});
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/** The plugin that follows an application's runtime for its page. */
|
|
70
|
+
export function defineAppRuntimePlugin(appId) {
|
|
71
|
+
const Follow = () => _jsx(AppRuntimePlugins, { appId: appId });
|
|
72
|
+
return definePlugin({
|
|
73
|
+
name: APP_RUNTIME_PLUGIN_NAME,
|
|
74
|
+
displayName: 'Its runtime',
|
|
75
|
+
description: 'Tells the application’s page which of its plugins its runtime holds.',
|
|
76
|
+
octicon: 'server',
|
|
77
|
+
build: () => ({
|
|
78
|
+
components: [
|
|
79
|
+
{
|
|
80
|
+
slot: LoopSlots.status,
|
|
81
|
+
id: 'app-runtime-plugins',
|
|
82
|
+
Component: Follow,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -21,7 +21,14 @@ export type Feedback = {
|
|
|
21
21
|
session: string;
|
|
22
22
|
liked: boolean;
|
|
23
23
|
comment: string;
|
|
24
|
+
/**
|
|
25
|
+
* The answer it is about (LOOP P-24): `answer-<n>`, its place among the
|
|
26
|
+
* conversation's answers; none for the conversation as a whole.
|
|
27
|
+
*/
|
|
28
|
+
message?: string;
|
|
24
29
|
};
|
|
30
|
+
/** How an answer is named in feedback: its place among the conversation's answers, from 1. */
|
|
31
|
+
export declare const answerIdOf: (answers: number) => string | undefined;
|
|
25
32
|
/** What the feedback strip says, in a person's words. */
|
|
26
33
|
export declare const FEEDBACK_WORDS: {
|
|
27
34
|
readonly ask: "Was this answer useful?";
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/** The longest comment kept, as the runtime holds it (`COMMENT_LIMIT`). */
|
|
6
6
|
export const FEEDBACK_COMMENT_LIMIT = 2000;
|
|
7
|
+
/** How an answer is named in feedback: its place among the conversation's answers, from 1. */
|
|
8
|
+
export const answerIdOf = (answers) => answers > 0 ? `answer-${answers}` : undefined;
|
|
7
9
|
/** What the feedback strip says, in a person's words. */
|
|
8
10
|
export const FEEDBACK_WORDS = {
|
|
9
11
|
ask: 'Was this answer useful?',
|
|
@@ -50,6 +52,7 @@ export async function sendFeedback(feedback, { agentBaseUrl, token, fetcher = fe
|
|
|
50
52
|
session: feedback.session,
|
|
51
53
|
liked: feedback.liked,
|
|
52
54
|
comment: feedback.comment.trim(),
|
|
55
|
+
...(feedback.message ? { message: feedback.message } : {}),
|
|
53
56
|
}),
|
|
54
57
|
});
|
|
55
58
|
const body = (await response.json().catch(() => ({})));
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An application's two Reactor plugins, named once (LOOP F-15).
|
|
3
|
+
*
|
|
4
|
+
* Whichever editor wrote it — a spec, the Canvas, an `app.py` — an
|
|
5
|
+
* application is a pair of plugins of one name, `loop-app-<id>`: the page's
|
|
6
|
+
* (`defineAppPlugin`) and the runtime's (`manifest_of` in
|
|
7
|
+
* `agent_runtimes.loop.apps.plugins`, which says the same name). Each
|
|
8
|
+
* declares the other: the page's `requiredBackendPlugins`, the runtime's
|
|
9
|
+
* `frontend_dependencies`; and both are delivered as one extension of that
|
|
10
|
+
* name, which is how Reactor's manager and graph show them as one
|
|
11
|
+
* application.
|
|
12
|
+
*
|
|
13
|
+
* The page follows what its runtime holds with Reactor's
|
|
14
|
+
* `useBackendPluginStream`, on `appPluginsBase` (`AppRuntimePlugins`).
|
|
15
|
+
*
|
|
16
|
+
* @module loop/apps/pluginPair
|
|
17
|
+
*/
|
|
18
|
+
import type { SandboxSnapshot } from '../core';
|
|
19
|
+
import type { SandboxTarget } from '../plugins/agents/switchable';
|
|
20
|
+
/** The one name of an application's two plugins, the page's and the runtime's. */
|
|
21
|
+
export declare const appPluginName: (appId: string) => string;
|
|
22
|
+
/** What each tier of an application's pair says. */
|
|
23
|
+
export type AppPluginPair = {
|
|
24
|
+
/** The page's plugin, and the runtime plugin it cannot work without. */
|
|
25
|
+
page: {
|
|
26
|
+
name: string;
|
|
27
|
+
requiredBackendPlugins: string[];
|
|
28
|
+
};
|
|
29
|
+
/** The runtime's plugin, and the page plugin it cannot be used without. */
|
|
30
|
+
server: {
|
|
31
|
+
name: string;
|
|
32
|
+
frontendDependencies: string[];
|
|
33
|
+
};
|
|
34
|
+
/** The extension that delivers both: the application. */
|
|
35
|
+
extension: string;
|
|
36
|
+
};
|
|
37
|
+
/** The pair an application of this id is, on both tiers. */
|
|
38
|
+
export declare function appPluginPair(appId: string): AppPluginPair;
|
|
39
|
+
/**
|
|
40
|
+
* Where Reactor's `useBackendPluginStream` reads an application's plugins on
|
|
41
|
+
* its runtime: `<base>/plugins/state`, `<base>/events/stream` — about that
|
|
42
|
+
* application only, so that a runtime several share says nothing of the
|
|
43
|
+
* others.
|
|
44
|
+
*/
|
|
45
|
+
export declare const appPluginsBase: (runtimeUrl: string, appId: string) => string;
|
|
46
|
+
/**
|
|
47
|
+
* The runtime an application's page follows, or `undefined` when it runs on
|
|
48
|
+
* none yet.
|
|
49
|
+
*
|
|
50
|
+
* - **In the browser alone**: none — the page is its own runtime.
|
|
51
|
+
* - **On Datalayer**: the runtime its agent was made on, once reported
|
|
52
|
+
* (`agent_base_url`) — at its address, embedded, on the visitors' runtime.
|
|
53
|
+
* - **Here** (`local`, `jupyter`): the workspace's server, once it runs —
|
|
54
|
+
* after the agent was made there with the application, never before.
|
|
55
|
+
*/
|
|
56
|
+
export declare function appRuntimeOf({ target, snapshot, serverUrl, }: {
|
|
57
|
+
target: SandboxTarget | undefined;
|
|
58
|
+
snapshot: Pick<SandboxSnapshot, 'state' | 'agentBaseUrl'>;
|
|
59
|
+
serverUrl: string | undefined;
|
|
60
|
+
}): string | undefined;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** The one name of an application's two plugins, the page's and the runtime's. */
|
|
6
|
+
export const appPluginName = (appId) => `loop-app-${appId}`;
|
|
7
|
+
/** The pair an application of this id is, on both tiers. */
|
|
8
|
+
export function appPluginPair(appId) {
|
|
9
|
+
const name = appPluginName(appId);
|
|
10
|
+
return {
|
|
11
|
+
page: { name, requiredBackendPlugins: [name] },
|
|
12
|
+
server: { name, frontendDependencies: [name] },
|
|
13
|
+
extension: name,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Where Reactor's `useBackendPluginStream` reads an application's plugins on
|
|
18
|
+
* its runtime: `<base>/plugins/state`, `<base>/events/stream` — about that
|
|
19
|
+
* application only, so that a runtime several share says nothing of the
|
|
20
|
+
* others.
|
|
21
|
+
*/
|
|
22
|
+
export const appPluginsBase = (runtimeUrl, appId) => `${runtimeUrl.replace(/\/+$/, '')}/api/v1/apps/${encodeURIComponent(appId)}`;
|
|
23
|
+
/**
|
|
24
|
+
* The runtime an application's page follows, or `undefined` when it runs on
|
|
25
|
+
* none yet.
|
|
26
|
+
*
|
|
27
|
+
* - **In the browser alone**: none — the page is its own runtime.
|
|
28
|
+
* - **On Datalayer**: the runtime its agent was made on, once reported
|
|
29
|
+
* (`agent_base_url`) — at its address, embedded, on the visitors' runtime.
|
|
30
|
+
* - **Here** (`local`, `jupyter`): the workspace's server, once it runs —
|
|
31
|
+
* after the agent was made there with the application, never before.
|
|
32
|
+
*/
|
|
33
|
+
export function appRuntimeOf({ target, snapshot, serverUrl, }) {
|
|
34
|
+
if (!target || target === 'browser') {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (snapshot.agentBaseUrl) {
|
|
38
|
+
return snapshot.agentBaseUrl;
|
|
39
|
+
}
|
|
40
|
+
if (target === 'datalayer') {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return snapshot.state === 'running' && serverUrl ? serverUrl : undefined;
|
|
44
|
+
}
|