@datalayer/agent-runtimes 1.3.27 → 1.3.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/lib/chat/ChatFloating.d.ts +12 -2
  2. package/lib/chat/ChatFloating.js +196 -25
  3. package/lib/chat/assistant/AssistantStage.d.ts +70 -0
  4. package/lib/chat/assistant/AssistantStage.js +192 -0
  5. package/lib/chat/assistant/SpeechBalloon.d.ts +26 -0
  6. package/lib/chat/assistant/SpeechBalloon.js +46 -0
  7. package/lib/chat/assistant/SpriteCharacter.d.ts +26 -0
  8. package/lib/chat/assistant/SpriteCharacter.js +73 -0
  9. package/lib/chat/assistant/characters.d.ts +30 -0
  10. package/lib/chat/assistant/characters.js +41 -0
  11. package/lib/chat/assistant/formats/acs.d.ts +88 -0
  12. package/lib/chat/assistant/formats/acs.js +493 -0
  13. package/lib/chat/assistant/formats/agentCompression.d.ts +5 -0
  14. package/lib/chat/assistant/formats/agentCompression.js +92 -0
  15. package/lib/chat/assistant/formats/blobs.d.ts +6 -0
  16. package/lib/chat/assistant/formats/blobs.js +52 -0
  17. package/lib/chat/assistant/formats/clippy.d.ts +21 -0
  18. package/lib/chat/assistant/formats/clippy.js +167 -0
  19. package/lib/chat/assistant/formats/index.d.ts +13 -0
  20. package/lib/chat/assistant/formats/index.js +15 -0
  21. package/lib/chat/assistant/formats/objectLiteral.d.ts +23 -0
  22. package/lib/chat/assistant/formats/objectLiteral.js +233 -0
  23. package/lib/chat/assistant/formats/stateAnimations.d.ts +16 -0
  24. package/lib/chat/assistant/formats/stateAnimations.js +115 -0
  25. package/lib/chat/assistant/formats/types.d.ts +56 -0
  26. package/lib/chat/assistant/formats/types.js +11 -0
  27. package/lib/chat/assistant/state.d.ts +62 -0
  28. package/lib/chat/assistant/state.js +116 -0
  29. package/lib/chat/header/ChatViewModeToggle.js +6 -3
  30. package/lib/chat/presence/Presence.d.ts +9 -0
  31. package/lib/chat/presence/Presence.js +57 -0
  32. package/lib/chat/presence/presenceStatus.d.ts +32 -0
  33. package/lib/chat/presence/presenceStatus.js +46 -0
  34. package/lib/chat/viewModes.d.ts +6 -0
  35. package/lib/chat/viewModes.js +10 -1
  36. package/lib/examples/ChatAssistantExample.d.ts +8 -0
  37. package/lib/examples/ChatAssistantExample.js +59 -0
  38. package/lib/examples/example-selector.js +1 -0
  39. package/lib/examples/main.js +3 -0
  40. package/lib/loop/apps/AppRenderer.d.ts +8 -1
  41. package/lib/loop/apps/AppRenderer.js +2 -1
  42. package/lib/loop/apps/appspec.d.ts +5 -1
  43. package/lib/loop/apps/appspec.js +21 -1
  44. package/lib/loop/apps/checks.js +3 -1
  45. package/lib/loop/core/index.d.ts +15 -0
  46. package/lib/loop/core/index.js +1 -0
  47. package/lib/loop/plugins/assistant-characters/index.d.ts +30 -0
  48. package/lib/loop/plugins/assistant-characters/index.js +41 -0
  49. package/lib/loop/plugins/chat/ChatView.js +21 -1
  50. package/lib/loop/plugins/chat/index.d.ts +8 -0
  51. package/lib/loop/plugins/page-layout/index.js +5 -1
  52. package/lib/specs/actions.js +6 -0
  53. package/lib/specs/apps.d.ts +3 -0
  54. package/lib/specs/apps.js +793 -65
  55. package/lib/specs/appspecSchema.js +20 -2
  56. package/lib/types/agentspecs.d.ts +10 -1
  57. package/lib/types/chat.d.ts +1 -1
  58. package/package.json +1 -1
@@ -0,0 +1,167 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The clippy.js character format: an `agent.js` that registers the
7
+ * character's data (`clippy.ready('Name', {...})`), a `map.png` sprite sheet
8
+ * the frames index into, and optionally a `sounds-*.js` that registers its
9
+ * sounds as data URLs (`clippy.soundsReady('Name', {...})`). The JavaScript
10
+ * is read as data, never run.
11
+ *
12
+ * @module chat/assistant/formats/clippy
13
+ */
14
+ import { blobBytes, imageTypeOf, objectUrl } from './blobs';
15
+ import { readRegistrationCall } from './objectLiteral';
16
+ import { AssistantCharacterFormatError, } from './types';
17
+ function fail(message) {
18
+ throw new AssistantCharacterFormatError(message);
19
+ }
20
+ function isObject(value) {
21
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
22
+ }
23
+ function isCount(value) {
24
+ return typeof value === 'number' && Number.isInteger(value) && value >= 0;
25
+ }
26
+ function readFrame(raw, where, frameCount, overlayCount) {
27
+ if (!isObject(raw))
28
+ fail(`agent.js: ${where} is not an object.`);
29
+ const duration = raw.duration;
30
+ if (typeof duration !== 'number' ||
31
+ !Number.isFinite(duration) ||
32
+ duration < 0) {
33
+ fail(`agent.js: ${where} has no duration in milliseconds.`);
34
+ }
35
+ const frame = { duration, images: [] };
36
+ if (raw.images !== undefined) {
37
+ if (!Array.isArray(raw.images))
38
+ fail(`agent.js: the images of ${where} are not a list.`);
39
+ if (overlayCount > 0 && raw.images.length > overlayCount) {
40
+ fail(`agent.js: ${where} layers ${raw.images.length} images, more than its overlayCount of ${overlayCount}.`);
41
+ }
42
+ frame.images = raw.images.map((image, k) => {
43
+ if (!Array.isArray(image) ||
44
+ image.length !== 2 ||
45
+ !isCount(image[0]) ||
46
+ !isCount(image[1])) {
47
+ return fail(`agent.js: image ${k} of ${where} is not an [x, y] sprite offset.`);
48
+ }
49
+ return { x: image[0], y: image[1] };
50
+ });
51
+ }
52
+ if (raw.sound !== undefined && raw.sound !== null) {
53
+ if (typeof raw.sound !== 'string' && typeof raw.sound !== 'number') {
54
+ fail(`agent.js: the sound of ${where} is not a sound id.`);
55
+ }
56
+ frame.sound = String(raw.sound);
57
+ }
58
+ if (raw.exitBranch !== undefined && raw.exitBranch !== null) {
59
+ if (!isCount(raw.exitBranch) || raw.exitBranch >= frameCount) {
60
+ fail(`agent.js: the exitBranch of ${where} is not a frame of its animation.`);
61
+ }
62
+ frame.exitBranch = raw.exitBranch;
63
+ }
64
+ if (raw.branching !== undefined && raw.branching !== null) {
65
+ const branches = isObject(raw.branching)
66
+ ? raw.branching.branches
67
+ : undefined;
68
+ if (!Array.isArray(branches)) {
69
+ fail(`agent.js: the branching of ${where} has no list of branches.`);
70
+ }
71
+ frame.branching = branches.map((branch, k) => {
72
+ if (!isObject(branch) ||
73
+ !isCount(branch.frameIndex) ||
74
+ branch.frameIndex >= frameCount ||
75
+ typeof branch.weight !== 'number' ||
76
+ branch.weight < 0) {
77
+ return fail(`agent.js: branch ${k} of ${where} is not a frame of its animation with a weight.`);
78
+ }
79
+ return { frameIndex: branch.frameIndex, weight: branch.weight };
80
+ });
81
+ }
82
+ return frame;
83
+ }
84
+ /** Read the character data of an `agent.js` (no sprite, no sounds). */
85
+ export function readClippyAgent(agentJs) {
86
+ const { name, data } = readRegistrationCall(agentJs, /(?:[A-Za-z_$][\w$]*\.)*ready/, 'agent.js');
87
+ if (!isObject(data))
88
+ fail('agent.js does not register an object of character data.');
89
+ const size = data.framesize;
90
+ if (!Array.isArray(size) ||
91
+ size.length !== 2 ||
92
+ !isCount(size[0]) ||
93
+ !isCount(size[1]) ||
94
+ size[0] === 0 ||
95
+ size[1] === 0) {
96
+ fail('agent.js has no framesize: a [width, height] in pixels is required.');
97
+ }
98
+ let overlayCount = 1;
99
+ if (data.overlayCount !== undefined) {
100
+ if (!isCount(data.overlayCount) || data.overlayCount === 0) {
101
+ fail('agent.js: overlayCount is not a positive whole number.');
102
+ }
103
+ overlayCount = data.overlayCount;
104
+ }
105
+ if (!isObject(data.animations))
106
+ fail('agent.js has no animations.');
107
+ const animations = {};
108
+ for (const [animationName, raw] of Object.entries(data.animations)) {
109
+ if (!isObject(raw) ||
110
+ !Array.isArray(raw.frames) ||
111
+ raw.frames.length === 0) {
112
+ fail(`agent.js: the animation ${animationName} has no frames.`);
113
+ }
114
+ const frames = raw.frames;
115
+ animations[animationName] = {
116
+ frames: frames.map((frame, k) => readFrame(frame, `frame ${k} of the animation ${animationName}`, frames.length, overlayCount)),
117
+ };
118
+ }
119
+ if (Object.keys(animations).length === 0)
120
+ fail('agent.js has no animations.');
121
+ return {
122
+ name: name?.trim() || 'Character',
123
+ frameSize: { width: size[0], height: size[1] },
124
+ animations,
125
+ overlayCount,
126
+ };
127
+ }
128
+ /** Read the sounds of a `sounds-*.js`: sound id to (data) URL. */
129
+ export function readClippySounds(soundsJs) {
130
+ const { data } = readRegistrationCall(soundsJs, /(?:[A-Za-z_$][\w$]*\.)*soundsReady/, 'The sounds file');
131
+ if (!isObject(data))
132
+ fail('The sounds file does not register an object of sounds.');
133
+ const sounds = {};
134
+ for (const [id, url] of Object.entries(data)) {
135
+ if (typeof url !== 'string' || !/^data:audio\//.test(url)) {
136
+ fail(`The sounds file: sound ${id} is not an audio data URL.`);
137
+ }
138
+ sounds[id] = url;
139
+ }
140
+ return sounds;
141
+ }
142
+ /**
143
+ * Read a clippy.js character from the files a person picked. Everything
144
+ * stays in the page: the sprite becomes an object URL.
145
+ */
146
+ export async function readClippyCharacter(files) {
147
+ const agent = readClippyAgent(files.agentJs);
148
+ const sounds = files.soundsJs !== undefined ? readClippySounds(files.soundsJs) : undefined;
149
+ if (sounds) {
150
+ for (const [animationName, animation] of Object.entries(agent.animations)) {
151
+ animation.frames.forEach((frame, k) => {
152
+ if (frame.sound !== undefined && !(frame.sound in sounds)) {
153
+ fail(`Frame ${k} of the animation ${animationName} plays the sound ${frame.sound}, which the sounds file does not hold.`);
154
+ }
155
+ });
156
+ }
157
+ }
158
+ const head = await blobBytes(files.mapPng.slice(0, 16));
159
+ const type = imageTypeOf(head);
160
+ if (!type)
161
+ fail('map.png is not an image (PNG, GIF, JPEG or WebP).');
162
+ const sprite = objectUrl(files.mapPng.type === type
163
+ ? files.mapPng
164
+ : new Blob([files.mapPng], { type }));
165
+ const { overlayCount: _overlays, ...character } = agent;
166
+ return { ...character, sprite, ...(sounds ? { sounds } : {}) };
167
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Character files a person brings (LOOP T-26): clippy.js sprite maps and
3
+ * Microsoft Agent `.acs` files, read in the page into one shape.
4
+ *
5
+ * @module chat/assistant/formats
6
+ */
7
+ export * from './types';
8
+ export { readClippyCharacter, readClippyAgent, readClippySounds, } from './clippy';
9
+ export type { ClippyCharacterFiles } from './clippy';
10
+ export { readAcsCharacter, parseAcs, decodeAcsImage, layoutAcsSprite, composeAcsCell, } from './acs';
11
+ export type { AcsFile, AcsAnimation, AcsFrame, AcsImage } from './acs';
12
+ export { decompressAgentData } from './agentCompression';
13
+ export { stateAnimations } from './stateAnimations';
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Character files a person brings (LOOP T-26): clippy.js sprite maps and
7
+ * Microsoft Agent `.acs` files, read in the page into one shape.
8
+ *
9
+ * @module chat/assistant/formats
10
+ */
11
+ export * from './types';
12
+ export { readClippyCharacter, readClippyAgent, readClippySounds, } from './clippy';
13
+ export { readAcsCharacter, parseAcs, decodeAcsImage, layoutAcsSprite, composeAcsCell, } from './acs';
14
+ export { decompressAgentData } from './agentCompression';
15
+ export { stateAnimations } from './stateAnimations';
@@ -0,0 +1,23 @@
1
+ export type LiteralValue = string | number | boolean | null | undefined | LiteralValue[] | {
2
+ [key: string]: LiteralValue;
3
+ };
4
+ /** The result of reading one value: the value and where the text goes on. */
5
+ export interface LiteralRead {
6
+ value: LiteralValue;
7
+ end: number;
8
+ }
9
+ /**
10
+ * Read the literal value starting at `start` (whitespace and comments
11
+ * skipped). `what` names the file in error messages.
12
+ */
13
+ export declare function readObjectLiteral(text: string, start: number, what: string): LiteralRead;
14
+ /**
15
+ * The name and the data of a `ready('Name', {...})` call — the way clippy.js
16
+ * agent and sound files register themselves (`clippy.ready`,
17
+ * `clippy.agent.ready`, `clippy.soundsReady`) — or of a file that is only
18
+ * the object literal (JSON).
19
+ */
20
+ export declare function readRegistrationCall(text: string, callee: RegExp, what: string): {
21
+ name?: string;
22
+ data: LiteralValue;
23
+ };
@@ -0,0 +1,233 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Reads a JavaScript object literal from text without running it: JSON, plus
7
+ * what hand-written JavaScript adds — unquoted keys, single quotes, comments,
8
+ * trailing commas. Anything that would need evaluating (a call, a variable,
9
+ * an expression) is refused.
10
+ *
11
+ * @module chat/assistant/formats/objectLiteral
12
+ */
13
+ import { AssistantCharacterFormatError } from './types';
14
+ const IDENTIFIER_START = /[A-Za-z_$]/;
15
+ const IDENTIFIER_PART = /[A-Za-z0-9_$]/;
16
+ const MAX_DEPTH = 64;
17
+ /**
18
+ * Read the literal value starting at `start` (whitespace and comments
19
+ * skipped). `what` names the file in error messages.
20
+ */
21
+ export function readObjectLiteral(text, start, what) {
22
+ let i = start;
23
+ const fail = (message) => {
24
+ const line = text.slice(0, i).split('\n').length;
25
+ throw new AssistantCharacterFormatError(`${what} cannot be read at line ${line}: ${message}.`);
26
+ };
27
+ const skip = () => {
28
+ for (;;) {
29
+ const c = text[i];
30
+ if (c === ' ' ||
31
+ c === '\t' ||
32
+ c === '\n' ||
33
+ c === '\r' ||
34
+ c === '\uFEFF') {
35
+ i++;
36
+ }
37
+ else if (c === '/' && text[i + 1] === '/') {
38
+ while (i < text.length && text[i] !== '\n')
39
+ i++;
40
+ }
41
+ else if (c === '/' && text[i + 1] === '*') {
42
+ const close = text.indexOf('*/', i + 2);
43
+ if (close < 0)
44
+ fail('a comment is never closed');
45
+ i = close + 2;
46
+ }
47
+ else {
48
+ return;
49
+ }
50
+ }
51
+ };
52
+ const readString = () => {
53
+ const quote = text[i++];
54
+ let out = '';
55
+ for (;;) {
56
+ if (i >= text.length)
57
+ fail('a string is never closed');
58
+ const c = text[i++];
59
+ if (c === quote)
60
+ return out;
61
+ if (c === '\n')
62
+ fail('a string runs past the end of its line');
63
+ if (c !== '\\') {
64
+ out += c;
65
+ continue;
66
+ }
67
+ const e = text[i++];
68
+ switch (e) {
69
+ case 'n':
70
+ out += '\n';
71
+ break;
72
+ case 't':
73
+ out += '\t';
74
+ break;
75
+ case 'r':
76
+ out += '\r';
77
+ break;
78
+ case 'b':
79
+ out += '\b';
80
+ break;
81
+ case 'f':
82
+ out += '\f';
83
+ break;
84
+ case 'v':
85
+ out += '\v';
86
+ break;
87
+ case '0':
88
+ out += '\0';
89
+ break;
90
+ case 'x': {
91
+ const hex = text.slice(i, i + 2);
92
+ if (!/^[0-9a-fA-F]{2}$/.test(hex))
93
+ fail('a \\x escape is malformed');
94
+ out += String.fromCharCode(parseInt(hex, 16));
95
+ i += 2;
96
+ break;
97
+ }
98
+ case 'u': {
99
+ const hex = text.slice(i, i + 4);
100
+ if (!/^[0-9a-fA-F]{4}$/.test(hex))
101
+ fail('a \\u escape is malformed');
102
+ out += String.fromCharCode(parseInt(hex, 16));
103
+ i += 4;
104
+ break;
105
+ }
106
+ case '\r':
107
+ if (text[i] === '\n')
108
+ i++;
109
+ break;
110
+ case '\n':
111
+ break;
112
+ case undefined:
113
+ fail('a string is never closed');
114
+ break;
115
+ default:
116
+ out += e;
117
+ }
118
+ }
119
+ };
120
+ const readNumber = () => {
121
+ const match = /^[+-]?(?:0[xX][0-9a-fA-F]+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)/.exec(text.slice(i, i + 64));
122
+ if (!match)
123
+ return fail('a number is malformed');
124
+ i += match[0].length;
125
+ const n = Number(match[0]);
126
+ if (!Number.isFinite(n))
127
+ fail('a number is malformed');
128
+ return n;
129
+ };
130
+ const readIdentifier = () => {
131
+ const begin = i;
132
+ while (i < text.length && IDENTIFIER_PART.test(text[i]))
133
+ i++;
134
+ return text.slice(begin, i);
135
+ };
136
+ const readValue = (depth) => {
137
+ if (depth > MAX_DEPTH)
138
+ fail('it nests too deeply');
139
+ skip();
140
+ const c = text[i];
141
+ if (c === undefined)
142
+ fail('the text ends where a value was expected');
143
+ if (c === '{') {
144
+ i++;
145
+ const obj = Object.create(null);
146
+ for (;;) {
147
+ skip();
148
+ if (text[i] === '}') {
149
+ i++;
150
+ return obj;
151
+ }
152
+ let key;
153
+ const k = text[i];
154
+ if (k === '"' || k === "'")
155
+ key = readString();
156
+ else if (k !== undefined && /[0-9]/.test(k))
157
+ key = String(readNumber());
158
+ else if (k !== undefined && IDENTIFIER_START.test(k))
159
+ key = readIdentifier();
160
+ else
161
+ return fail(`a key was expected, found "${k ?? 'the end'}"`);
162
+ skip();
163
+ if (text[i] !== ':')
164
+ fail(`a ":" was expected after the key "${key}"`);
165
+ i++;
166
+ obj[key] = readValue(depth + 1);
167
+ skip();
168
+ if (text[i] === ',')
169
+ i++;
170
+ else if (text[i] !== '}')
171
+ fail('a "," or "}" was expected in an object');
172
+ }
173
+ }
174
+ if (c === '[') {
175
+ i++;
176
+ const arr = [];
177
+ for (;;) {
178
+ skip();
179
+ if (text[i] === ']') {
180
+ i++;
181
+ return arr;
182
+ }
183
+ arr.push(readValue(depth + 1));
184
+ skip();
185
+ if (text[i] === ',')
186
+ i++;
187
+ else if (text[i] !== ']')
188
+ fail('a "," or "]" was expected in a list');
189
+ }
190
+ }
191
+ if (c === '"' || c === "'")
192
+ return readString();
193
+ if (/[-+.0-9]/.test(c))
194
+ return readNumber();
195
+ if (IDENTIFIER_START.test(c)) {
196
+ const word = readIdentifier();
197
+ if (word === 'true')
198
+ return true;
199
+ if (word === 'false')
200
+ return false;
201
+ if (word === 'null')
202
+ return null;
203
+ if (word === 'undefined')
204
+ return undefined;
205
+ return fail(`"${word}" is code, not data, and is not run`);
206
+ }
207
+ return fail(`"${c}" is not the start of a value`);
208
+ };
209
+ const value = readValue(0);
210
+ return { value, end: i };
211
+ }
212
+ /**
213
+ * The name and the data of a `ready('Name', {...})` call — the way clippy.js
214
+ * agent and sound files register themselves (`clippy.ready`,
215
+ * `clippy.agent.ready`, `clippy.soundsReady`) — or of a file that is only
216
+ * the object literal (JSON).
217
+ */
218
+ export function readRegistrationCall(text, callee, what) {
219
+ const call = new RegExp(`(?:^|[^A-Za-z0-9_$])(?:${callee.source})\\s*\\(\\s*(["'])((?:\\\\.|(?!\\1).)*)\\1\\s*,`).exec(text);
220
+ if (call) {
221
+ const { value } = readObjectLiteral(text, call.index + call[0].length, what);
222
+ return { name: call[2], data: value };
223
+ }
224
+ const first = text.replace(/^\uFEFF/, '').trimStart();
225
+ if (first.startsWith('{')) {
226
+ const { value, end } = readObjectLiteral(text, text.indexOf('{'), what);
227
+ if (text.slice(end).trim().replace(/;$/, '') !== '') {
228
+ throw new AssistantCharacterFormatError(`${what} holds more than one object: only the character's data is read.`);
229
+ }
230
+ return { data: value };
231
+ }
232
+ throw new AssistantCharacterFormatError(`${what} is not a clippy.js file: it has no ready('Name', {...}) call and is not a JSON object.`);
233
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Which of a character's animations act each state of the assistant (T-22),
3
+ * by the names the characters of Office use — the same in clippy.js maps and
4
+ * in Microsoft Agent files — and by the states an Agent character declares.
5
+ *
6
+ * @module chat/assistant/formats/stateAnimations
7
+ */
8
+ import type { AssistantState } from '../state';
9
+ import type { AssistantCharacterData } from './types';
10
+ /**
11
+ * For each state, the animation names to pick from, by what the character
12
+ * offers: the names Office characters use, then the states an Agent file
13
+ * declares; failing both, the idle animations; failing those, the first
14
+ * animation. Never empty.
15
+ */
16
+ export declare function stateAnimations(character: AssistantCharacterData): Record<AssistantState, string[]>;
@@ -0,0 +1,115 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** Names, in order of preference, matched without case. */
6
+ const NAMES = {
7
+ idle: [],
8
+ thinking: [
9
+ 'Thinking',
10
+ 'Think',
11
+ 'CheckingSomething',
12
+ 'Searching',
13
+ 'Search',
14
+ 'Uncertain',
15
+ ],
16
+ working: [
17
+ 'Processing',
18
+ 'Working',
19
+ 'Writing',
20
+ 'Write',
21
+ 'Searching',
22
+ 'Search',
23
+ 'GetTechy',
24
+ 'CheckingSomething',
25
+ 'Read',
26
+ 'Reading',
27
+ ],
28
+ waiting: [
29
+ 'GetAttention',
30
+ 'Alert',
31
+ 'Suggest',
32
+ 'LookDown',
33
+ 'GestureDown',
34
+ 'Acknowledge',
35
+ ],
36
+ greeting: ['Greeting', 'Greet', 'Show', 'Wave'],
37
+ speaking: [
38
+ 'Explain',
39
+ 'Speak',
40
+ 'Speaking',
41
+ 'Announce',
42
+ 'GestureRight',
43
+ 'GestureLeft',
44
+ ],
45
+ goodbye: ['GoodBye', 'Goodbye', 'Hide', 'Wave'],
46
+ };
47
+ /** States a Microsoft Agent character declares, for each assistant state. */
48
+ const AUTHORED = {
49
+ idle: ['IdlingLevel1', 'IdlingLevel2', 'IdlingLevel3'],
50
+ thinking: [],
51
+ working: [],
52
+ waiting: ['Listening'],
53
+ greeting: ['Showing'],
54
+ speaking: ['Speaking'],
55
+ goodbye: ['Hiding'],
56
+ };
57
+ const IDLE = /^(idle|rest ?pose)/i;
58
+ const STATES = [
59
+ 'idle',
60
+ 'thinking',
61
+ 'working',
62
+ 'waiting',
63
+ 'greeting',
64
+ 'speaking',
65
+ 'goodbye',
66
+ ];
67
+ /**
68
+ * For each state, the animation names to pick from, by what the character
69
+ * offers: the names Office characters use, then the states an Agent file
70
+ * declares; failing both, the idle animations; failing those, the first
71
+ * animation. Never empty.
72
+ */
73
+ export function stateAnimations(character) {
74
+ const names = Object.keys(character.animations);
75
+ if (names.length === 0) {
76
+ throw new Error(`The character ${character.name} has no animations.`);
77
+ }
78
+ const byLower = new Map();
79
+ for (const name of names) {
80
+ if (!byLower.has(name.toLowerCase()))
81
+ byLower.set(name.toLowerCase(), name);
82
+ }
83
+ const authored = new Map();
84
+ for (const [state, list] of Object.entries(character.authoredStates ?? {})) {
85
+ authored.set(state.toLowerCase().replace(/\s+/g, ''), list);
86
+ }
87
+ const pick = (wanted) => {
88
+ const out = [];
89
+ for (const want of wanted) {
90
+ const name = byLower.get(want.toLowerCase());
91
+ if (name && !out.includes(name))
92
+ out.push(name);
93
+ }
94
+ return out;
95
+ };
96
+ const idleByName = names.filter(name => IDLE.test(name));
97
+ const idleAuthored = pick(AUTHORED.idle.flatMap(s => authored.get(s.toLowerCase()) ?? []));
98
+ const idle = [...new Set([...idleByName, ...idleAuthored])];
99
+ const fallback = idle.length > 0 ? idle : [names[0]];
100
+ const result = {};
101
+ for (const state of STATES) {
102
+ if (state === 'idle') {
103
+ result.idle = fallback;
104
+ continue;
105
+ }
106
+ const found = [
107
+ ...new Set([
108
+ ...pick(NAMES[state]),
109
+ ...pick(AUTHORED[state].flatMap(s => authored.get(s.toLowerCase()) ?? [])),
110
+ ]),
111
+ ];
112
+ result[state] = found.length > 0 ? found : fallback;
113
+ }
114
+ return result;
115
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The one shape a character file is read into (LOOP T-26), whatever its
3
+ * format: a sprite sheet, frames that index into it, animations by name.
4
+ *
5
+ * @module chat/assistant/formats/types
6
+ */
7
+ /** One frame of an animation. */
8
+ export interface AssistantCharacterFrame {
9
+ /** How long the frame shows, in milliseconds. */
10
+ duration: number;
11
+ /**
12
+ * The sprite offsets of the frame's images, layered bottom to top: each is
13
+ * the top-left corner, in the sprite sheet, of a `frameSize` cell (drawn as
14
+ * `background-position: -x px -y px`). An empty list is an empty frame.
15
+ */
16
+ images: Array<{
17
+ x: number;
18
+ y: number;
19
+ }>;
20
+ /** The id of the sound played when the frame shows, a key of `sounds`. */
21
+ sound?: string;
22
+ /** Where the animation may jump after this frame, weights in percent. */
23
+ branching?: {
24
+ frameIndex: number;
25
+ weight: number;
26
+ }[];
27
+ /** The frame to go to when the animation is asked to end early. */
28
+ exitBranch?: number;
29
+ }
30
+ /** A named sequence of frames. */
31
+ export interface AssistantCharacterAnimation {
32
+ frames: AssistantCharacterFrame[];
33
+ }
34
+ /** A character, read from a file a person holds the rights to. */
35
+ export interface AssistantCharacterData {
36
+ name: string;
37
+ frameSize: {
38
+ width: number;
39
+ height: number;
40
+ };
41
+ /** The sprite sheet: a URL (object URL or data URL) the frames' offsets index into. */
42
+ sprite: string;
43
+ animations: Record<string, AssistantCharacterAnimation>;
44
+ /** Sound id to URL. */
45
+ sounds?: Record<string, string>;
46
+ /**
47
+ * The character's own grouping of its animations into states, when the
48
+ * format carries one (a Microsoft Agent character does: `Showing`,
49
+ * `Hiding`, `Speaking`, `IdlingLevel1`…). Read by `stateAnimations`.
50
+ */
51
+ authoredStates?: Record<string, string[]>;
52
+ }
53
+ /** A character file that cannot be read: the message says what is wrong. */
54
+ export declare class AssistantCharacterFormatError extends Error {
55
+ constructor(message: string);
56
+ }
@@ -0,0 +1,11 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** A character file that cannot be read: the message says what is wrong. */
6
+ export class AssistantCharacterFormatError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'AssistantCharacterFormatError';
10
+ }
11
+ }