@datalayer/agent-runtimes 1.3.95 → 1.3.96
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/dist-embed/datalayer-app-main.js +3 -3
- package/dist-embed/datalayer-app.js +1 -1
- package/lib/apps/__tests__/fixtures/sceneCheckCases.json +352 -0
- package/lib/apps/apps/sceneChecks.d.ts +92 -0
- package/lib/apps/apps/sceneChecks.js +649 -0
- package/lib/apps/apps/sceneShape.d.ts +8 -0
- package/lib/apps/apps/sceneShape.js +191 -0
- package/lib/apps/apps/sceneYaml.d.ts +64 -0
- package/lib/apps/apps/sceneYaml.js +253 -0
- package/lib/apps/core/canvasParts.d.ts +28 -0
- package/lib/apps/core/canvasParts.js +14 -0
- package/lib/apps/core/index.d.ts +1 -0
- package/lib/apps/core/index.js +2 -0
- package/lib/apps/embed/loader.js +10 -0
- package/lib/apps/plugins/agents/SandboxSelector.js +13 -2
- package/lib/apps/plugins/agents/host.d.ts +2 -0
- package/lib/apps/plugins/agents/host.js +16 -0
- package/lib/apps/plugins/agents/plugin.js +10 -2
- package/lib/apps/plugins/agents/switchable.d.ts +12 -0
- package/lib/apps/plugins/agents/switchable.js +23 -0
- package/lib/apps/plugins/canvas-parts/index.d.ts +44 -0
- package/lib/apps/plugins/canvas-parts/index.js +209 -0
- package/lib/hooks/useAgUi.js +4 -1
- package/lib/protocols/AGUIAdapter.js +5 -1
- package/lib/protocols/agUiRefusal.d.ts +47 -0
- package/lib/protocols/agUiRefusal.js +87 -0
- package/lib/specs/apps.js +20 -12
- package/lib/specs/scenes.js +103 -98
- package/lib/specs/teams/teams.js +1 -1
- package/package.json +3 -1
- package/scripts/dist-embed.mjs +116 -0
- package/scripts/record-scene-checks.py +600 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { SCENE_SCHEMA } from '../../specs/sceneSchema';
|
|
6
|
+
import { sectionOfKey } from './sceneChecks';
|
|
7
|
+
const DEFS = (SCENE_SCHEMA.$defs ?? {});
|
|
8
|
+
/**
|
|
9
|
+
* Field names pydantic also takes for an alias (`populate_by_name`): the
|
|
10
|
+
* schema names `schema` and `as`, the model `schema_` and `shown_as`.
|
|
11
|
+
*/
|
|
12
|
+
const FIELD_NAMES = {
|
|
13
|
+
schema_: 'schema',
|
|
14
|
+
shown_as: 'as',
|
|
15
|
+
};
|
|
16
|
+
const isMapping = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
+
/** A value as Python prints it in a choice: `'browser'`. */
|
|
18
|
+
const python = (value) => typeof value === 'string'
|
|
19
|
+
? `'${value}'`
|
|
20
|
+
: value === null
|
|
21
|
+
? 'None'
|
|
22
|
+
: typeof value === 'boolean'
|
|
23
|
+
? value
|
|
24
|
+
? 'True'
|
|
25
|
+
: 'False'
|
|
26
|
+
: String(value);
|
|
27
|
+
/** pydantic's list of choices: `'a'`, `'a' or 'b'`, `'a', 'b' or 'c'`. */
|
|
28
|
+
const choices = (values) => values.length <= 1
|
|
29
|
+
? values.map(python).join('')
|
|
30
|
+
: `${values.slice(0, -1).map(python).join(', ')} or ${python(values[values.length - 1])}`;
|
|
31
|
+
/** The definition a `$ref` names, and its name. */
|
|
32
|
+
const resolved = (schema) => {
|
|
33
|
+
if (typeof schema.$ref === 'string') {
|
|
34
|
+
const model = schema.$ref.split('/').pop() ?? '';
|
|
35
|
+
return { schema: DEFS[model] ?? {}, model };
|
|
36
|
+
}
|
|
37
|
+
return { schema };
|
|
38
|
+
};
|
|
39
|
+
const said = (loc, message) => [
|
|
40
|
+
{ loc, kind: 'said', message },
|
|
41
|
+
];
|
|
42
|
+
/** A number as pydantic reads one in lax mode, or why it does not. */
|
|
43
|
+
function numberIssue(value, integer) {
|
|
44
|
+
if (typeof value === 'boolean') {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === 'number') {
|
|
48
|
+
return integer && !Number.isInteger(value)
|
|
49
|
+
? 'Input should be a valid integer, got a number with a fractional part'
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
const read = Number(value.trim());
|
|
54
|
+
if (value.trim() === '' || Number.isNaN(read)) {
|
|
55
|
+
return integer
|
|
56
|
+
? 'Input should be a valid integer, unable to parse string as an integer'
|
|
57
|
+
: 'Input should be a valid number, unable to parse string as a number';
|
|
58
|
+
}
|
|
59
|
+
return integer && !Number.isInteger(read)
|
|
60
|
+
? 'Input should be a valid integer, unable to parse string as an integer'
|
|
61
|
+
: undefined;
|
|
62
|
+
}
|
|
63
|
+
return integer
|
|
64
|
+
? 'Input should be a valid integer'
|
|
65
|
+
: 'Input should be a valid number';
|
|
66
|
+
}
|
|
67
|
+
function issuesOf(given, value, loc) {
|
|
68
|
+
// `X | null`: nothing is fine, anything else is read as X.
|
|
69
|
+
if (Array.isArray(given.anyOf)) {
|
|
70
|
+
const branches = given.anyOf.filter(branch => branch.type !== 'null');
|
|
71
|
+
if (value === null && branches.length < given.anyOf.length) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
if (branches.length === 1) {
|
|
75
|
+
return issuesOf(branches[0], value, loc);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const { schema, model } = resolved(given);
|
|
79
|
+
if (Array.isArray(schema.enum)) {
|
|
80
|
+
return schema.enum.includes(value)
|
|
81
|
+
? []
|
|
82
|
+
: said(loc, `Input should be ${choices(schema.enum)}`);
|
|
83
|
+
}
|
|
84
|
+
switch (schema.type) {
|
|
85
|
+
case 'object':
|
|
86
|
+
return objectIssues(schema, model, value, loc);
|
|
87
|
+
case 'array': {
|
|
88
|
+
if (!Array.isArray(value)) {
|
|
89
|
+
return said(loc, 'Input should be a valid list');
|
|
90
|
+
}
|
|
91
|
+
const items = schema.items;
|
|
92
|
+
return items
|
|
93
|
+
? value.flatMap((item, index) => issuesOf(items, item, [...loc, index]))
|
|
94
|
+
: [];
|
|
95
|
+
}
|
|
96
|
+
case 'string': {
|
|
97
|
+
if (typeof value !== 'string') {
|
|
98
|
+
return said(loc, 'Input should be a valid string');
|
|
99
|
+
}
|
|
100
|
+
const pattern = schema.pattern;
|
|
101
|
+
return typeof pattern === 'string' && !new RegExp(pattern).test(value)
|
|
102
|
+
? said(loc, `String should match pattern '${pattern}'`)
|
|
103
|
+
: [];
|
|
104
|
+
}
|
|
105
|
+
case 'number':
|
|
106
|
+
case 'integer': {
|
|
107
|
+
const wrong = numberIssue(value, schema.type === 'integer');
|
|
108
|
+
if (wrong) {
|
|
109
|
+
return said(loc, wrong);
|
|
110
|
+
}
|
|
111
|
+
const read = Number(value);
|
|
112
|
+
if (typeof schema.minimum === 'number' && read < schema.minimum) {
|
|
113
|
+
return said(loc, `Input should be greater than or equal to ${schema.minimum}`);
|
|
114
|
+
}
|
|
115
|
+
if (typeof schema.maximum === 'number' && read > schema.maximum) {
|
|
116
|
+
return said(loc, `Input should be less than or equal to ${schema.maximum}`);
|
|
117
|
+
}
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
case 'boolean':
|
|
121
|
+
return typeof value === 'boolean'
|
|
122
|
+
? []
|
|
123
|
+
: said(loc, 'Input should be a valid boolean');
|
|
124
|
+
default:
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function objectIssues(schema, model, value, loc) {
|
|
129
|
+
if (!isMapping(value)) {
|
|
130
|
+
return said(loc, model
|
|
131
|
+
? `Input should be a valid dictionary or instance of ${model}`
|
|
132
|
+
: 'Input should be a valid dictionary');
|
|
133
|
+
}
|
|
134
|
+
const properties = schema.properties;
|
|
135
|
+
if (!properties) {
|
|
136
|
+
// A mapping of one kind of thing: `positions`, each a place.
|
|
137
|
+
const each = schema.additionalProperties;
|
|
138
|
+
return typeof each === 'object' && each !== null
|
|
139
|
+
? Object.entries(value).flatMap(([key, item]) => issuesOf(each, item, [...loc, key]))
|
|
140
|
+
: [];
|
|
141
|
+
}
|
|
142
|
+
const given = Object.fromEntries(Object.entries(value).map(([key, item]) => [FIELD_NAMES[key] ?? key, item]));
|
|
143
|
+
const required = Array.isArray(schema.required)
|
|
144
|
+
? schema.required
|
|
145
|
+
: [];
|
|
146
|
+
// Each field in the model's order, then what the model has no field for.
|
|
147
|
+
const issues = Object.entries(properties).flatMap(([key, field]) => key in given
|
|
148
|
+
? issuesOf(field, given[key], [...loc, key])
|
|
149
|
+
: required.includes(key)
|
|
150
|
+
? [{ loc: [...loc, key], kind: 'missing' }]
|
|
151
|
+
: []);
|
|
152
|
+
if (schema.additionalProperties === false) {
|
|
153
|
+
for (const key of Object.keys(given)) {
|
|
154
|
+
if (!(key in properties)) {
|
|
155
|
+
issues.push({ loc: [...loc, key], kind: 'extra' });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return issues;
|
|
160
|
+
}
|
|
161
|
+
/** One issue as agentspecs says it (`_sentences`). */
|
|
162
|
+
const sentence = (issue) => {
|
|
163
|
+
const where = issue.loc.join('.');
|
|
164
|
+
if (issue.kind === 'missing') {
|
|
165
|
+
return `${where} is missing`;
|
|
166
|
+
}
|
|
167
|
+
if (issue.kind === 'extra') {
|
|
168
|
+
return `${where} is not a field of the spec`;
|
|
169
|
+
}
|
|
170
|
+
return where ? `${where}: ${issue.message}` : (issue.message ?? '');
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* What `loop` says of a scene's shape — every issue, after the scene's id —
|
|
174
|
+
* filed under the part of the scene its first issue is about; nothing when
|
|
175
|
+
* the shape is right. `data` is the text read as plain data, in agentspecs'
|
|
176
|
+
* spelling.
|
|
177
|
+
*/
|
|
178
|
+
export function sceneShapeSays(data) {
|
|
179
|
+
const issues = issuesOf(SCENE_SCHEMA, data, []);
|
|
180
|
+
if (!issues.length) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
const id = isMapping(data) && 'id' in data
|
|
184
|
+
? python(data.id).replace(/^'|'$/g, '')
|
|
185
|
+
: 'the scene';
|
|
186
|
+
const first = String(issues[0].loc[0] ?? '');
|
|
187
|
+
return {
|
|
188
|
+
says: `${id}: ${issues.map(sentence).join('; ')}`,
|
|
189
|
+
section: sectionOfKey(first),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { SceneSpec } from '../../types/scenes';
|
|
2
|
+
import { type SceneOwnApps, type SceneProblem, type SceneSection } from './sceneChecks';
|
|
3
|
+
export type { SceneSpec };
|
|
4
|
+
/** What a scene spec says of itself. */
|
|
5
|
+
export declare const SCENE_SCHEMA = "loop.scene/v1";
|
|
6
|
+
/** A spec with nothing said yet: signed-in people may watch, the transcript shows everything. */
|
|
7
|
+
export declare const emptySceneSpec: () => SceneSpec;
|
|
8
|
+
/**
|
|
9
|
+
* What is written: the spec without what says nothing — an empty string, an
|
|
10
|
+
* empty list, an empty map — as `generate_apps.py` drops them, so the text
|
|
11
|
+
* beside the stage reads as a person would write it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function compactScene(value: unknown): unknown;
|
|
14
|
+
/**
|
|
15
|
+
* The keys agentspecs spells with an underscore, as the TypeScript spec
|
|
16
|
+
* spells them (`generate_scenes.py`'s `CAMEL`): the text beside the stage
|
|
17
|
+
* is written as agentspecs reads it, so that a scene's YAML pastes in and
|
|
18
|
+
* the schema's completion names what is there.
|
|
19
|
+
*/
|
|
20
|
+
export declare const TEXT_KEYS: Readonly<Record<string, string>>;
|
|
21
|
+
/**
|
|
22
|
+
* What a scene spec carries that is not written by hand: what it names that
|
|
23
|
+
* is not enabled today (`setup`, computed by agentspecs' `scene_setup`) and
|
|
24
|
+
* the last rehearsal played (`played`, what *Live* is read from). agentspecs
|
|
25
|
+
* refuses both in a scene file — they are read off a scene, never into one —
|
|
26
|
+
* so the text does not write them, and an edit of the text does not erase
|
|
27
|
+
* them (`sceneWithDerived`).
|
|
28
|
+
*/
|
|
29
|
+
export declare const SCENE_DERIVED_KEYS: readonly ["setup", "played"];
|
|
30
|
+
/** The spec with what is not written by hand carried over from another: the one the text was read from. */
|
|
31
|
+
export declare const sceneWithDerived: (spec: SceneSpec, from: SceneSpec) => SceneSpec;
|
|
32
|
+
/**
|
|
33
|
+
* The scene as YAML, spelled as agentspecs spells it: the text under the
|
|
34
|
+
* stage (S-01, S-08), edited there as on the stage (`sceneOfYaml`). What is
|
|
35
|
+
* not written by hand is left out, so the text reads as a scene file
|
|
36
|
+
* (`SCENE_DERIVED_KEYS`).
|
|
37
|
+
*/
|
|
38
|
+
export declare function sceneYamlOf(spec: SceneSpec): string;
|
|
39
|
+
/**
|
|
40
|
+
* The scene the text says, in the words the problem is said with when it
|
|
41
|
+
* says none: not YAML, not a mapping, not `loop.scene/v1`. A spec that is
|
|
42
|
+
* one is laid over the empty spec, as one read from its item is.
|
|
43
|
+
*/
|
|
44
|
+
export declare function sceneOfYaml(text: string): {
|
|
45
|
+
spec?: SceneSpec;
|
|
46
|
+
problem?: string;
|
|
47
|
+
/** The part of the scene the problem is about, when it is about one. */
|
|
48
|
+
section?: SceneSection;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* A spec said in part, laid over the empty one: a part left unsaid — of the
|
|
52
|
+
* scene, of a cast member, of a system of the setting, of the rehearsal —
|
|
53
|
+
* reads as empty rather than as `undefined`, so that what was written
|
|
54
|
+
* compact (`compactScene`) reads back whole.
|
|
55
|
+
*/
|
|
56
|
+
export declare function sceneOfData(spec: Partial<SceneSpec>): SceneSpec;
|
|
57
|
+
/**
|
|
58
|
+
* What the text says of itself (S-08): what stands in the way, each in
|
|
59
|
+
* agentspecs' sentence with the part of the scene it is about — a text that
|
|
60
|
+
* is no scene first of all, since nothing else can be read of it.
|
|
61
|
+
*/
|
|
62
|
+
export declare function sceneTextProblems(text: string, own?: SceneOwnApps): SceneProblem[];
|
|
63
|
+
/** The member the audience talks to, as the text says it (`entryOf`); empty while it does not read. */
|
|
64
|
+
export declare function sceneTextEntry(text: string): string;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* A scene's spec, `loop.scene/v1`, as text: how it is read and how it is
|
|
7
|
+
* written — what `appspec.ts` is for an application.
|
|
8
|
+
*
|
|
9
|
+
* agentspecs writes a scene in its own spelling (`talks_to`, `runs_in`) and
|
|
10
|
+
* agent-runtimes' `SceneSpec` in its own (`talksTo`, `runsIn`); a text in
|
|
11
|
+
* either reads (`sceneOfYaml`) and is written back in agentspecs' spelling
|
|
12
|
+
* (`sceneYamlOf`), so that what an editor saves is what `loop` reads. What is
|
|
13
|
+
* read *off* a scene rather than written into one — its `setup`, its recorded
|
|
14
|
+
* `played` — is never written into the text and never erased by an edit of it
|
|
15
|
+
* (`SCENE_DERIVED_KEYS`, `sceneWithDerived`). A text's own refusals are the
|
|
16
|
+
* scene's checks over what it reads (`sceneTextProblems`).
|
|
17
|
+
*
|
|
18
|
+
* Moved from the landing's Studio on 2026-10-10 with `sceneChecks.ts`
|
|
19
|
+
* (plans/STUDIO.md S-10), so that every shell editing a scene reads and writes
|
|
20
|
+
* it alike. Pure: no React, no network.
|
|
21
|
+
*
|
|
22
|
+
* @module apps/apps/sceneYaml
|
|
23
|
+
*/
|
|
24
|
+
import { parse, stringify } from 'yaml';
|
|
25
|
+
import { entryOf, sceneCheck, } from './sceneChecks';
|
|
26
|
+
import { sceneShapeSays } from './sceneShape';
|
|
27
|
+
/** What a scene spec says of itself. */
|
|
28
|
+
export const SCENE_SCHEMA = 'loop.scene/v1';
|
|
29
|
+
/** A spec with nothing said yet: signed-in people may watch, the transcript shows everything. */
|
|
30
|
+
export const emptySceneSpec = () => ({
|
|
31
|
+
schema: SCENE_SCHEMA,
|
|
32
|
+
id: '',
|
|
33
|
+
version: '0.0.1',
|
|
34
|
+
name: '',
|
|
35
|
+
description: '',
|
|
36
|
+
tags: [],
|
|
37
|
+
icon: '',
|
|
38
|
+
emoji: '',
|
|
39
|
+
team: '',
|
|
40
|
+
entry: '',
|
|
41
|
+
cast: [],
|
|
42
|
+
setting: { systems: [], period: '', language: 'en', assumes: '' },
|
|
43
|
+
script: [],
|
|
44
|
+
stage: {
|
|
45
|
+
positions: {},
|
|
46
|
+
opensFirst: '',
|
|
47
|
+
transcript: { tools: true, narration: true },
|
|
48
|
+
inspectors: [],
|
|
49
|
+
restsAfter: '',
|
|
50
|
+
pace: 'steady',
|
|
51
|
+
},
|
|
52
|
+
audience: { who: 'signed-in', ceilingPerAsk: 0, asksADay: 0 },
|
|
53
|
+
rehearsal: {
|
|
54
|
+
beats: [],
|
|
55
|
+
within: '',
|
|
56
|
+
verified: { live: [], recorded: [], unverified: [] },
|
|
57
|
+
},
|
|
58
|
+
deployment: { account: '', page: '', addresses: {} },
|
|
59
|
+
setup: [],
|
|
60
|
+
});
|
|
61
|
+
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
62
|
+
/**
|
|
63
|
+
* What is written: the spec without what says nothing — an empty string, an
|
|
64
|
+
* empty list, an empty map — as `generate_apps.py` drops them, so the text
|
|
65
|
+
* beside the stage reads as a person would write it.
|
|
66
|
+
*/
|
|
67
|
+
export function compactScene(value) {
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return value.map(compactScene);
|
|
70
|
+
}
|
|
71
|
+
if (isRecord(value)) {
|
|
72
|
+
const kept = {};
|
|
73
|
+
for (const [key, item] of Object.entries(value)) {
|
|
74
|
+
if (item === '' || item === undefined || item === null) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(item) && item.length === 0) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (isRecord(item) && Object.keys(item).length === 0) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
kept[key] = compactScene(item);
|
|
84
|
+
}
|
|
85
|
+
return kept;
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The keys agentspecs spells with an underscore, as the TypeScript spec
|
|
91
|
+
* spells them (`generate_scenes.py`'s `CAMEL`): the text beside the stage
|
|
92
|
+
* is written as agentspecs reads it, so that a scene's YAML pastes in and
|
|
93
|
+
* the schema's completion names what is there.
|
|
94
|
+
*/
|
|
95
|
+
export const TEXT_KEYS = {
|
|
96
|
+
runs_in: 'runsIn',
|
|
97
|
+
talks_to: 'talksTo',
|
|
98
|
+
opens_first: 'opensFirst',
|
|
99
|
+
rests_after: 'restsAfter',
|
|
100
|
+
ceiling_per_ask: 'ceilingPerAsk',
|
|
101
|
+
asks_a_day: 'asksADay',
|
|
102
|
+
must_say: 'mustSay',
|
|
103
|
+
must_not_say: 'mustNotSay',
|
|
104
|
+
};
|
|
105
|
+
const SPEC_KEYS = Object.fromEntries(Object.entries(TEXT_KEYS).map(([text, spec]) => [spec, text]));
|
|
106
|
+
/** Every key renamed by a map, deep; a key the map does not name — a member's id among them — is left as it is. */
|
|
107
|
+
function respelled(value, keys) {
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
return value.map(item => respelled(item, keys));
|
|
110
|
+
}
|
|
111
|
+
if (isRecord(value)) {
|
|
112
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
113
|
+
keys[key] ?? key,
|
|
114
|
+
respelled(item, keys),
|
|
115
|
+
]));
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* What a scene spec carries that is not written by hand: what it names that
|
|
121
|
+
* is not enabled today (`setup`, computed by agentspecs' `scene_setup`) and
|
|
122
|
+
* the last rehearsal played (`played`, what *Live* is read from). agentspecs
|
|
123
|
+
* refuses both in a scene file — they are read off a scene, never into one —
|
|
124
|
+
* so the text does not write them, and an edit of the text does not erase
|
|
125
|
+
* them (`sceneWithDerived`).
|
|
126
|
+
*/
|
|
127
|
+
export const SCENE_DERIVED_KEYS = ['setup', 'played'];
|
|
128
|
+
/** The spec with what is not written by hand carried over from another: the one the text was read from. */
|
|
129
|
+
export const sceneWithDerived = (spec, from) => ({
|
|
130
|
+
...spec,
|
|
131
|
+
setup: from.setup ?? [],
|
|
132
|
+
...(from.played ? { played: from.played } : {}),
|
|
133
|
+
});
|
|
134
|
+
/**
|
|
135
|
+
* The scene as YAML, spelled as agentspecs spells it: the text under the
|
|
136
|
+
* stage (S-01, S-08), edited there as on the stage (`sceneOfYaml`). What is
|
|
137
|
+
* not written by hand is left out, so the text reads as a scene file
|
|
138
|
+
* (`SCENE_DERIVED_KEYS`).
|
|
139
|
+
*/
|
|
140
|
+
export function sceneYamlOf(spec) {
|
|
141
|
+
const written = Object.fromEntries(Object.entries(spec).filter(([key]) => !SCENE_DERIVED_KEYS.includes(key)));
|
|
142
|
+
return stringify(respelled(compactScene(written), SPEC_KEYS), {
|
|
143
|
+
lineWidth: 0,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The scene the text says, in the words the problem is said with when it
|
|
148
|
+
* says none: not YAML, not a mapping, not `loop.scene/v1`. A spec that is
|
|
149
|
+
* one is laid over the empty spec, as one read from its item is.
|
|
150
|
+
*/
|
|
151
|
+
export function sceneOfYaml(text) {
|
|
152
|
+
let parsed;
|
|
153
|
+
try {
|
|
154
|
+
parsed = parse(text);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const why = error instanceof Error ? error.message.split('\n')[0] : String(error);
|
|
158
|
+
return { problem: `The text does not read: ${why}` };
|
|
159
|
+
}
|
|
160
|
+
if (!isRecord(parsed)) {
|
|
161
|
+
return {
|
|
162
|
+
problem: 'The text is not a scene: it starts with its name and its cast.',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (parsed.schema !== SCENE_SCHEMA) {
|
|
166
|
+
return {
|
|
167
|
+
problem: `The text is not a scene: it says schema ${JSON.stringify(parsed.schema ?? '')}, and a scene says ${SCENE_SCHEMA}.`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// A scene of the wrong shape is refused as `loop` refuses it, before it is
|
|
171
|
+
// read into a spec: read into one, a cast written as one word made the
|
|
172
|
+
// reader throw (`sceneShape`, 2026-10-10). Checked as agentspecs spells it.
|
|
173
|
+
const shape = sceneShapeSays(respelled(parsed, SPEC_KEYS));
|
|
174
|
+
if (shape) {
|
|
175
|
+
return { problem: shape.says, section: shape.section };
|
|
176
|
+
}
|
|
177
|
+
// Spelled as agentspecs spells it, or as the spec does: both read.
|
|
178
|
+
return {
|
|
179
|
+
spec: sceneOfData(respelled(parsed, TEXT_KEYS)),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/** A cast member with nothing said: what `compactScene` dropped reads as empty again. */
|
|
183
|
+
const EMPTY_MEMBER = {
|
|
184
|
+
app: '',
|
|
185
|
+
ref: '',
|
|
186
|
+
server: '',
|
|
187
|
+
brief: '',
|
|
188
|
+
persona: { name: '', face: '', line: '' },
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* A spec said in part, laid over the empty one: a part left unsaid — of the
|
|
192
|
+
* scene, of a cast member, of a system of the setting, of the rehearsal —
|
|
193
|
+
* reads as empty rather than as `undefined`, so that what was written
|
|
194
|
+
* compact (`compactScene`) reads back whole.
|
|
195
|
+
*/
|
|
196
|
+
export function sceneOfData(spec) {
|
|
197
|
+
const empty = emptySceneSpec();
|
|
198
|
+
const setting = spec.setting ?? {};
|
|
199
|
+
const rehearsal = spec.rehearsal ?? {};
|
|
200
|
+
return {
|
|
201
|
+
...empty,
|
|
202
|
+
...spec,
|
|
203
|
+
schema: SCENE_SCHEMA,
|
|
204
|
+
cast: (spec.cast ?? []).map(member => ({
|
|
205
|
+
...EMPTY_MEMBER,
|
|
206
|
+
...member,
|
|
207
|
+
persona: { ...EMPTY_MEMBER.persona, ...(member.persona ?? {}) },
|
|
208
|
+
})),
|
|
209
|
+
setting: {
|
|
210
|
+
...empty.setting,
|
|
211
|
+
...setting,
|
|
212
|
+
systems: (setting.systems ?? []).map(system => ({
|
|
213
|
+
...system,
|
|
214
|
+
as: system.as ?? '',
|
|
215
|
+
holds: system.holds ?? '',
|
|
216
|
+
})),
|
|
217
|
+
},
|
|
218
|
+
script: spec.script ?? [],
|
|
219
|
+
stage: {
|
|
220
|
+
...empty.stage,
|
|
221
|
+
...(spec.stage ?? {}),
|
|
222
|
+
transcript: {
|
|
223
|
+
...empty.stage.transcript,
|
|
224
|
+
...(spec.stage?.transcript ?? {}),
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
audience: { ...empty.audience, ...(spec.audience ?? {}) },
|
|
228
|
+
rehearsal: {
|
|
229
|
+
...empty.rehearsal,
|
|
230
|
+
...rehearsal,
|
|
231
|
+
verified: { ...empty.rehearsal.verified, ...(rehearsal.verified ?? {}) },
|
|
232
|
+
},
|
|
233
|
+
deployment: { ...empty.deployment, ...(spec.deployment ?? {}) },
|
|
234
|
+
setup: spec.setup ?? [],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* What the text says of itself (S-08): what stands in the way, each in
|
|
239
|
+
* agentspecs' sentence with the part of the scene it is about — a text that
|
|
240
|
+
* is no scene first of all, since nothing else can be read of it.
|
|
241
|
+
*/
|
|
242
|
+
export function sceneTextProblems(text, own) {
|
|
243
|
+
const read = sceneOfYaml(text);
|
|
244
|
+
if (!read.spec) {
|
|
245
|
+
return [{ says: read.problem ?? '', section: read.section ?? 'scene' }];
|
|
246
|
+
}
|
|
247
|
+
return sceneCheck(read.spec, own, { written: true });
|
|
248
|
+
}
|
|
249
|
+
/** The member the audience talks to, as the text says it (`entryOf`); empty while it does not read. */
|
|
250
|
+
export function sceneTextEntry(text) {
|
|
251
|
+
const read = sceneOfYaml(text);
|
|
252
|
+
return read.spec ? entryOf(read.spec) : '';
|
|
253
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The kinds of part an agent is built from on the Canvas. */
|
|
2
|
+
export type CanvasPartKind = 'connection' | 'rule' | 'skill' | 'test' | 'event' | 'model';
|
|
3
|
+
/**
|
|
4
|
+
* A part of an agent the Canvas may place (LOOP C-20): a connection to a
|
|
5
|
+
* server of the catalogue, a skill, a model, a rule, a test, an event —
|
|
6
|
+
* contributed by the plugin of what it comes from. The toolbox is what the
|
|
7
|
+
* enabled plugins contribute here, and nothing else: a plugin disabled takes
|
|
8
|
+
* its parts off it, as it takes its blocks off the palette.
|
|
9
|
+
*/
|
|
10
|
+
export type CanvasPartContribution = {
|
|
11
|
+
/** Unique among the parts: its kind and what it is, `connection:tavily`. */
|
|
12
|
+
id: string;
|
|
13
|
+
kind: CanvasPartKind;
|
|
14
|
+
/** What the toolbox calls it. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** One line of what it is. */
|
|
17
|
+
says: string;
|
|
18
|
+
emoji?: string;
|
|
19
|
+
/**
|
|
20
|
+
* What it arrives with, spelled as the Appspec's text spells it: a
|
|
21
|
+
* connection's, a rule's, a test's or an event's object, a skill's or a
|
|
22
|
+
* model's id. Every example is one the Appspec's schema takes as it is.
|
|
23
|
+
*/
|
|
24
|
+
example: unknown;
|
|
25
|
+
/** Whether what it names is enabled today; a part that is not is offered, and its application's setup notes say so. */
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
};
|
|
28
|
+
export declare const LoopCanvasPart: import("@datalayer/reactor").ContributionPoint<CanvasPartContribution>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The agent's toolbox as a contribution point (LOOP C-20), beside the
|
|
7
|
+
* Canvas's palette of blocks (`canvasBlocks`, C-12): kept apart so that
|
|
8
|
+
* reading the toolbox loads the reactor and the catalogue, and nothing of the
|
|
9
|
+
* workspace.
|
|
10
|
+
*
|
|
11
|
+
* @module apps/core/canvasParts
|
|
12
|
+
*/
|
|
13
|
+
import { defineContributionPoint } from '@datalayer/reactor';
|
|
14
|
+
export const LoopCanvasPart = defineContributionPoint('loop.canvas.part');
|
package/lib/apps/core/index.d.ts
CHANGED
|
@@ -598,6 +598,7 @@ export type AssistantMenuContribution = {
|
|
|
598
598
|
};
|
|
599
599
|
export declare const LoopAssistantMenu: import("@datalayer/reactor").ContributionPoint<AssistantMenuContribution>;
|
|
600
600
|
export { LoopCanvasBlock, type CanvasBlockContribution } from './canvasBlocks';
|
|
601
|
+
export { LoopCanvasPart, type CanvasPartContribution, type CanvasPartKind, } from './canvasParts';
|
|
601
602
|
export { LoopA2uiComponent, type A2uiComponentContribution, } from './a2uiComponents';
|
|
602
603
|
/**
|
|
603
604
|
* The per-example chat extras a host feeds the loop's conversation live.
|
package/lib/apps/core/index.js
CHANGED
|
@@ -173,6 +173,8 @@ export const LoopAssistantMenu = defineContributionPoint('loop.assistant.menu');
|
|
|
173
173
|
// A block the Canvas may place (LOOP C-12), in a module of its own so that
|
|
174
174
|
// the palette is read without the rest of the contracts.
|
|
175
175
|
export { LoopCanvasBlock } from './canvasBlocks';
|
|
176
|
+
// A part of an agent the Canvas may place (LOOP C-20), beside the blocks.
|
|
177
|
+
export { LoopCanvasPart, } from './canvasParts';
|
|
176
178
|
// A component an application adds to the catalog it is drawn with (LOOP
|
|
177
179
|
// P-17): the components its developer wrote, for its workspace alone.
|
|
178
180
|
export { LoopA2uiComponent, } from './a2uiComponents';
|
package/lib/apps/embed/loader.js
CHANGED
|
@@ -28,6 +28,16 @@
|
|
|
28
28
|
console.error('datalayer-app: the script has to be loaded by a script tag with a src, so it knows where its module is.');
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
+
// `@jupyter-widgets` assigns to a bare `__webpack_public_path__` at the top
|
|
32
|
+
// of the module (see `loop-main.tsx`): in an ES module that is a
|
|
33
|
+
// ReferenceError unless the global exists, and the module's own body runs
|
|
34
|
+
// after everything it imports, so it is declared here, before the import —
|
|
35
|
+
// without it no application loads (`… did not load (__webpack_public_path__
|
|
36
|
+
// is not defined)`, seen with `loop apps run --web`, LOOP P-08).
|
|
37
|
+
const globals = window;
|
|
38
|
+
if (globals['__webpack_public_path__'] === undefined) {
|
|
39
|
+
globals['__webpack_public_path__'] = '';
|
|
40
|
+
}
|
|
31
41
|
const main = new URL(MODULE_FILE, script.src).href;
|
|
32
42
|
import(/* @vite-ignore */ main).catch((error) => {
|
|
33
43
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -6,7 +6,7 @@ import { KernelIndicator } from '@datalayer/jupyter-react/kernel-indicator';
|
|
|
6
6
|
import { useSignalValue } from '@datalayer/reactor/react';
|
|
7
7
|
import { IDLE_SANDBOX_SNAPSHOT_SIGNAL, sandboxIndicatorState, } from '../../core';
|
|
8
8
|
import { useOptionalSandboxService } from './useSandboxService';
|
|
9
|
-
import { SANDBOX_TARGETS, TARGET_SPECS, } from './switchable';
|
|
9
|
+
import { SANDBOX_TARGETS, TARGET_SPECS, heldTargetReason, } from './switchable';
|
|
10
10
|
export function SandboxSelector(_props) {
|
|
11
11
|
const service = useOptionalSandboxService();
|
|
12
12
|
const [moving, setMoving] = useState(false);
|
|
@@ -58,7 +58,18 @@ export function SandboxSelector(_props) {
|
|
|
58
58
|
p: 0,
|
|
59
59
|
'& > li': { listStyle: 'none' },
|
|
60
60
|
'& > li::marker': { content: 'none' },
|
|
61
|
-
}, children: SANDBOX_TARGETS.map((entry, position) =>
|
|
61
|
+
}, children: SANDBOX_TARGETS.map((entry, position) => {
|
|
62
|
+
// Held here: drawn, said why on hover, not taken. `aria-disabled`
|
|
63
|
+
// rather than `disabled`: a disabled button takes no pointer, and
|
|
64
|
+
// its reason would never be read.
|
|
65
|
+
const held = heldTargetReason(entry);
|
|
66
|
+
return (_jsx(SegmentedControl.Button, { selected: position === index, "aria-disabled": held ? true : undefined, "data-sandbox-target": entry, "data-sandbox-target-held": held ? '' : undefined, sx: {
|
|
67
|
+
listStyle: 'none',
|
|
68
|
+
...(held
|
|
69
|
+
? { color: 'fg.subtle', cursor: 'not-allowed' }
|
|
70
|
+
: null),
|
|
71
|
+
}, title: held ?? TARGET_SPECS[entry].hint, onClick: () => (held ? undefined : void choose(entry)), children: TARGET_SPECS[entry].label }, entry));
|
|
72
|
+
}) }), failure || snapshot.errorReason ? (
|
|
62
73
|
// The reason, in the header, in full. It used to read "switch failed"
|
|
63
74
|
// with the cause hidden in a `title` — which is why a switch that
|
|
64
75
|
// could not reach the server looked like a click that did nothing.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* What the page the agent is drawn in can offer, asked in one place.
|
|
7
|
+
*
|
|
8
|
+
* A local agent and the Jupyter server it starts run beside JupyterLab, not
|
|
9
|
+
* in a web page (`heldTargetReason`). The question is jupyter-react's; it is
|
|
10
|
+
* asked here so that a test can answer it without loading jupyter-react.
|
|
11
|
+
*
|
|
12
|
+
* @module apps/plugins/agents/host
|
|
13
|
+
*/
|
|
14
|
+
import { loadJupyterConfig } from '@datalayer/jupyter-react';
|
|
15
|
+
/** Whether the page is JupyterLab's own. */
|
|
16
|
+
export const insideJupyterLab = () => loadJupyterConfig().insideJupyterLab;
|
|
@@ -12,8 +12,10 @@ import { AnonymousKeyBadge, AnonymousKeyHeaderBadge, } from './AnonymousKeyBadge
|
|
|
12
12
|
import { SandboxSelector } from './SandboxSelector';
|
|
13
13
|
import { TeamMemberPicker } from './TeamMemberPicker';
|
|
14
14
|
import { createTeamSelection } from './team';
|
|
15
|
-
import { createSwitchableSandboxService, TARGET_SPECS, targetHasAgent, } from './switchable';
|
|
15
|
+
import { createSwitchableSandboxService, heldTargetReason, TARGET_SPECS, targetHasAgent, } from './switchable';
|
|
16
16
|
export const AGENTS_PLUGIN_NAME = '@datalayer/loop-plugin-agents';
|
|
17
|
+
/** The target asked for, or the page itself when this page holds it. */
|
|
18
|
+
const offeredTarget = (target) => heldTargetReason(target) ? 'browser' : target;
|
|
17
19
|
export const AgentsPlugin = definePlugin({
|
|
18
20
|
name: AGENTS_PLUGIN_NAME,
|
|
19
21
|
displayName: 'Agents',
|
|
@@ -43,7 +45,13 @@ export const AgentsPlugin = definePlugin({
|
|
|
43
45
|
// must not be started on a target the reader cannot move off — unless
|
|
44
46
|
// the host fixed the target itself, as an application deployed on
|
|
45
47
|
// Datalayer is (AppRenderer): there is nothing for the reader to move.
|
|
46
|
-
|
|
48
|
+
//
|
|
49
|
+
// And never on a target this page holds: *Local*, the default, is
|
|
50
|
+
// held on the web (`heldTargetReason`), so a page starts in itself.
|
|
51
|
+
// A host that names no target gets the service's own default, Local —
|
|
52
|
+
// read here, so that it is held on the web like any other.
|
|
53
|
+
initialTarget: offeredTarget((showVariants || config.targetFixed ? config.target : 'browser') ??
|
|
54
|
+
'local'),
|
|
47
55
|
kernelSource: config.kernelSource,
|
|
48
56
|
localAgent: config.localAgent,
|
|
49
57
|
// The capacity plugins' say, read at switch time — lazily, because a
|