@datalayer/agent-runtimes 1.3.79 → 1.3.81

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 (43) hide show
  1. package/lib/chat/assistant/AssistantStage.d.ts +9 -4
  2. package/lib/chat/assistant/AssistantStage.js +18 -6
  3. package/lib/chat/assistant/SpriteCharacter.d.ts +25 -3
  4. package/lib/chat/assistant/SpriteCharacter.js +68 -3
  5. package/lib/chat/assistant/formats/acf.d.ts +56 -0
  6. package/lib/chat/assistant/formats/acf.js +317 -0
  7. package/lib/chat/assistant/formats/acs.d.ts +85 -7
  8. package/lib/chat/assistant/formats/acs.js +111 -39
  9. package/lib/chat/assistant/formats/files.d.ts +6 -2
  10. package/lib/chat/assistant/formats/files.js +18 -4
  11. package/lib/chat/assistant/formats/index.d.ts +6 -3
  12. package/lib/chat/assistant/formats/index.js +4 -2
  13. package/lib/chat/assistant/formats/types.d.ts +14 -0
  14. package/lib/chat/assistant/state.d.ts +2 -0
  15. package/lib/chat/assistant/state.js +9 -0
  16. package/lib/chat/base/ChatBase.js +7 -0
  17. package/lib/chat/base/loopStep.d.ts +40 -0
  18. package/lib/chat/base/loopStep.js +73 -0
  19. package/lib/loop/apps/appspec.js +32 -2
  20. package/lib/loop/apps/checks.js +53 -0
  21. package/lib/loop/apps/hostTools.d.ts +17 -0
  22. package/lib/loop/apps/hostTools.js +20 -0
  23. package/lib/loop/apps/rules.d.ts +22 -1
  24. package/lib/loop/apps/rules.js +49 -1
  25. package/lib/loop/embed/AppEmbed.d.ts +57 -2
  26. package/lib/loop/embed/AppEmbed.js +71 -8
  27. package/lib/loop/embed/element.js +40 -1
  28. package/lib/loop/embed/hostBridge.d.ts +79 -0
  29. package/lib/loop/embed/hostBridge.js +104 -0
  30. package/lib/loop/index.d.ts +2 -1
  31. package/lib/loop/index.js +3 -1
  32. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.d.ts +5 -2
  33. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +7 -1
  34. package/lib/loop/plugins/a2ui-surface/index.d.ts +1 -1
  35. package/lib/loop/plugins/a2ui-surface/index.js +4 -4
  36. package/lib/loop/plugins/a2ui-surface/toolResult.d.ts +27 -0
  37. package/lib/loop/plugins/a2ui-surface/toolResult.js +22 -0
  38. package/lib/specs/agents/agents.js +6 -25
  39. package/lib/specs/apps.js +22 -8
  40. package/lib/specs/appspecSchema.js +67 -0
  41. package/lib/types/agentspecs.d.ts +28 -0
  42. package/package.json +1 -1
  43. package/scripts/codegen/generate_apps.py +7 -0
@@ -1,4 +1,4 @@
1
- import { type AssistantCharacterData } from './types';
1
+ import { type AssistantCharacterData, type AssistantMouthShape } from './types';
2
2
  export declare const ACS_SIGNATURE = 2882382787;
3
3
  export declare const ACF_SIGNATURE = 2882382788;
4
4
  export interface AcsLocator {
@@ -10,6 +10,26 @@ export interface AcsFrameImage {
10
10
  x: number;
11
11
  y: number;
12
12
  }
13
+ /**
14
+ * A mouth overlay (ACSOVERLAYINFO), drawn only while the character speaks:
15
+ * its image over the frame's top image, or in its place.
16
+ */
17
+ export interface AcsOverlay {
18
+ /** 0 closed, 1 to 4 wide open, 5 medium, 6 narrow (`MOUTH_SHAPES`). */
19
+ type: number;
20
+ /** Whether the overlay takes the place of the frame's top image. */
21
+ replaceTop: boolean;
22
+ image: number;
23
+ x: number;
24
+ y: number;
25
+ /**
26
+ * What the mouth is drawn over in place of the frame's images, when the
27
+ * file says (an `.aca` gives the frame without its top image).
28
+ */
29
+ base?: AcsFrameImage[];
30
+ }
31
+ /** The mouth shapes of the format, by overlay type. */
32
+ export declare const MOUTH_SHAPES: readonly AssistantMouthShape[];
13
33
  export interface AcsFrame {
14
34
  /** Images, as stored: the first is drawn on top. */
15
35
  images: AcsFrameImage[];
@@ -23,6 +43,8 @@ export interface AcsFrame {
23
43
  frameIndex: number;
24
44
  probability: number;
25
45
  }[];
46
+ /** Its mouth overlays, by shape; none for most frames. */
47
+ overlays: AcsOverlay[];
26
48
  }
27
49
  export interface AcsAnimation {
28
50
  name: string;
@@ -57,6 +79,33 @@ export interface AcsImage {
57
79
  height: number;
58
80
  pixels: Uint8Array;
59
81
  }
82
+ /**
83
+ * Little-endian reads that refuse, in a sentence naming the file kind, to
84
+ * run past the end. `terminated`: whether a STRING ends with a null
85
+ * character, as in an `.acs` (an `.acf`'s do not).
86
+ */
87
+ export declare class AgentReader {
88
+ readonly bytes: Uint8Array;
89
+ readonly kind: string;
90
+ readonly terminated: boolean;
91
+ private readonly view;
92
+ pos: number;
93
+ constructor(bytes: Uint8Array, kind?: string, terminated?: boolean);
94
+ need(count: number, what: string): void;
95
+ seek(locator: AcsLocator, what: string): void;
96
+ u8(what: string): number;
97
+ u16(what: string): number;
98
+ i16(what: string): number;
99
+ u32(what: string): number;
100
+ skip(count: number, what: string): void;
101
+ take(count: number, what: string): Uint8Array;
102
+ locator(what: string): AcsLocator;
103
+ /**
104
+ * STRING: a ULONG count of UTF-16 characters, then, in an `.acs`, a null
105
+ * terminator.
106
+ */
107
+ string(what: string): string;
108
+ }
60
109
  /**
61
110
  * Read the tables of an `.acs`: the character, its animations and frames,
62
111
  * where its images and sounds are. Images are decoded by `decodeAcsImage`.
@@ -64,6 +113,23 @@ export interface AcsImage {
64
113
  export declare function parseAcs(buffer: ArrayBuffer): AcsFile;
65
114
  /** Decode one image of an `.acs` into palette indices, top row first. */
66
115
  export declare function decodeAcsImage(file: AcsFile, index: number): AcsImage;
116
+ /**
117
+ * What a Microsoft Agent character is drawn from, whichever file it came in:
118
+ * an `.acs` (`parseAcs`), or an `.acf` and its `.aca` files (`readAcfCharacter`).
119
+ */
120
+ export interface AgentCharacterSource {
121
+ name: string;
122
+ width: number;
123
+ height: number;
124
+ transparentIndex: number;
125
+ palette: Uint32Array;
126
+ states: Record<string, string[]>;
127
+ animations: AcsAnimation[];
128
+ /** Image `index`, decoded. */
129
+ decode: (index: number) => AcsImage;
130
+ /** The bytes of sound `index` (RIFF WAVE). */
131
+ sound: (index: number) => Uint8Array;
132
+ }
67
133
  /** One cell of the sprite sheet: the frames that look the same share it. */
68
134
  export interface AcsSpriteLayout {
69
135
  columns: number;
@@ -72,17 +138,29 @@ export interface AcsSpriteLayout {
72
138
  cells: AcsFrameImage[][];
73
139
  /** For each animation, for each frame, its cell, or -1 for an empty frame. */
74
140
  frameCells: number[][];
141
+ /**
142
+ * For each animation, for each frame, the cell of each mouth it has — the
143
+ * frame with that overlay drawn over its top image, or in its place.
144
+ */
145
+ mouthCells: Array<Array<Partial<Record<AssistantMouthShape, number>>>>;
75
146
  }
76
- /** Assign a cell to each distinct, non-empty frame. */
77
- export declare function layoutAcsSprite(file: AcsFile): AcsSpriteLayout;
147
+ /** The images of a frame with a mouth: over the top image, or in its place. */
148
+ export declare function mouthImages(frame: Pick<AcsFrame, 'images'>, overlay: AcsOverlay): AcsFrameImage[];
149
+ /** Assign a cell to each distinct, non-empty frame, and to each mouth. */
150
+ export declare function layoutAcsSprite(file: Pick<AgentCharacterSource, 'width' | 'height' | 'animations'>): AcsSpriteLayout;
78
151
  /**
79
152
  * Composite a cell as Agent draws a frame: its images from last to first,
80
153
  * each at its offset, the transparent colour left clear. RGBA, frame-sized.
81
154
  */
82
- export declare function composeAcsCell(file: AcsFile, images: AcsFrameImage[], decode?: (index: number) => AcsImage): Uint8ClampedArray;
155
+ export declare function composeAcsCell(file: Pick<AgentCharacterSource, 'width' | 'height' | 'transparentIndex' | 'palette'>, images: AcsFrameImage[], decode?: (index: number) => AcsImage): Uint8ClampedArray;
156
+ /**
157
+ * Draw a Microsoft Agent character in the page: its sprite sheet (an object
158
+ * URL), its animations with their mouths, its sounds as object URLs.
159
+ */
160
+ export declare function drawAgentCharacter(source: AgentCharacterSource): Promise<AssistantCharacterData>;
83
161
  /**
84
- * Read a Microsoft Agent character (`.acs`) a person picked: its animations,
85
- * a sprite sheet drawn in the page (an object URL), its sounds as object
86
- * URLs. Nothing leaves the page.
162
+ * Read a Microsoft Agent character (`.acs`) a person picked: its animations
163
+ * and their mouths, a sprite sheet drawn in the page (an object URL), its
164
+ * sounds as object URLs. Nothing leaves the page.
87
165
  */
88
166
  export declare function readAcsCharacter(buffer: ArrayBuffer): Promise<AssistantCharacterData>;
@@ -27,22 +27,41 @@ const STYLE_BALLOON = 0x200;
27
27
  /** The widest a sprite sheet is laid out, and the largest it may be. */
28
28
  const SHEET_MAX_WIDTH = 4096;
29
29
  const SHEET_MAX_SIDE = 16384;
30
- class Reader {
30
+ /** The mouth shapes of the format, by overlay type. */
31
+ export const MOUTH_SHAPES = [
32
+ 'closed',
33
+ 'wide1',
34
+ 'wide2',
35
+ 'wide3',
36
+ 'wide4',
37
+ 'medium',
38
+ 'narrow',
39
+ ];
40
+ /**
41
+ * Little-endian reads that refuse, in a sentence naming the file kind, to
42
+ * run past the end. `terminated`: whether a STRING ends with a null
43
+ * character, as in an `.acs` (an `.acf`'s do not).
44
+ */
45
+ export class AgentReader {
31
46
  bytes;
47
+ kind;
48
+ terminated;
32
49
  view;
33
50
  pos = 0;
34
- constructor(bytes) {
51
+ constructor(bytes, kind = '.acs', terminated = true) {
35
52
  this.bytes = bytes;
53
+ this.kind = kind;
54
+ this.terminated = terminated;
36
55
  this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
37
56
  }
38
57
  need(count, what) {
39
58
  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.`);
59
+ throw new AssistantCharacterFormatError(`This ${this.kind} ends early: ${what} at byte ${this.pos} runs past the end of the file.`);
41
60
  }
42
61
  }
43
62
  seek(locator, what) {
44
63
  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.`);
64
+ throw new AssistantCharacterFormatError(`This ${this.kind} is damaged: ${what} is said to be at byte ${locator.offset}, past the end of the file.`);
46
65
  }
47
66
  this.pos = locator.offset;
48
67
  }
@@ -81,17 +100,21 @@ class Reader {
81
100
  locator(what) {
82
101
  return { offset: this.u32(what), size: this.u32(what) };
83
102
  }
84
- /** STRING: a ULONG count of UTF-16 characters, then a null terminator. */
103
+ /**
104
+ * STRING: a ULONG count of UTF-16 characters, then, in an `.acs`, a null
105
+ * terminator.
106
+ */
85
107
  string(what) {
86
108
  const count = this.u32(what);
87
109
  if (count === 0)
88
110
  return '';
89
- this.need((count + 1) * 2, what);
111
+ const stored = this.terminated ? count + 1 : count;
112
+ this.need(stored * 2, what);
90
113
  let out = '';
91
114
  for (let k = 0; k < count; k++) {
92
115
  out += String.fromCharCode(this.view.getUint16(this.pos + k * 2, true));
93
116
  }
94
- this.pos += (count + 1) * 2;
117
+ this.pos += stored * 2;
95
118
  return out;
96
119
  }
97
120
  }
@@ -190,17 +213,26 @@ function readFrames(r, animation) {
190
213
  for (let k = 0; k < branchCount; k++) {
191
214
  branches.push({ frameIndex: r.u16(where), probability: r.u16(where) });
192
215
  }
216
+ const overlays = [];
193
217
  const overlayCount = r.u8(where);
194
218
  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);
219
+ // A mouth overlay, drawn only while the character speaks: its type,
220
+ // whether it replaces the top image, its image, an unknown byte, the
221
+ // region flag, its offset, its size (halved, and not needed: the
222
+ // image has its own), its region.
223
+ const type = r.u8(where);
224
+ const replaceTop = r.u8(where) !== 0;
225
+ const image = r.u16(where);
226
+ r.skip(1, where);
198
227
  const hasRegion = r.u8(where) !== 0;
199
- r.skip(8, where);
228
+ const x = r.i16(where);
229
+ const y = r.i16(where);
230
+ r.skip(4, where);
200
231
  if (hasRegion)
201
232
  r.skip(r.u32(where), where);
233
+ overlays.push({ type, replaceTop, image, x, y });
202
234
  }
203
- frames.push({ images, sound, duration, exitBranch, branches });
235
+ frames.push({ images, sound, duration, exitBranch, branches, overlays });
204
236
  }
205
237
  return frames;
206
238
  }
@@ -213,10 +245,10 @@ export function parseAcs(buffer) {
213
245
  if (bytes.length < 36) {
214
246
  throw new AssistantCharacterFormatError(`This is not a Microsoft Agent character: the file is ${bytes.length} bytes, shorter than the header of an .acs.`);
215
247
  }
216
- const r = new Reader(bytes);
248
+ const r = new AgentReader(bytes);
217
249
  const signature = r.u32('the signature');
218
250
  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.');
251
+ throw new AssistantCharacterFormatError('This is a Microsoft Agent .acf, whose animations are in separate .aca files: pick the .acf together with its .aca files.');
220
252
  }
221
253
  if (signature !== ACS_SIGNATURE) {
222
254
  throw new AssistantCharacterFormatError(`This is not a Microsoft Agent character (.acs): it starts with 0x${signature.toString(16).padStart(8, '0')}, not 0xabcdabc3.`);
@@ -276,6 +308,11 @@ export function parseAcs(buffer) {
276
308
  if (frame.sound >= sounds.length) {
277
309
  throw new AssistantCharacterFormatError(`This .acs is damaged: frame ${f} of ${name} plays sound ${frame.sound}, and there are ${sounds.length}.`);
278
310
  }
311
+ for (const overlay of frame.overlays) {
312
+ if (overlay.image >= images.length) {
313
+ throw new AssistantCharacterFormatError(`This .acs is damaged: a mouth of frame ${f} of ${name} draws image ${overlay.image}, and there are ${images.length}.`);
314
+ }
315
+ }
279
316
  for (const branch of frame.branches) {
280
317
  if (branch.frameIndex >= frames.length) {
281
318
  throw new AssistantCharacterFormatError(`This .acs is damaged: frame ${f} of ${name} branches to frame ${branch.frameIndex}, and there are ${frames.length}.`);
@@ -304,7 +341,7 @@ export function decodeAcsImage(file, index) {
304
341
  if (!entry) {
305
342
  throw new AssistantCharacterFormatError(`This .acs has no image ${index}.`);
306
343
  }
307
- const r = new Reader(file.bytes);
344
+ const r = new AgentReader(file.bytes);
308
345
  const what = `image ${index}`;
309
346
  r.seek(entry.locator, what);
310
347
  r.skip(1, what);
@@ -334,21 +371,39 @@ export function decodeAcsImage(file, index) {
334
371
  }
335
372
  return { width, height, pixels };
336
373
  }
337
- /** Assign a cell to each distinct, non-empty frame. */
374
+ /** The images of a frame with a mouth: over the top image, or in its place. */
375
+ export function mouthImages(frame, overlay) {
376
+ const mouth = { image: overlay.image, x: overlay.x, y: overlay.y };
377
+ const under = overlay.base ?? (overlay.replaceTop ? frame.images.slice(1) : frame.images);
378
+ return [mouth, ...under];
379
+ }
380
+ /** Assign a cell to each distinct, non-empty frame, and to each mouth. */
338
381
  export function layoutAcsSprite(file) {
339
382
  const cells = [];
340
383
  const byKey = new Map();
341
- const frameCells = file.animations.map(animation => animation.frames.map(frame => {
342
- if (frame.images.length === 0)
384
+ const cellOf = (images) => {
385
+ if (images.length === 0)
343
386
  return -1;
344
- const key = frame.images.map(i => `${i.image},${i.x},${i.y}`).join(';');
387
+ const key = images.map(i => `${i.image},${i.x},${i.y}`).join(';');
345
388
  let cell = byKey.get(key);
346
389
  if (cell === undefined) {
347
390
  cell = cells.length;
348
- cells.push(frame.images);
391
+ cells.push(images);
349
392
  byKey.set(key, cell);
350
393
  }
351
394
  return cell;
395
+ };
396
+ const frameCells = file.animations.map(animation => animation.frames.map(frame => cellOf(frame.images)));
397
+ const mouthCells = file.animations.map(animation => animation.frames.map((frame, f) => {
398
+ const mouths = {};
399
+ for (const overlay of frame.overlays) {
400
+ const shape = MOUTH_SHAPES[overlay.type];
401
+ if (!shape) {
402
+ throw new AssistantCharacterFormatError(`This character is damaged: a mouth of frame ${f} of ${animation.name} is of type ${overlay.type}, where 0 to 6 are expected.`);
403
+ }
404
+ mouths[shape] = cellOf(mouthImages(frame, overlay));
405
+ }
406
+ return mouths;
352
407
  }));
353
408
  const count = Math.max(1, cells.length);
354
409
  // Near square, and no wider than SHEET_MAX_WIDTH.
@@ -356,9 +411,9 @@ export function layoutAcsSprite(file) {
356
411
  const rows = Math.ceil(count / columns);
357
412
  if (columns * file.width > SHEET_MAX_SIDE ||
358
413
  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.`);
414
+ throw new AssistantCharacterFormatError(`This character has ${cells.length} distinct frames of ${file.width}×${file.height} pixels, more than one sprite sheet can hold.`);
360
415
  }
361
- return { columns, rows, cells, frameCells };
416
+ return { columns, rows, cells, frameCells, mouthCells };
362
417
  }
363
418
  /**
364
419
  * Composite a cell as Agent draws a frame: its images from last to first,
@@ -403,7 +458,7 @@ function makeSheet(width, height) {
403
458
  canvas = element;
404
459
  toBlob = () => new Promise((resolve, reject) => element.toBlob(blob => blob
405
460
  ? resolve(blob)
406
- : reject(new AssistantCharacterFormatError('The sprite sheet of this .acs could not be drawn.')), 'image/png'));
461
+ : reject(new AssistantCharacterFormatError('The sprite sheet of this character could not be drawn.')), 'image/png'));
407
462
  }
408
463
  else {
409
464
  throw new AssistantCharacterFormatError('This page has no canvas to draw the character on.');
@@ -415,19 +470,17 @@ function makeSheet(width, height) {
415
470
  return { context, toBlob };
416
471
  }
417
472
  /**
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.
473
+ * Draw a Microsoft Agent character in the page: its sprite sheet (an object
474
+ * URL), its animations with their mouths, its sounds as object URLs.
421
475
  */
422
- export async function readAcsCharacter(buffer) {
423
- const file = parseAcs(buffer);
424
- const layout = layoutAcsSprite(file);
425
- const { width, height } = file;
476
+ export async function drawAgentCharacter(source) {
477
+ const layout = layoutAcsSprite(source);
478
+ const { width, height } = source;
426
479
  const decoded = new Map();
427
480
  const decode = (index) => {
428
481
  let image = decoded.get(index);
429
482
  if (!image) {
430
- image = decodeAcsImage(file, index);
483
+ image = source.decode(index);
431
484
  decoded.set(index, image);
432
485
  }
433
486
  return image;
@@ -439,17 +492,18 @@ export async function readAcsCharacter(buffer) {
439
492
  });
440
493
  layout.cells.forEach((images, cell) => {
441
494
  const data = sheet.context.createImageData(width, height);
442
- data.data.set(composeAcsCell(file, images, decode));
495
+ data.data.set(composeAcsCell(source, images, decode));
443
496
  const { x, y } = cellAt(cell);
444
497
  sheet.context.putImageData(data, x, y);
445
498
  });
446
499
  const sprite = objectUrl(await sheet.toBlob());
447
500
  const used = new Set();
448
501
  const animations = {};
449
- file.animations.forEach((animation, a) => {
502
+ source.animations.forEach((animation, a) => {
450
503
  animations[animation.name] = {
451
504
  frames: animation.frames.map((frame, f) => {
452
505
  const cell = layout.frameCells[a][f];
506
+ const mouths = Object.entries(layout.mouthCells[a][f]);
453
507
  if (frame.sound >= 0)
454
508
  used.add(frame.sound);
455
509
  return {
@@ -468,6 +522,11 @@ export async function readAcsCharacter(buffer) {
468
522
  frame.exitBranch < animation.frames.length
469
523
  ? { exitBranch: frame.exitBranch }
470
524
  : {}),
525
+ ...(mouths.length
526
+ ? {
527
+ mouths: Object.fromEntries(mouths.map(([shape, at]) => [shape, cellAt(at)])),
528
+ }
529
+ : {}),
471
530
  };
472
531
  }),
473
532
  };
@@ -476,18 +535,31 @@ export async function readAcsCharacter(buffer) {
476
535
  if (used.size > 0) {
477
536
  sounds = {};
478
537
  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
- }));
538
+ sounds[String(index)] = objectUrl(new Blob([source.sound(index).slice()], { type: 'audio/wav' }));
483
539
  }
484
540
  }
485
541
  return {
486
- name: file.name,
542
+ name: source.name,
487
543
  frameSize: { width, height },
488
544
  sprite,
489
545
  animations,
490
546
  ...(sounds ? { sounds } : {}),
491
- authoredStates: file.states,
547
+ authoredStates: source.states,
492
548
  };
493
549
  }
550
+ /**
551
+ * Read a Microsoft Agent character (`.acs`) a person picked: its animations
552
+ * and their mouths, a sprite sheet drawn in the page (an object URL), its
553
+ * sounds as object URLs. Nothing leaves the page.
554
+ */
555
+ export async function readAcsCharacter(buffer) {
556
+ const file = parseAcs(buffer);
557
+ return drawAgentCharacter({
558
+ ...file,
559
+ decode: index => decodeAcsImage(file, index),
560
+ sound: index => {
561
+ const { offset, size } = file.sounds[index];
562
+ return file.bytes.subarray(offset, offset + size);
563
+ },
564
+ });
565
+ }
@@ -1,8 +1,8 @@
1
1
  import { type AssistantCharacterData } from './types';
2
2
  /** What a file picker for a character accepts. */
3
- export declare const CHARACTER_FILES_ACCEPT = ".acs,.js,.png,.gif,.webp,.jpg,.jpeg";
3
+ export declare const CHARACTER_FILES_ACCEPT = ".acs,.acf,.aca,.js,.png,.gif,.webp,.jpg,.jpeg";
4
4
  /** The sentence for a pick that is not a character. */
5
- export declare const CHARACTER_FILES_EXPECTED = "Pick one .acs file, or a clippy.js character: its agent.js and its map image (and a sounds file if you have one).";
5
+ export declare const CHARACTER_FILES_EXPECTED = "Pick one .acs file; or an .acf file with its .aca files; or a clippy.js character: its agent.js and its map image (and a sounds file if you have one).";
6
6
  /** A picked file, as much of `File` as the reader needs. */
7
7
  export type PickedFile = Blob & {
8
8
  name: string;
@@ -11,6 +11,10 @@ export type PickedFile = Blob & {
11
11
  export declare function characterFilesOf(files: ReadonlyArray<PickedFile>): {
12
12
  kind: 'acs';
13
13
  acs: PickedFile;
14
+ } | {
15
+ kind: 'acf';
16
+ acf: PickedFile;
17
+ acas: PickedFile[];
14
18
  } | {
15
19
  kind: 'clippy';
16
20
  agentJs: PickedFile;
@@ -4,25 +4,32 @@
4
4
  */
5
5
  /**
6
6
  * The files a person picked, read as one character (LOOP T-26): one
7
- * Microsoft Agent `.acs`, or a clippy.js character's `agent.js` and map
8
- * image (and its sounds file, if there is one). Read in the page; nothing is
7
+ * Microsoft Agent `.acs`; a Microsoft Agent `.acf` with the `.aca` files it
8
+ * names; or a clippy.js character's `agent.js` and map image (and its
9
+ * sounds file, if there is one). Read in the page; nothing is
9
10
  * sent anywhere.
10
11
  *
11
12
  * @module chat/assistant/formats/files
12
13
  */
14
+ import { readAcfCharacter } from './acf';
13
15
  import { readAcsCharacter } from './acs';
14
16
  import { readClippyCharacter } from './clippy';
15
17
  import { AssistantCharacterFormatError, } from './types';
16
18
  /** What a file picker for a character accepts. */
17
- export const CHARACTER_FILES_ACCEPT = '.acs,.js,.png,.gif,.webp,.jpg,.jpeg';
19
+ export const CHARACTER_FILES_ACCEPT = '.acs,.acf,.aca,.js,.png,.gif,.webp,.jpg,.jpeg';
18
20
  /** The sentence for a pick that is not a character. */
19
- export const CHARACTER_FILES_EXPECTED = 'Pick one .acs file, or a clippy.js character: its agent.js and its map image (and a sounds file if you have one).';
21
+ export const CHARACTER_FILES_EXPECTED = 'Pick one .acs file; or an .acf file with its .aca files; or a clippy.js character: its agent.js and its map image (and a sounds file if you have one).';
20
22
  /** Which of the picked files make the character, or why none do. */
21
23
  export function characterFilesOf(files) {
22
24
  const acs = files.find(file => file.name.toLowerCase().endsWith('.acs'));
23
25
  if (acs) {
24
26
  return { kind: 'acs', acs };
25
27
  }
28
+ const acf = files.find(file => file.name.toLowerCase().endsWith('.acf'));
29
+ if (acf) {
30
+ const acas = files.filter(file => file.name.toLowerCase().endsWith('.aca'));
31
+ return { kind: 'acf', acf, acas };
32
+ }
26
33
  const agentJs = files.find(file => file.name.toLowerCase() === 'agent.js');
27
34
  const map = files.find(file => /\.(png|gif|webp|jpe?g)$/i.test(file.name));
28
35
  const sounds = files.find(file => /^sounds-.*\.js$/i.test(file.name));
@@ -40,6 +47,13 @@ export async function readCharacterFiles(files) {
40
47
  if (picked.kind === 'acs') {
41
48
  return readAcsCharacter(await picked.acs.arrayBuffer());
42
49
  }
50
+ if (picked.kind === 'acf') {
51
+ const acas = {};
52
+ for (const aca of picked.acas) {
53
+ acas[aca.name] = await aca.arrayBuffer();
54
+ }
55
+ return readAcfCharacter(await picked.acf.arrayBuffer(), acas);
56
+ }
43
57
  return readClippyCharacter({
44
58
  agentJs: await picked.agentJs.text(),
45
59
  mapPng: picked.map,
@@ -1,14 +1,17 @@
1
1
  /**
2
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.
3
+ * Microsoft Agent `.acs` files, or `.acf` files with their `.aca` files,
4
+ * read in the page into one shape.
4
5
  *
5
6
  * @module chat/assistant/formats
6
7
  */
7
8
  export * from './types';
8
9
  export { readClippyCharacter, readClippyAgent, readClippySounds, } from './clippy';
9
10
  export type { ClippyCharacterFiles } from './clippy';
10
- export { readAcsCharacter, parseAcs, decodeAcsImage, layoutAcsSprite, composeAcsCell, } from './acs';
11
- export type { AcsFile, AcsAnimation, AcsFrame, AcsImage } from './acs';
11
+ export { readAcsCharacter, parseAcs, decodeAcsImage, layoutAcsSprite, composeAcsCell, drawAgentCharacter, mouthImages, MOUTH_SHAPES, } from './acs';
12
+ export type { AcsFile, AcsAnimation, AcsFrame, AcsImage, AcsOverlay, AgentCharacterSource, } from './acs';
13
+ export { readAcfCharacter, parseAcf, parseAca } from './acf';
14
+ export type { AcfFile, AcfAnimationEntry, AcaFile } from './acf';
12
15
  export { decompressAgentData } from './agentCompression';
13
16
  export { stateAnimations } from './stateAnimations';
14
17
  export { CHARACTER_FILES_ACCEPT, CHARACTER_FILES_EXPECTED, characterFilesOf, readCharacterFiles, } from './files';
@@ -4,13 +4,15 @@
4
4
  */
5
5
  /**
6
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.
7
+ * Microsoft Agent `.acs` files, or `.acf` files with their `.aca` files,
8
+ * read in the page into one shape.
8
9
  *
9
10
  * @module chat/assistant/formats
10
11
  */
11
12
  export * from './types';
12
13
  export { readClippyCharacter, readClippyAgent, readClippySounds, } from './clippy';
13
- export { readAcsCharacter, parseAcs, decodeAcsImage, layoutAcsSprite, composeAcsCell, } from './acs';
14
+ export { readAcsCharacter, parseAcs, decodeAcsImage, layoutAcsSprite, composeAcsCell, drawAgentCharacter, mouthImages, MOUTH_SHAPES, } from './acs';
15
+ export { readAcfCharacter, parseAcf, parseAca } from './acf';
14
16
  export { decompressAgentData } from './agentCompression';
15
17
  export { stateAnimations } from './stateAnimations';
16
18
  export { CHARACTER_FILES_ACCEPT, CHARACTER_FILES_EXPECTED, characterFilesOf, readCharacterFiles, } from './files';
@@ -4,6 +4,11 @@
4
4
  *
5
5
  * @module chat/assistant/formats/types
6
6
  */
7
+ /**
8
+ * The shapes of a mouth while the character speaks, as Microsoft Agent
9
+ * names its overlays: closed, wide open (four), medium, narrow.
10
+ */
11
+ export type AssistantMouthShape = 'closed' | 'wide1' | 'wide2' | 'wide3' | 'wide4' | 'medium' | 'narrow';
7
12
  /** One frame of an animation. */
8
13
  export interface AssistantCharacterFrame {
9
14
  /** How long the frame shows, in milliseconds. */
@@ -26,6 +31,15 @@ export interface AssistantCharacterFrame {
26
31
  }[];
27
32
  /** The frame to go to when the animation is asked to end early. */
28
33
  exitBranch?: number;
34
+ /**
35
+ * The frame with each mouth the character has for it, drawn while it
36
+ * speaks in place of `images`: the sprite offset of each (Microsoft Agent's
37
+ * mouth overlays, composited into the sheet). Most frames have none.
38
+ */
39
+ mouths?: Partial<Record<AssistantMouthShape, {
40
+ x: number;
41
+ y: number;
42
+ }>>;
29
43
  }
30
44
  /** A named sequence of frames. */
31
45
  export interface AssistantCharacterAnimation {
@@ -103,6 +103,8 @@ export declare function latestSaying(items: readonly unknown[]): AssistantSaying
103
103
  * popup's button calls it back — for the session, or for good; the header's
104
104
  * *Floating assistant* always calls it back.
105
105
  */
106
+ /** The openings of the mouth, by level: shut, a little, half, wide. */
107
+ export declare function mouthOpening(level: number): 0 | 1 | 2 | 3;
106
108
  export type AssistantAway = 'none' | 'page' | 'session' | 'always';
107
109
  /** Where the choice is kept: the session's storage, or for good. */
108
110
  export declare const ASSISTANT_AWAY_KEY = "datalayer-assistant-away";
@@ -125,6 +125,15 @@ export function latestSaying(items) {
125
125
  }
126
126
  return undefined;
127
127
  }
128
+ /**
129
+ * How long the assistant is away, once sent (T-27): for the page — the
130
+ * popup's button calls it back — for the session, or for good; the header's
131
+ * *Floating assistant* always calls it back.
132
+ */
133
+ /** The openings of the mouth, by level: shut, a little, half, wide. */
134
+ export function mouthOpening(level) {
135
+ return level < 0.08 ? 0 : level < 0.3 ? 1 : level < 0.6 ? 2 : 3;
136
+ }
128
137
  /** Where the choice is kept: the session's storage, or for good. */
129
138
  export const ASSISTANT_AWAY_KEY = 'datalayer-assistant-away';
130
139
  /** The away a page starts with, from what was kept. Storage may be refused. */
@@ -61,6 +61,7 @@ import { useNotebookTools } from '../../tools/adapters/agent-runtimes/notebookHo
61
61
  import { useAgentInspectorSink } from '../../components/inspector/agentInspector';
62
62
  import { chatInspectorRecorder, } from '../../components/inspector/chatInspect';
63
63
  import { loopMessageChange, speakerOf, withLoopMessage } from './loopMessage';
64
+ import { loopStepOf, withLoopStep } from './loopStep';
64
65
  // Tracks pending prompts already auto-sent for a given conversation scope.
65
66
  // This prevents layout-driven unmount/remount cycles from re-sending prompts.
66
67
  const sentPendingPromptKeys = new Set();
@@ -2684,6 +2685,12 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2684
2685
  }
2685
2686
  break;
2686
2687
  case 'activity': {
2688
+ // A step of an application's code, drawn as it runs (LOOP P-16).
2689
+ const step = loopStepOf(event.activity);
2690
+ if (step) {
2691
+ setDisplayItems(prev => withLoopStep(prev, step));
2692
+ break;
2693
+ }
2687
2694
  // An application's code changed a message it sent: its author
2688
2695
  // said, or the message taken away (LOOP P-15).
2689
2696
  const change = loopMessageChange(event.activity);