@datalayer/agent-runtimes 1.3.28 → 1.3.30

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 (99) hide show
  1. package/lib/AgentNode.js +14 -33
  2. package/lib/chat/ChatFloating.d.ts +12 -2
  3. package/lib/chat/ChatFloating.js +196 -25
  4. package/lib/chat/assistant/AssistantStage.d.ts +70 -0
  5. package/lib/chat/assistant/AssistantStage.js +192 -0
  6. package/lib/chat/assistant/SpeechBalloon.d.ts +26 -0
  7. package/lib/chat/assistant/SpeechBalloon.js +46 -0
  8. package/lib/chat/assistant/SpriteCharacter.d.ts +26 -0
  9. package/lib/chat/assistant/SpriteCharacter.js +73 -0
  10. package/lib/chat/assistant/characters.d.ts +30 -0
  11. package/lib/chat/assistant/characters.js +41 -0
  12. package/lib/chat/assistant/formats/acs.d.ts +88 -0
  13. package/lib/chat/assistant/formats/acs.js +493 -0
  14. package/lib/chat/assistant/formats/agentCompression.d.ts +5 -0
  15. package/lib/chat/assistant/formats/agentCompression.js +92 -0
  16. package/lib/chat/assistant/formats/blobs.d.ts +6 -0
  17. package/lib/chat/assistant/formats/blobs.js +52 -0
  18. package/lib/chat/assistant/formats/clippy.d.ts +21 -0
  19. package/lib/chat/assistant/formats/clippy.js +167 -0
  20. package/lib/chat/assistant/formats/index.d.ts +13 -0
  21. package/lib/chat/assistant/formats/index.js +15 -0
  22. package/lib/chat/assistant/formats/objectLiteral.d.ts +23 -0
  23. package/lib/chat/assistant/formats/objectLiteral.js +233 -0
  24. package/lib/chat/assistant/formats/stateAnimations.d.ts +16 -0
  25. package/lib/chat/assistant/formats/stateAnimations.js +115 -0
  26. package/lib/chat/assistant/formats/types.d.ts +56 -0
  27. package/lib/chat/assistant/formats/types.js +11 -0
  28. package/lib/chat/assistant/state.d.ts +62 -0
  29. package/lib/chat/assistant/state.js +116 -0
  30. package/lib/chat/base/ChatBase.js +41 -32
  31. package/lib/chat/base/modelChoice.d.ts +13 -0
  32. package/lib/chat/base/modelChoice.js +13 -0
  33. package/lib/chat/header/ChatViewModeToggle.js +6 -3
  34. package/lib/{loop/plugins/chat → chat/presence}/Presence.js +1 -1
  35. package/lib/{loop/plugins/chat → chat/presence}/presenceStatus.d.ts +7 -1
  36. package/lib/{loop/plugins/chat → chat/presence}/presenceStatus.js +18 -0
  37. package/lib/chat/viewModes.d.ts +6 -0
  38. package/lib/chat/viewModes.js +10 -1
  39. package/lib/examples/ChatAssistantExample.d.ts +8 -0
  40. package/lib/examples/ChatAssistantExample.js +59 -0
  41. package/lib/examples/example-selector.js +1 -0
  42. package/lib/examples/main.js +3 -0
  43. package/lib/loop/apps/AppRenderer.d.ts +34 -2
  44. package/lib/loop/apps/AppRenderer.js +96 -29
  45. package/lib/loop/apps/agent.d.ts +15 -0
  46. package/lib/loop/apps/agent.js +42 -0
  47. package/lib/loop/apps/appspec.d.ts +5 -1
  48. package/lib/loop/apps/appspec.js +21 -1
  49. package/lib/loop/apps/checks.js +3 -1
  50. package/lib/loop/apps/index.d.ts +1 -0
  51. package/lib/loop/apps/index.js +1 -0
  52. package/lib/loop/core/index.d.ts +23 -0
  53. package/lib/loop/core/index.js +1 -0
  54. package/lib/loop/embed/AppEmbed.d.ts +42 -0
  55. package/lib/loop/embed/AppEmbed.js +205 -0
  56. package/lib/loop/embed/datalayer-app.d.ts +1 -0
  57. package/lib/loop/embed/datalayer-app.js +16 -0
  58. package/lib/loop/embed/element.d.ts +57 -0
  59. package/lib/loop/embed/element.js +405 -0
  60. package/lib/loop/embed/embedConfig.d.ts +120 -0
  61. package/lib/loop/embed/embedConfig.js +148 -0
  62. package/lib/loop/embed/embedTheme.d.ts +24 -0
  63. package/lib/loop/embed/embedTheme.js +83 -0
  64. package/lib/loop/index.d.ts +6 -2
  65. package/lib/loop/index.js +8 -2
  66. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +21 -3
  67. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +34 -6
  68. package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
  69. package/lib/loop/plugins/agent-capacity/index.js +3 -1
  70. package/lib/loop/plugins/agents/switchable.d.ts +7 -0
  71. package/lib/loop/plugins/agents/switchable.js +91 -88
  72. package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
  73. package/lib/loop/plugins/app-page/AppPage.js +53 -0
  74. package/lib/loop/plugins/app-page/appPage.d.ts +180 -0
  75. package/lib/loop/plugins/app-page/appPage.js +443 -0
  76. package/lib/loop/plugins/app-page/index.d.ts +8 -0
  77. package/lib/loop/plugins/app-page/index.js +34 -0
  78. package/lib/loop/plugins/assistant-characters/index.d.ts +30 -0
  79. package/lib/loop/plugins/assistant-characters/index.js +41 -0
  80. package/lib/loop/plugins/chat/ChatView.js +50 -39
  81. package/lib/loop/plugins/chat/index.d.ts +1 -1
  82. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
  83. package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
  84. package/lib/loop/plugins/page-layout/index.d.ts +13 -0
  85. package/lib/loop/plugins/page-layout/index.js +15 -2
  86. package/lib/loop/presets.d.ts +12 -0
  87. package/lib/loop/presets.js +5 -3
  88. package/lib/specs/actions.js +14 -0
  89. package/lib/specs/agents/agents.js +4 -0
  90. package/lib/specs/apps.d.ts +13 -1
  91. package/lib/specs/apps.js +2715 -373
  92. package/lib/specs/appspecSchema.js +21 -3
  93. package/lib/types/agentspecs.d.ts +21 -1
  94. package/lib/types/chat.d.ts +1 -1
  95. package/lib/types/config.d.ts +12 -0
  96. package/package.json +2 -1
  97. package/scripts/codegen/generate_agents.py +50 -2
  98. package/scripts/codegen/generate_apps.py +36 -6
  99. /package/lib/{loop/plugins/chat → chat/presence}/Presence.d.ts +0 -0
@@ -0,0 +1,493 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The Microsoft Agent character format (`.acs`), read in the page after
7
+ * Lebeau's public description (MSAgent Character Data Specification 1.3):
8
+ * an ACSHEADER of four locators — the character (ACSCHARACTERINFO: size,
9
+ * transparent colour, palette, states, names), the animations
10
+ * (ACSANIMATIONINFO, each frame a list of images with offsets, a sound, a
11
+ * duration in 1/100 s, branches), the images (ACSIMAGEINFO: 8-bit
12
+ * bottom-up bitmaps, most compressed) and the sounds (RIFF WAVE).
13
+ *
14
+ * Each distinct frame is composited from its images, as Agent draws it, into
15
+ * one cell of a sprite sheet, so a frame of the result is a single offset.
16
+ *
17
+ * @module chat/assistant/formats/acs
18
+ */
19
+ import { decompressAgentData } from './agentCompression';
20
+ import { objectUrl } from './blobs';
21
+ import { AssistantCharacterFormatError, } from './types';
22
+ export const ACS_SIGNATURE = 0xabcdabc3;
23
+ export const ACF_SIGNATURE = 0xabcdabc4;
24
+ /** Character style flags (ACSCHARACTERINFO). */
25
+ const STYLE_VOICE = 0x20;
26
+ const STYLE_BALLOON = 0x200;
27
+ /** The widest a sprite sheet is laid out, and the largest it may be. */
28
+ const SHEET_MAX_WIDTH = 4096;
29
+ const SHEET_MAX_SIDE = 16384;
30
+ class Reader {
31
+ bytes;
32
+ view;
33
+ pos = 0;
34
+ constructor(bytes) {
35
+ this.bytes = bytes;
36
+ this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
37
+ }
38
+ need(count, what) {
39
+ if (count < 0 || this.pos + count > this.bytes.length) {
40
+ throw new AssistantCharacterFormatError(`This .acs ends early: ${what} at byte ${this.pos} runs past the end of the file.`);
41
+ }
42
+ }
43
+ seek(locator, what) {
44
+ if (locator.offset + locator.size > this.bytes.length) {
45
+ throw new AssistantCharacterFormatError(`This .acs is damaged: ${what} is said to be at byte ${locator.offset}, past the end of the file.`);
46
+ }
47
+ this.pos = locator.offset;
48
+ }
49
+ u8(what) {
50
+ this.need(1, what);
51
+ return this.view.getUint8(this.pos++);
52
+ }
53
+ u16(what) {
54
+ this.need(2, what);
55
+ const v = this.view.getUint16(this.pos, true);
56
+ this.pos += 2;
57
+ return v;
58
+ }
59
+ i16(what) {
60
+ this.need(2, what);
61
+ const v = this.view.getInt16(this.pos, true);
62
+ this.pos += 2;
63
+ return v;
64
+ }
65
+ u32(what) {
66
+ this.need(4, what);
67
+ const v = this.view.getUint32(this.pos, true);
68
+ this.pos += 4;
69
+ return v;
70
+ }
71
+ skip(count, what) {
72
+ this.need(count, what);
73
+ this.pos += count;
74
+ }
75
+ take(count, what) {
76
+ this.need(count, what);
77
+ const out = this.bytes.subarray(this.pos, this.pos + count);
78
+ this.pos += count;
79
+ return out;
80
+ }
81
+ locator(what) {
82
+ return { offset: this.u32(what), size: this.u32(what) };
83
+ }
84
+ /** STRING: a ULONG count of UTF-16 characters, then a null terminator. */
85
+ string(what) {
86
+ const count = this.u32(what);
87
+ if (count === 0)
88
+ return '';
89
+ this.need((count + 1) * 2, what);
90
+ let out = '';
91
+ for (let k = 0; k < count; k++) {
92
+ out += String.fromCharCode(this.view.getUint16(this.pos + k * 2, true));
93
+ }
94
+ this.pos += (count + 1) * 2;
95
+ return out;
96
+ }
97
+ }
98
+ function readCharacterInfo(r, at) {
99
+ r.seek(at, 'the character information');
100
+ const minor = r.u16('the version');
101
+ const major = r.u16('the version');
102
+ const localized = r.locator('the names locator');
103
+ r.skip(16, 'the character id');
104
+ const width = r.u16('the character width');
105
+ const height = r.u16('the character height');
106
+ const transparentIndex = r.u8('the transparent colour');
107
+ const style = r.u32('the character style');
108
+ r.skip(4, 'the animation set version');
109
+ if (style & STYLE_VOICE) {
110
+ r.skip(16 + 16 + 4 + 2, 'the voice');
111
+ if (r.u8('the voice') !== 0) {
112
+ r.skip(2, 'the voice language');
113
+ r.string('the voice dialect');
114
+ r.skip(4, 'the voice gender and age');
115
+ r.string('the voice style');
116
+ }
117
+ }
118
+ if (style & STYLE_BALLOON) {
119
+ r.skip(2 + 12, 'the balloon');
120
+ r.string('the balloon font');
121
+ r.skip(4 + 4 + 1 + 1, 'the balloon font');
122
+ }
123
+ const colours = r.u32('the colour table');
124
+ if (colours === 0 || colours > 256) {
125
+ throw new AssistantCharacterFormatError(`This .acs is damaged or of an unknown version: its colour table says ${colours} colours, where 1 to 256 are expected.`);
126
+ }
127
+ const palette = new Uint32Array(256);
128
+ const table = r.take(colours * 4, 'the colour table');
129
+ for (let k = 0; k < colours; k++) {
130
+ // RGBQUAD is blue, green, red, reserved; stored as RGBA in little-endian order.
131
+ const b = table[k * 4];
132
+ const g = table[k * 4 + 1];
133
+ const red = table[k * 4 + 2];
134
+ palette[k] = (0xff << 24) | (b << 16) | (g << 8) | red;
135
+ }
136
+ if (r.u8('the tray icon flag') !== 0) {
137
+ r.skip(r.u32('the tray icon'), 'the tray icon');
138
+ r.skip(r.u32('the tray icon'), 'the tray icon');
139
+ }
140
+ const states = {};
141
+ const stateCount = r.u16('the states');
142
+ for (let s = 0; s < stateCount; s++) {
143
+ const name = r.string('a state name');
144
+ const count = r.u16('the animations of a state');
145
+ const names = [];
146
+ for (let k = 0; k < count; k++)
147
+ names.push(r.string('an animation of a state'));
148
+ states[name] = names;
149
+ }
150
+ let name = '';
151
+ if (localized.size > 0) {
152
+ r.seek(localized, 'the character names');
153
+ const count = r.u16('the character names');
154
+ const names = [];
155
+ for (let k = 0; k < count; k++) {
156
+ const lang = r.u16('a name language');
157
+ const value = r.string('a character name');
158
+ r.string('a character description');
159
+ r.string('a character extra data');
160
+ names.push({ lang, name: value });
161
+ }
162
+ // English (0x09 primary language) when there is one, else the first.
163
+ name = (names.find(n => (n.lang & 0x3ff) === 0x09) ?? names[0])?.name ?? '';
164
+ }
165
+ return {
166
+ version: { major, minor },
167
+ width,
168
+ height,
169
+ transparentIndex,
170
+ palette,
171
+ states,
172
+ name: name.trim() || 'Character',
173
+ };
174
+ }
175
+ function readFrames(r, animation) {
176
+ const frameCount = r.u16(`the frames of ${animation}`);
177
+ const frames = [];
178
+ for (let f = 0; f < frameCount; f++) {
179
+ const where = `frame ${f} of ${animation}`;
180
+ const imageCount = r.u16(where);
181
+ const images = [];
182
+ for (let k = 0; k < imageCount; k++) {
183
+ images.push({ image: r.u32(where), x: r.i16(where), y: r.i16(where) });
184
+ }
185
+ const sound = r.i16(where);
186
+ const duration = r.u16(where);
187
+ const exitBranch = r.i16(where);
188
+ const branches = [];
189
+ const branchCount = r.u8(where);
190
+ for (let k = 0; k < branchCount; k++) {
191
+ branches.push({ frameIndex: r.u16(where), probability: r.u16(where) });
192
+ }
193
+ const overlayCount = r.u8(where);
194
+ for (let k = 0; k < overlayCount; k++) {
195
+ // Mouth overlays, drawn only while Agent speaks: read past, not kept.
196
+ // Type, replace flag, image index, an unknown byte; then the region flag.
197
+ r.skip(5, where);
198
+ const hasRegion = r.u8(where) !== 0;
199
+ r.skip(8, where);
200
+ if (hasRegion)
201
+ r.skip(r.u32(where), where);
202
+ }
203
+ frames.push({ images, sound, duration, exitBranch, branches });
204
+ }
205
+ return frames;
206
+ }
207
+ /**
208
+ * Read the tables of an `.acs`: the character, its animations and frames,
209
+ * where its images and sounds are. Images are decoded by `decodeAcsImage`.
210
+ */
211
+ export function parseAcs(buffer) {
212
+ const bytes = new Uint8Array(buffer);
213
+ if (bytes.length < 36) {
214
+ throw new AssistantCharacterFormatError(`This is not a Microsoft Agent character: the file is ${bytes.length} bytes, shorter than the header of an .acs.`);
215
+ }
216
+ const r = new Reader(bytes);
217
+ const signature = r.u32('the signature');
218
+ if (signature === ACF_SIGNATURE) {
219
+ throw new AssistantCharacterFormatError('This is a Microsoft Agent .acf, whose animations are in separate .aca files: .acf is not read yet, load the .acs of the character.');
220
+ }
221
+ if (signature !== ACS_SIGNATURE) {
222
+ throw new AssistantCharacterFormatError(`This is not a Microsoft Agent character (.acs): it starts with 0x${signature.toString(16).padStart(8, '0')}, not 0xabcdabc3.`);
223
+ }
224
+ const characterAt = r.locator('the header');
225
+ const animationsAt = r.locator('the header');
226
+ const imagesAt = r.locator('the header');
227
+ const soundsAt = r.locator('the header');
228
+ const info = readCharacterInfo(r, characterAt);
229
+ if (info.width === 0 || info.height === 0) {
230
+ throw new AssistantCharacterFormatError(`This .acs is damaged: its character is ${info.width}×${info.height} pixels.`);
231
+ }
232
+ r.seek(imagesAt, 'the image list');
233
+ const imageCount = r.u32('the image list');
234
+ r.need(imageCount * 12, 'the image list');
235
+ const images = [];
236
+ for (let k = 0; k < imageCount; k++) {
237
+ images.push({ locator: r.locator('the image list') });
238
+ r.skip(4, 'the image list');
239
+ }
240
+ const sounds = [];
241
+ if (soundsAt.size > 0) {
242
+ r.seek(soundsAt, 'the sound list');
243
+ const soundCount = r.u32('the sound list');
244
+ r.need(soundCount * 12, 'the sound list');
245
+ for (let k = 0; k < soundCount; k++) {
246
+ const locator = r.locator('the sound list');
247
+ r.skip(4, 'the sound list');
248
+ if (locator.offset + locator.size > bytes.length) {
249
+ throw new AssistantCharacterFormatError(`This .acs is damaged: sound ${k} is said to be past the end of the file.`);
250
+ }
251
+ sounds.push(locator);
252
+ }
253
+ }
254
+ r.seek(animationsAt, 'the animation list');
255
+ const animationCount = r.u32('the animation list');
256
+ const entries = [];
257
+ for (let k = 0; k < animationCount; k++) {
258
+ const name = r.string('an animation name');
259
+ entries.push({ name, at: r.locator(`the locator of ${name}`) });
260
+ }
261
+ if (entries.length === 0) {
262
+ throw new AssistantCharacterFormatError('This .acs has no animations.');
263
+ }
264
+ const animations = entries.map(({ name, at }) => {
265
+ r.seek(at, `the animation ${name}`);
266
+ r.string(`the animation ${name}`);
267
+ const transition = r.u8(`the animation ${name}`);
268
+ const returnAnimation = r.string(`the animation ${name}`);
269
+ const frames = readFrames(r, `the animation ${name}`);
270
+ frames.forEach((frame, f) => {
271
+ for (const image of frame.images) {
272
+ if (image.image >= images.length) {
273
+ throw new AssistantCharacterFormatError(`This .acs is damaged: frame ${f} of ${name} draws image ${image.image}, and there are ${images.length}.`);
274
+ }
275
+ }
276
+ if (frame.sound >= sounds.length) {
277
+ throw new AssistantCharacterFormatError(`This .acs is damaged: frame ${f} of ${name} plays sound ${frame.sound}, and there are ${sounds.length}.`);
278
+ }
279
+ for (const branch of frame.branches) {
280
+ if (branch.frameIndex >= frames.length) {
281
+ throw new AssistantCharacterFormatError(`This .acs is damaged: frame ${f} of ${name} branches to frame ${branch.frameIndex}, and there are ${frames.length}.`);
282
+ }
283
+ }
284
+ });
285
+ return { name, transition, returnAnimation, frames };
286
+ });
287
+ return {
288
+ version: info.version,
289
+ name: info.name,
290
+ width: info.width,
291
+ height: info.height,
292
+ transparentIndex: info.transparentIndex,
293
+ palette: info.palette,
294
+ states: info.states,
295
+ animations,
296
+ images,
297
+ sounds,
298
+ bytes,
299
+ };
300
+ }
301
+ /** Decode one image of an `.acs` into palette indices, top row first. */
302
+ export function decodeAcsImage(file, index) {
303
+ const entry = file.images[index];
304
+ if (!entry) {
305
+ throw new AssistantCharacterFormatError(`This .acs has no image ${index}.`);
306
+ }
307
+ const r = new Reader(file.bytes);
308
+ const what = `image ${index}`;
309
+ r.seek(entry.locator, what);
310
+ r.skip(1, what);
311
+ const width = r.u16(what);
312
+ const height = r.u16(what);
313
+ const compressed = r.u8(what) !== 0;
314
+ const stride = (width + 3) & ~3;
315
+ const size = stride * height;
316
+ let bitmap;
317
+ if (compressed) {
318
+ const data = r.take(r.u32(what), what);
319
+ bitmap = decompressAgentData(data, size, `Image ${index} of this .acs`);
320
+ }
321
+ else {
322
+ // The specification puts a DATABLOCK (a size, then the bits) here; take
323
+ // the bits directly when the size is not there.
324
+ const start = r.pos;
325
+ const announced = r.pos + 4 <= file.bytes.length ? r.u32(what) : -1;
326
+ if (announced !== size)
327
+ r.pos = start;
328
+ bitmap = r.take(size, what);
329
+ }
330
+ const pixels = new Uint8Array(width * height);
331
+ for (let y = 0; y < height; y++) {
332
+ // Bitmaps are stored bottom-up.
333
+ pixels.set(bitmap.subarray((height - 1 - y) * stride, (height - 1 - y) * stride + width), y * width);
334
+ }
335
+ return { width, height, pixels };
336
+ }
337
+ /** Assign a cell to each distinct, non-empty frame. */
338
+ export function layoutAcsSprite(file) {
339
+ const cells = [];
340
+ const byKey = new Map();
341
+ const frameCells = file.animations.map(animation => animation.frames.map(frame => {
342
+ if (frame.images.length === 0)
343
+ return -1;
344
+ const key = frame.images.map(i => `${i.image},${i.x},${i.y}`).join(';');
345
+ let cell = byKey.get(key);
346
+ if (cell === undefined) {
347
+ cell = cells.length;
348
+ cells.push(frame.images);
349
+ byKey.set(key, cell);
350
+ }
351
+ return cell;
352
+ }));
353
+ const count = Math.max(1, cells.length);
354
+ // Near square, and no wider than SHEET_MAX_WIDTH.
355
+ const columns = Math.max(1, Math.min(Math.floor(SHEET_MAX_WIDTH / file.width), Math.ceil(Math.sqrt((count * file.height) / file.width))));
356
+ const rows = Math.ceil(count / columns);
357
+ if (columns * file.width > SHEET_MAX_SIDE ||
358
+ rows * file.height > SHEET_MAX_SIDE) {
359
+ throw new AssistantCharacterFormatError(`This .acs has ${cells.length} distinct frames of ${file.width}×${file.height} pixels, more than one sprite sheet can hold.`);
360
+ }
361
+ return { columns, rows, cells, frameCells };
362
+ }
363
+ /**
364
+ * Composite a cell as Agent draws a frame: its images from last to first,
365
+ * each at its offset, the transparent colour left clear. RGBA, frame-sized.
366
+ */
367
+ export function composeAcsCell(file, images, decode = index => decodeAcsImage(file, index)) {
368
+ const { width, height, transparentIndex, palette } = file;
369
+ const out = new Uint32Array(width * height);
370
+ for (let k = images.length - 1; k >= 0; k--) {
371
+ const { image, x, y } = images[k];
372
+ const decoded = decode(image);
373
+ for (let row = 0; row < decoded.height; row++) {
374
+ const ty = y + row;
375
+ if (ty < 0 || ty >= height)
376
+ continue;
377
+ for (let col = 0; col < decoded.width; col++) {
378
+ const tx = x + col;
379
+ if (tx < 0 || tx >= width)
380
+ continue;
381
+ const index = decoded.pixels[row * decoded.width + col];
382
+ if (index !== transparentIndex) {
383
+ out[ty * width + tx] = palette[index];
384
+ }
385
+ }
386
+ }
387
+ }
388
+ return new Uint8ClampedArray(out.buffer);
389
+ }
390
+ /** A canvas to draw the sheet on: an OffscreenCanvas, else a DOM canvas. */
391
+ function makeSheet(width, height) {
392
+ let canvas;
393
+ let toBlob;
394
+ if (typeof OffscreenCanvas !== 'undefined') {
395
+ const offscreen = new OffscreenCanvas(width, height);
396
+ canvas = offscreen;
397
+ toBlob = () => offscreen.convertToBlob({ type: 'image/png' });
398
+ }
399
+ else if (typeof document !== 'undefined') {
400
+ const element = document.createElement('canvas');
401
+ element.width = width;
402
+ element.height = height;
403
+ canvas = element;
404
+ toBlob = () => new Promise((resolve, reject) => element.toBlob(blob => blob
405
+ ? resolve(blob)
406
+ : reject(new AssistantCharacterFormatError('The sprite sheet of this .acs could not be drawn.')), 'image/png'));
407
+ }
408
+ else {
409
+ throw new AssistantCharacterFormatError('This page has no canvas to draw the character on.');
410
+ }
411
+ const context = canvas.getContext('2d');
412
+ if (!context) {
413
+ throw new AssistantCharacterFormatError('This page has no 2D canvas to draw the character on.');
414
+ }
415
+ return { context, toBlob };
416
+ }
417
+ /**
418
+ * Read a Microsoft Agent character (`.acs`) a person picked: its animations,
419
+ * a sprite sheet drawn in the page (an object URL), its sounds as object
420
+ * URLs. Nothing leaves the page.
421
+ */
422
+ export async function readAcsCharacter(buffer) {
423
+ const file = parseAcs(buffer);
424
+ const layout = layoutAcsSprite(file);
425
+ const { width, height } = file;
426
+ const decoded = new Map();
427
+ const decode = (index) => {
428
+ let image = decoded.get(index);
429
+ if (!image) {
430
+ image = decodeAcsImage(file, index);
431
+ decoded.set(index, image);
432
+ }
433
+ return image;
434
+ };
435
+ const sheet = makeSheet(layout.columns * width, layout.rows * height);
436
+ const cellAt = (cell) => ({
437
+ x: (cell % layout.columns) * width,
438
+ y: Math.floor(cell / layout.columns) * height,
439
+ });
440
+ layout.cells.forEach((images, cell) => {
441
+ const data = sheet.context.createImageData(width, height);
442
+ data.data.set(composeAcsCell(file, images, decode));
443
+ const { x, y } = cellAt(cell);
444
+ sheet.context.putImageData(data, x, y);
445
+ });
446
+ const sprite = objectUrl(await sheet.toBlob());
447
+ const used = new Set();
448
+ const animations = {};
449
+ file.animations.forEach((animation, a) => {
450
+ animations[animation.name] = {
451
+ frames: animation.frames.map((frame, f) => {
452
+ const cell = layout.frameCells[a][f];
453
+ if (frame.sound >= 0)
454
+ used.add(frame.sound);
455
+ return {
456
+ duration: frame.duration * 10,
457
+ images: cell < 0 ? [] : [cellAt(cell)],
458
+ ...(frame.sound >= 0 ? { sound: String(frame.sound) } : {}),
459
+ ...(frame.branches.length
460
+ ? {
461
+ branching: frame.branches.map(b => ({
462
+ frameIndex: b.frameIndex,
463
+ weight: b.probability,
464
+ })),
465
+ }
466
+ : {}),
467
+ ...(frame.exitBranch >= 0 &&
468
+ frame.exitBranch < animation.frames.length
469
+ ? { exitBranch: frame.exitBranch }
470
+ : {}),
471
+ };
472
+ }),
473
+ };
474
+ });
475
+ let sounds;
476
+ if (used.size > 0) {
477
+ sounds = {};
478
+ for (const index of [...used].sort((x, y) => x - y)) {
479
+ const { offset, size } = file.sounds[index];
480
+ sounds[String(index)] = objectUrl(new Blob([file.bytes.slice(offset, offset + size)], {
481
+ type: 'audio/wav',
482
+ }));
483
+ }
484
+ }
485
+ return {
486
+ name: file.name,
487
+ frameSize: { width, height },
488
+ sprite,
489
+ animations,
490
+ ...(sounds ? { sounds } : {}),
491
+ authoredStates: file.states,
492
+ };
493
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Decompress `input` into exactly `size` bytes; `what` names the data in
3
+ * error messages. Anything that does not decode to that size is refused.
4
+ */
5
+ export declare function decompressAgentData(input: Uint8Array, size: number, what: string): Uint8Array;
@@ -0,0 +1,92 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The compression of Microsoft Agent character data, after Lebeau's public
7
+ * description (MSAgent Character Data Specification 1.3, "Compression
8
+ * Algorithm"): a leading 0x00, then a bit stream read least significant bit
9
+ * first, where 0 is a literal byte and 1 a copy from earlier in the output,
10
+ * ended by a 20-bit offset of 0xFFFFF.
11
+ *
12
+ * @module chat/assistant/formats/agentCompression
13
+ */
14
+ import { AssistantCharacterFormatError } from './types';
15
+ const OFFSET_TIERS = [
16
+ [6, 1],
17
+ [9, 65],
18
+ [12, 577],
19
+ [20, 4673],
20
+ ];
21
+ /**
22
+ * Decompress `input` into exactly `size` bytes; `what` names the data in
23
+ * error messages. Anything that does not decode to that size is refused.
24
+ */
25
+ export function decompressAgentData(input, size, what) {
26
+ const fail = (reason) => {
27
+ throw new AssistantCharacterFormatError(`${what} is compressed but cannot be decompressed: ${reason}.`);
28
+ };
29
+ if (input.length === 0 || input[0] !== 0) {
30
+ fail('it does not start with the 0x00 byte of the format');
31
+ }
32
+ const out = new Uint8Array(size);
33
+ let written = 0;
34
+ let byte = 1;
35
+ let bit = 0;
36
+ const total = input.length * 8;
37
+ const pop = () => {
38
+ const at = byte * 8 + bit;
39
+ if (at >= total)
40
+ fail('the data ends before its end marker');
41
+ const v = (input[byte] >> bit) & 1;
42
+ if (++bit === 8) {
43
+ bit = 0;
44
+ byte++;
45
+ }
46
+ return v;
47
+ };
48
+ const popBits = (count) => {
49
+ let v = 0;
50
+ for (let k = 0; k < count; k++) {
51
+ v |= pop() << k;
52
+ }
53
+ return v >>> 0;
54
+ };
55
+ for (;;) {
56
+ if (pop() === 0) {
57
+ if (written >= size)
58
+ fail(`it holds more than the ${size} bytes announced`);
59
+ out[written++] = popBits(8);
60
+ continue;
61
+ }
62
+ let ones = 0;
63
+ while (ones < 3 && pop() === 1)
64
+ ones++;
65
+ const [bits, add] = OFFSET_TIERS[ones];
66
+ const raw = popBits(bits);
67
+ let count = 2;
68
+ if (bits === 20) {
69
+ if (raw === 0xfffff)
70
+ break;
71
+ count++;
72
+ }
73
+ const offset = raw + add;
74
+ if (offset > written)
75
+ fail('a copy reaches before the start of the data');
76
+ let lengthBits = 0;
77
+ while (pop() === 1) {
78
+ if (++lengthBits > 11)
79
+ fail('a copy length is malformed');
80
+ }
81
+ count += (1 << lengthBits) - 1 + popBits(lengthBits);
82
+ if (written + count > size)
83
+ fail(`it holds more than the ${size} bytes announced`);
84
+ for (let k = 0; k < count; k++, written++) {
85
+ out[written] = out[written - offset];
86
+ }
87
+ }
88
+ if (written !== size) {
89
+ fail(`it holds ${written} bytes where ${size} were announced`);
90
+ }
91
+ return out;
92
+ }
@@ -0,0 +1,6 @@
1
+ /** The bytes of a blob. */
2
+ export declare function blobBytes(blob: Blob): Promise<Uint8Array>;
3
+ /** An object URL for a blob, which stays in the page. */
4
+ export declare function objectUrl(blob: Blob): string;
5
+ /** The image type of the bytes, from their signature, if a known one. */
6
+ export declare function imageTypeOf(bytes: Uint8Array): string | undefined;
@@ -0,0 +1,52 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Blobs to bytes and URLs, in the page: a character file is read where the
7
+ * person picked it and is never uploaded.
8
+ *
9
+ * @module chat/assistant/formats/blobs
10
+ */
11
+ import { AssistantCharacterFormatError } from './types';
12
+ /** The bytes of a blob. */
13
+ export async function blobBytes(blob) {
14
+ if (typeof blob.arrayBuffer === 'function') {
15
+ return new Uint8Array(await blob.arrayBuffer());
16
+ }
17
+ return new Promise((resolve, reject) => {
18
+ const reader = new FileReader();
19
+ reader.onload = () => resolve(new Uint8Array(reader.result));
20
+ reader.onerror = () => reject(reader.error);
21
+ reader.readAsArrayBuffer(blob);
22
+ });
23
+ }
24
+ /** An object URL for a blob, which stays in the page. */
25
+ export function objectUrl(blob) {
26
+ if (typeof URL === 'undefined' || typeof URL.createObjectURL !== 'function') {
27
+ throw new AssistantCharacterFormatError('This page cannot hold a picked file: it has no URL.createObjectURL.');
28
+ }
29
+ return URL.createObjectURL(blob);
30
+ }
31
+ const IMAGE_SIGNATURES = [
32
+ {
33
+ type: 'image/png',
34
+ bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
35
+ },
36
+ { type: 'image/gif', bytes: [0x47, 0x49, 0x46, 0x38] },
37
+ { type: 'image/jpeg', bytes: [0xff, 0xd8, 0xff] },
38
+ ];
39
+ /** The image type of the bytes, from their signature, if a known one. */
40
+ export function imageTypeOf(bytes) {
41
+ for (const { type, bytes: signature } of IMAGE_SIGNATURES) {
42
+ if (signature.every((b, i) => bytes[i] === b)) {
43
+ return type;
44
+ }
45
+ }
46
+ if (bytes.length >= 12 &&
47
+ String.fromCharCode(...bytes.subarray(0, 4)) === 'RIFF' &&
48
+ String.fromCharCode(...bytes.subarray(8, 12)) === 'WEBP') {
49
+ return 'image/webp';
50
+ }
51
+ return undefined;
52
+ }
@@ -0,0 +1,21 @@
1
+ import { type AssistantCharacterData } from './types';
2
+ /** The files of a clippy.js character, as the person picked them. */
3
+ export interface ClippyCharacterFiles {
4
+ /** The text of `agent.js`. */
5
+ agentJs: string;
6
+ /** The sprite sheet, `map.png`. */
7
+ mapPng: Blob;
8
+ /** The text of a `sounds-mp3.js` or `sounds-ogg.js`, if picked. */
9
+ soundsJs?: string;
10
+ }
11
+ /** Read the character data of an `agent.js` (no sprite, no sounds). */
12
+ export declare function readClippyAgent(agentJs: string): Omit<AssistantCharacterData, 'sprite'> & {
13
+ overlayCount: number;
14
+ };
15
+ /** Read the sounds of a `sounds-*.js`: sound id to (data) URL. */
16
+ export declare function readClippySounds(soundsJs: string): Record<string, string>;
17
+ /**
18
+ * Read a clippy.js character from the files a person picked. Everything
19
+ * stays in the page: the sprite becomes an object URL.
20
+ */
21
+ export declare function readClippyCharacter(files: ClippyCharacterFiles): Promise<AssistantCharacterData>;