@qvac/harness 0.8.5 → 0.8.7

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 (88) hide show
  1. package/CHANGELOG.md +1278 -472
  2. package/README.md +49 -0
  3. package/dist-lib/index.d.ts +42 -2
  4. package/dist-lib/index.js +447 -58
  5. package/dist-lib/lib/audio-transcode-bare.d.ts +1 -1
  6. package/dist-lib/lib/audio-transcode-bare.js +22 -25
  7. package/dist-lib/lib/audio-transcode-node.d.ts +1 -1
  8. package/dist-lib/lib/audio-transcode-node.js +1 -3
  9. package/dist-lib/lib/capabilities/audio-resample.d.ts +1 -1
  10. package/dist-lib/lib/capabilities/audio-resample.js +7 -8
  11. package/dist-lib/lib/capabilities/completion.d.ts +6 -3
  12. package/dist-lib/lib/capabilities/completion.js +96 -49
  13. package/dist-lib/lib/capabilities/download.d.ts +3 -1
  14. package/dist-lib/lib/capabilities/download.js +1 -1
  15. package/dist-lib/lib/capabilities/preview-model-source.d.ts +1 -0
  16. package/dist-lib/lib/capabilities/preview-model-source.js +1 -0
  17. package/dist-lib/lib/capabilities/tool-selection.d.ts +1 -3
  18. package/dist-lib/lib/capabilities/tool-selection.js +2 -4
  19. package/dist-lib/lib/capabilities/tool-turns.d.ts +0 -1
  20. package/dist-lib/lib/capabilities/tool-turns.js +0 -6
  21. package/dist-lib/lib/capabilities/usage-breakdown.js +4 -1
  22. package/dist-lib/lib/capabilities/voice.d.ts +5 -1
  23. package/dist-lib/lib/capabilities/voice.js +94 -26
  24. package/dist-lib/lib/connect.d.ts +4 -1
  25. package/dist-lib/lib/connect.js +16 -1
  26. package/dist-lib/lib/contract-types.d.ts +12 -0
  27. package/dist-lib/lib/helpers/tool-call-markup.d.ts +1 -0
  28. package/dist-lib/lib/helpers/tool-call-markup.js +3 -0
  29. package/dist-lib/lib/models/context-choice.d.ts +15 -0
  30. package/dist-lib/lib/models/context-choice.js +55 -0
  31. package/dist-lib/lib/models/download-gate.d.ts +20 -0
  32. package/dist-lib/lib/models/download-gate.js +73 -0
  33. package/dist-lib/lib/models/memory-cap.d.ts +13 -0
  34. package/dist-lib/lib/models/memory-cap.js +21 -0
  35. package/dist-lib/lib/models/model-catalog-constants.d.ts +3 -1
  36. package/dist-lib/lib/models/model-catalog-constants.js +9 -1
  37. package/dist-lib/lib/models/model-catalog.d.ts +6 -2
  38. package/dist-lib/lib/models/model-catalog.js +35 -3
  39. package/dist-lib/lib/models/model-handle.d.ts +8 -1
  40. package/dist-lib/lib/models/model-handle.js +20 -2
  41. package/dist-lib/lib/models/model-list.d.ts +8 -1
  42. package/dist-lib/lib/models/model-list.js +23 -0
  43. package/dist-lib/lib/models/model-pool.d.ts +35 -5
  44. package/dist-lib/lib/models/model-pool.js +97 -32
  45. package/dist-lib/lib/models/sdk-faults.d.ts +1 -0
  46. package/dist-lib/lib/models/sdk-faults.js +6 -0
  47. package/dist-lib/lib/office/engine/pptx/picture-area.d.ts +23 -0
  48. package/dist-lib/lib/office/engine/pptx/picture-area.js +84 -0
  49. package/dist-lib/lib/office/engine/pptx/presentation.d.ts +2 -0
  50. package/dist-lib/lib/office/engine/pptx/presentation.js +55 -17
  51. package/dist-lib/lib/office/engine/pptx/types.d.ts +1 -0
  52. package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +1 -0
  53. package/dist-lib/lib/office/engine/xlsx/workbook.js +34 -3
  54. package/dist-lib/lib/serve.js +23 -3
  55. package/dist-lib/lib/spawn.js +4 -10
  56. package/dist-lib/lib/tools/exec.d.ts +4 -1
  57. package/dist-lib/lib/tools/exec.js +55 -11
  58. package/dist-lib/lib/tools/generate-image.d.ts +4 -2
  59. package/dist-lib/lib/tools/generate-image.js +5 -2
  60. package/dist-lib/lib/tools/generate-music.d.ts +4 -2
  61. package/dist-lib/lib/tools/generate-music.js +5 -2
  62. package/dist-lib/lib/tools/office/ops.js +1 -1
  63. package/dist-lib/lib/tools/office.js +29 -6
  64. package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
  65. package/dist-lib/lib/tools/skills/catalog.js +2 -0
  66. package/dist-lib/lib/tools/skills/cli-schema-check.d.ts +1 -0
  67. package/dist-lib/lib/tools/skills/cli-schema-check.js +3 -0
  68. package/dist-lib/lib/tools/skills/cli-schema-load.js +10 -0
  69. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +2 -0
  70. package/dist-lib/lib/tools/skills/cli-schema.js +6 -3
  71. package/dist-lib/lib/tools/skills/frontmatter-parser.d.ts +1 -0
  72. package/dist-lib/lib/tools/skills/manifest-parse.d.ts +1 -0
  73. package/dist-lib/lib/tools/skills/manifest-parse.js +12 -0
  74. package/dist-lib/lib/tools/skills/mention.d.ts +0 -1
  75. package/dist-lib/lib/tools/skills/mention.js +0 -3
  76. package/dist-lib/lib/tools/translate.d.ts +3 -1
  77. package/dist-lib/lib/tools/translate.js +6 -1
  78. package/dist-lib/schema/harness.js +43 -4
  79. package/dist-lib/spec/bind.js +9 -0
  80. package/dist-lib/spec/calls.js +3 -0
  81. package/dist-lib/spec/hrpc/hrpc.json +39 -0
  82. package/dist-lib/spec/hrpc/index.js +39 -3
  83. package/dist-lib/spec/hrpc/messages.js +101 -13
  84. package/dist-lib/spec/hyperschema/index.js +101 -13
  85. package/dist-lib/spec/hyperschema/schema.json +62 -1
  86. package/dist-lib/spec/hyperschema/types.d.ts +20 -2
  87. package/package.json +8 -10
  88. package/worker/harness-sidecar.mjs +2 -1
@@ -0,0 +1,84 @@
1
+ const FRAME = 0.8;
2
+ const MARGIN = 0.03;
3
+ const MIN_BAND = 0.15;
4
+ const MIN_SPLIT = 0.5;
5
+ export function framed(slide) {
6
+ return {
7
+ x: Math.round((slide.width * (1 - FRAME)) / 2),
8
+ y: Math.round((slide.height * (1 - FRAME)) / 2),
9
+ width: Math.round(slide.width * FRAME),
10
+ height: Math.round(slide.height * FRAME)
11
+ };
12
+ }
13
+ export function fitted(size, area) {
14
+ const scale = Math.min(area.width / size.width, area.height / size.height);
15
+ const width = Math.round(size.width * scale);
16
+ const height = Math.round(size.height * scale);
17
+ return {
18
+ x: area.x + Math.round((area.width - width) / 2),
19
+ y: area.y + Math.round((area.height - height) / 2),
20
+ width,
21
+ height
22
+ };
23
+ }
24
+ function bandBelow(slide, shapes, margin) {
25
+ const top = Math.max(...shapes.map(({ box }) => box.y + box.height)) + margin;
26
+ const band = {
27
+ x: margin,
28
+ y: top,
29
+ width: slide.width - 2 * margin,
30
+ height: slide.height - top - margin
31
+ };
32
+ return band.height >= slide.height * MIN_BAND ? band : null;
33
+ }
34
+ function halves(box, gap) {
35
+ if (box.width >= box.height) {
36
+ const half = Math.round((box.width - gap) / 2);
37
+ const rest = box.width - half - gap;
38
+ return [
39
+ { ...box, width: half },
40
+ { ...box, x: box.x + half + gap, width: rest }
41
+ ];
42
+ }
43
+ const half = Math.round((box.height - gap) / 2);
44
+ const rest = box.height - half - gap;
45
+ return [
46
+ { ...box, height: half },
47
+ { ...box, y: box.y + half + gap, height: rest }
48
+ ];
49
+ }
50
+ function split(shape, gap) {
51
+ const [kept, freed] = halves(shape.box, gap);
52
+ const box = shape.role === 'picture' ? fitted(shape.box, kept) : kept;
53
+ return { box: freed, cleared: [], beside: { shape, box } };
54
+ }
55
+ function largest(shapes) {
56
+ return shapes.reduce((best, shape) => !best || shape.box.width * shape.box.height > best.box.width * best.box.height ? shape : best, undefined);
57
+ }
58
+ function overlaps(a, b) {
59
+ return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
60
+ }
61
+ function place(slide, shapes) {
62
+ const slot = shapes.find((shape) => shape.role === 'slot');
63
+ if (slot) {
64
+ return { box: slot.box, cleared: [slot], beside: null };
65
+ }
66
+ const whole = { box: framed(slide), cleared: [], beside: null };
67
+ if (!shapes.length) {
68
+ return whole;
69
+ }
70
+ const margin = Math.round(slide.height * MARGIN);
71
+ const band = bandBelow(slide, shapes, margin);
72
+ if (band) {
73
+ return { box: band, cleared: [], beside: null };
74
+ }
75
+ const bodies = shapes.filter((shape) => shape.role === 'body');
76
+ const wide = bodies.filter((shape) => shape.box.width >= slide.width * MIN_SPLIT);
77
+ const shared = largest(wide) ?? largest(shapes.filter((shape) => shape.role === 'picture')) ?? largest(bodies);
78
+ return shared ? split(shared, margin) : whole;
79
+ }
80
+ export function pictureArea(slide, shapes) {
81
+ const area = place(slide, shapes.filter((shape) => shape.role !== 'blank'));
82
+ const covered = shapes.filter((shape) => shape.role === 'blank' && overlaps(shape.box, area.box));
83
+ return { ...area, cleared: [...area.cleared, ...covered] };
84
+ }
@@ -50,5 +50,7 @@ export declare class Presentation {
50
50
  private chartOf;
51
51
  private chartPart;
52
52
  private box;
53
+ private narrow;
54
+ private pictureArea;
53
55
  private pictureBox;
54
56
  }
@@ -12,6 +12,7 @@ import * as fit from './fit.js';
12
12
  import * as template from './template.js';
13
13
  import * as slides from './slides.js';
14
14
  import * as notes from './notes.js';
15
+ import { fitted, framed, pictureArea } from './picture-area.js';
15
16
  import { pick, describe, layoutName, layoutType, BODY_TYPES } from './read.js';
16
17
  import { SHAPE_ORDER, shapesOf, placeholderFrom, pictureElement, textBoxElement, tableElement, chartFrameElement, setBackground, nextShapeId } from './shapes.js';
17
18
  const { a: A, p: P, r: R, p14: P14 } = xml.ns;
@@ -154,16 +155,19 @@ function checkSlideText(doc, at, opts) {
154
155
  }
155
156
  }
156
157
  }
157
- function fitted(info, current) {
158
- const scale = Math.min(current.width / info.width, current.height / info.height);
159
- const width = Math.round(info.width * scale);
160
- const height = Math.round(info.height * scale);
161
- return {
162
- x: current.x + Math.round((current.width - width) / 2),
163
- y: current.y + Math.round((current.height - height) / 2),
164
- width,
165
- height
166
- };
158
+ function roleOf(shape) {
159
+ const { el, kind, placeholder } = shape;
160
+ if (kind === 'picture') {
161
+ return 'picture';
162
+ }
163
+ if (el.localName !== 'sp' || !placeholder || !BODY_TYPES.has(placeholder)) {
164
+ return 'other';
165
+ }
166
+ const txBody = xml.child(el, P, 'txBody');
167
+ if (txBody && text.readParagraphs(txBody).some((p) => p.text.trim())) {
168
+ return placeholder === 'subTitle' ? 'other' : 'body';
169
+ }
170
+ return placeholder === 'obj' ? 'slot' : 'blank';
167
171
  }
168
172
  function tableBody(tc) {
169
173
  const existing = xml.child(tc, A, 'txBody');
@@ -366,6 +370,9 @@ export class Presentation {
366
370
  const layoutDoc = this.package.xml(layout);
367
371
  const doc = slideFromLayout(layoutDoc);
368
372
  checkSlideText(doc, at, opts);
373
+ if (opts.image !== undefined) {
374
+ probe(opts.image);
375
+ }
369
376
  this.insertSlide({ doc, layout, at, ids: all.map((e) => e.id), before: all[at]?.el ?? null });
370
377
  if (opts.title !== undefined) {
371
378
  this.setText({ slide: at, shape: 'title' }, opts.title);
@@ -373,14 +380,25 @@ export class Presentation {
373
380
  if (opts.body !== undefined) {
374
381
  this.setText({ slide: at, shape: 'body' }, opts.body);
375
382
  }
376
- return { index: at, layout: layoutName(layoutDoc), shapes: shapesOf(doc).map(describe) };
383
+ if (opts.image !== undefined) {
384
+ this.addPicture(at, opts.image);
385
+ }
386
+ const shapes = shapesOf(this.package.xml(slides.slideAt(this.deck, at).part));
387
+ return { index: at, layout: layoutName(layoutDoc), shapes: shapes.map(describe) };
377
388
  }
378
389
  addPicture(slide, bytes, opts = {}) {
379
390
  const entry = slides.slideAt(this.deck, slide);
380
- const box = this.pictureBox(probe(bytes), opts);
391
+ const area = this.pictureArea(entry.part, opts);
392
+ const box = this.pictureBox(probe(bytes), opts, area.box);
381
393
  const { rId } = media.addImage(this.package, bytes, { source: entry.part, dir: '../media' });
382
394
  const doc = this.package.xml(entry.part);
383
395
  const id = nextShapeId(doc);
396
+ for (const shape of area.cleared) {
397
+ xml.remove(shape.el);
398
+ }
399
+ if (area.beside) {
400
+ this.narrow(entry.part, area.beside);
401
+ }
384
402
  this.spTree(entry.part).appendChild(pictureElement(doc, { id, rId, box, name: opts.name, description: opts.description }));
385
403
  this.package.touch(entry.part);
386
404
  return { id, ...box };
@@ -722,14 +740,34 @@ export class Presentation {
722
740
  }
723
741
  return box;
724
742
  }
725
- pictureBox(info, opts) {
743
+ narrow(part, { shape, box }) {
744
+ writeXfrm(shape.el, box);
745
+ const txBody = xml.child(shape.el, P, 'txBody');
746
+ const estimate = txBody ? this.estimate(part, shape.el, txBody) : null;
747
+ if (txBody && estimate?.overflow) {
748
+ applyAutofit(txBody, estimate);
749
+ }
750
+ }
751
+ pictureArea(part, opts) {
752
+ if (opts.x !== undefined || opts.y !== undefined) {
753
+ return { box: framed(this.slideSize), cleared: [], beside: null };
754
+ }
755
+ const shapes = shapesOf(this.package.xml(part)).flatMap((shape) => {
756
+ if (shape.group !== null || SKIPPED_PLACEHOLDERS.has(shape.placeholder ?? '')) {
757
+ return [];
758
+ }
759
+ const box = inherit.box({ pkg: this.package, slide: part, el: shape.el });
760
+ return box ? [{ el: shape.el, box, role: roleOf(shape) }] : [];
761
+ });
762
+ return pictureArea(this.slideSize, shapes);
763
+ }
764
+ pictureBox(info, opts, area) {
726
765
  checkPlacement(opts);
727
- const slide = this.slideSize;
728
766
  const ratio = info.height / info.width;
729
767
  let { width, height } = opts;
730
768
  if (width === undefined && height === undefined) {
731
769
  const natural = { width: info.width * EMU_PER_PIXEL, height: info.height * EMU_PER_PIXEL };
732
- const scale = Math.min(1, (slide.width * 0.8) / natural.width, (slide.height * 0.8) / natural.height);
770
+ const scale = Math.min(1, area.width / natural.width, area.height / natural.height);
733
771
  width = Math.round(natural.width * scale);
734
772
  height = Math.round(natural.height * scale);
735
773
  }
@@ -742,8 +780,8 @@ export class Presentation {
742
780
  if (!width || !height) {
743
781
  throw new DocsError('INVALID_BOX', 'the picture would have no visible size');
744
782
  }
745
- const x = opts.x ?? Math.max(0, Math.round((slide.width - width) / 2));
746
- const y = opts.y ?? Math.max(0, Math.round((slide.height - height) / 2));
783
+ const x = opts.x ?? Math.max(0, area.x + Math.round((area.width - width) / 2));
784
+ const y = opts.y ?? Math.max(0, area.y + Math.round((area.height - height) / 2));
747
785
  return { x, y, width, height };
748
786
  }
749
787
  }
@@ -61,6 +61,7 @@ export interface AddSlideOptions {
61
61
  at?: number;
62
62
  title?: TextInput;
63
63
  body?: TextInput;
64
+ image?: Uint8Array;
64
65
  }
65
66
  export interface AddedSlide {
66
67
  index: number;
@@ -156,6 +156,7 @@ export declare class Workbook {
156
156
  private ownCharts;
157
157
  private rangeValues;
158
158
  private besideData;
159
+ private rowsBelowDrawings;
159
160
  private drawingOf;
160
161
  private ensureDrawing;
161
162
  }
@@ -2,6 +2,7 @@ import { Package } from '../opc/package.js';
2
2
  import * as xml from '../xml.js';
3
3
  import * as media from '../opc/media.js';
4
4
  import { probe } from '../image.js';
5
+ import { points } from '../units.js';
5
6
  import * as refs from './refs.js';
6
7
  import * as formula from './formula.js';
7
8
  import { Sheet, SHEET_ORDER } from './sheet.js';
@@ -170,6 +171,12 @@ function pictureAnchor(doc, anchor, spec) {
170
171
  root.appendChild(d('clientData'));
171
172
  return root;
172
173
  }
174
+ function markerCell(marker) {
175
+ return {
176
+ row: Number(xml.text(xml.child(marker, XDR, 'row'))),
177
+ col: Number(xml.text(xml.child(marker, XDR, 'col')))
178
+ };
179
+ }
173
180
  function nextDrawingId(doc) {
174
181
  return (Math.max(0, ...xml.descendants(doc, XDR, 'cNvPr').map((el) => Number(el.getAttribute('id')) || 0)) + 1);
175
182
  }
@@ -421,7 +428,7 @@ export class Workbook {
421
428
  const target = this.sheet(sheet);
422
429
  const info = probe(bytes);
423
430
  const o = isRecord(opts) ? opts : {};
424
- const at = refs.parse(o.cell === undefined ? 'A1' : o.cell);
431
+ const at = o.cell === undefined ? this.besideData(target) : refs.parse(o.cell);
425
432
  const size = imageSize(o, info);
426
433
  const drawing = this.ensureDrawing(target);
427
434
  const { rId } = media.addImage(this.package, bytes, { source: drawing, dir: '../media' });
@@ -791,7 +798,8 @@ export class Workbook {
791
798
  const from = xml.child(anchor, XDR, 'from');
792
799
  const info = chart.readChart(this.package.xml(rel.part));
793
800
  if (from) {
794
- info.cell = refs.cell(Number(xml.text(xml.child(from, XDR, 'row'))), Number(xml.text(xml.child(from, XDR, 'col'))));
801
+ const { row, col } = markerCell(from);
802
+ info.cell = refs.cell(row, col);
795
803
  }
796
804
  out.push(info);
797
805
  }
@@ -823,7 +831,30 @@ export class Workbook {
823
831
  }
824
832
  besideData(sheet) {
825
833
  const used = sheet.usedRange();
826
- return used ? { row: used.top, col: used.right + 2 } : { row: 0, col: 0 };
834
+ const col = used ? used.right + 2 : 0;
835
+ return { row: Math.max(used ? used.top : 0, ...this.rowsBelowDrawings(sheet, col)), col };
836
+ }
837
+ rowsBelowDrawings(sheet, col) {
838
+ const drawing = this.drawingOf(sheet);
839
+ if (!drawing) {
840
+ return [];
841
+ }
842
+ const format = xml.child(sheet.root, X, 'sheetFormatPr');
843
+ const rowHeight = points(Number(format?.getAttribute('defaultRowHeight') ?? 15));
844
+ return xml.children(this.package.root(drawing), XDR).flatMap((anchor) => {
845
+ const from = xml.child(anchor, XDR, 'from');
846
+ if (!from || markerCell(from).col !== col) {
847
+ return [];
848
+ }
849
+ const to = xml.child(anchor, XDR, 'to');
850
+ if (to) {
851
+ return [markerCell(to).row + 2];
852
+ }
853
+ const ext = xml.child(anchor, XDR, 'ext');
854
+ return ext
855
+ ? [markerCell(from).row + Math.ceil(Number(ext.getAttribute('cy')) / rowHeight) + 1]
856
+ : [];
857
+ });
827
858
  }
828
859
  drawingOf(sheet) {
829
860
  const ref = xml.child(sheet.root, X, 'drawing');
@@ -8,7 +8,7 @@ import HRPC from '../spec/hrpc/index.js';
8
8
  import { isCapability } from './capabilities/invoke.js';
9
9
  import { CUSTOM_TASK_TYPES } from './models/model-catalog.js';
10
10
  import { unsupportedKvCacheDeletion } from './capabilities/kv-cache.js';
11
- import { errorMessage } from './guards.js';
11
+ import { errorMessage, isRecord } from './guards.js';
12
12
  import { hlog } from './log.js';
13
13
  import { pushQueue } from './queue.js';
14
14
  import { TextCoalescer } from './helpers/text-coalescer.js';
@@ -23,7 +23,8 @@ const MODEL_STATUSES = new Set([
23
23
  'ready',
24
24
  'error',
25
25
  'unloading',
26
- 'unloaded'
26
+ 'unloaded',
27
+ 'held'
27
28
  ]);
28
29
  const DOWNLOAD_PROGRESS_INTERVAL_MS = 500;
29
30
  const WIRE_HIGH_WATER_MARK = 64;
@@ -50,6 +51,20 @@ export function serveHarness(stream, harness) {
50
51
  await harness.setModelIdleTimeout?.(ms);
51
52
  return { ok: true };
52
53
  },
54
+ setModelMemoryCap: async ({ percent }) => {
55
+ await harness.setModelMemoryCap?.(percent);
56
+ return { ok: true };
57
+ },
58
+ memoryUsage: async () => {
59
+ if (!harness.memoryUsage) {
60
+ throw new Error('harness does not support memoryUsage');
61
+ }
62
+ return harness.memoryUsage();
63
+ },
64
+ setDownloadsAllowed: async ({ allowed }) => {
65
+ await harness.setDownloadsAllowed?.({ allowed: allowed === true });
66
+ return { ok: true };
67
+ },
53
68
  listManagedSkills: async () => ({
54
69
  skills: (await harness.listManagedSkills?.()) ?? []
55
70
  }),
@@ -282,7 +297,8 @@ function modelEvents(harness) {
282
297
  total,
283
298
  message: state.message,
284
299
  catalogChanged,
285
- loaded
300
+ loaded,
301
+ addon: state.addon
286
302
  };
287
303
  const now = Date.now();
288
304
  if (state.status === 'downloading') {
@@ -509,10 +525,14 @@ function completion(harness) {
509
525
  model: frame.model ?? '',
510
526
  agent: frame.agent ?? {},
511
527
  cacheKey: frame.cacheKey ?? undefined,
528
+ session: frame.session ?? undefined,
512
529
  thinkingDefault: frame.thinkingDefault ?? undefined,
513
530
  generationParams: frame.generationParams ?? undefined,
514
531
  responseFormat: frame.responseFormat ?? undefined,
515
532
  captureInferenceCalls: frame.captureInferenceCalls ?? undefined,
533
+ typedText: isRecord(frame.payload) && typeof frame.payload.typedText === 'string'
534
+ ? frame.payload.typedText
535
+ : undefined,
516
536
  messages: (frame.messages ?? []).map(({ role, content, attachments }) => ({
517
537
  role,
518
538
  content,
@@ -46,9 +46,8 @@ export function spawnHarness(opts) {
46
46
  batchSize: opts.embedBatchSize
47
47
  });
48
48
  return connectHarness(async () => {
49
- const { default: Sidecar } = await import('bare-sidecar');
50
- const { wrap } = await import('bare-stow/host');
51
- const sidecar = new Sidecar(opts.entry, [
49
+ const { default: launch } = await import('bare-supervisor/runner/sidecar');
50
+ const { ipc, exit } = launch(opts.entry, [
52
51
  opts.skillsDir ?? '',
53
52
  JSON.stringify(opts.sdkStorage),
54
53
  JSON.stringify(embedConfig),
@@ -60,13 +59,8 @@ export function spawnHarness(opts) {
60
59
  opts.musicClamps ? JSON.stringify(opts.musicClamps) : '',
61
60
  opts.wheelsDir ?? '',
62
61
  opts.gpuStats === undefined ? '' : JSON.stringify(opts.gpuStats)
63
- ]);
64
- sidecar.stdout.on('data', (data) => logger?.info('harness', data.toString().trimEnd()));
65
- sidecar.stderr.on('data', (data) => logger?.warn('harness', data.toString().trimEnd()));
66
- const ipc = wrap(sidecar);
67
- const died = new Promise((resolve) => {
68
- sidecar.once('exit', (code) => resolve(`harness exited (${code}) before ready`));
69
- });
62
+ ], logger);
63
+ const died = exit.then((code) => `harness exited (${code}) before ready`);
70
64
  const failure = await Promise.race([ipc.ready.then(() => null), died]);
71
65
  if (failure) {
72
66
  throw new Error(failure);
@@ -14,6 +14,7 @@ export interface ExecToolOptions {
14
14
  readonly pythonNetwork?: boolean;
15
15
  readonly credentialStore?: CredentialStoreFn;
16
16
  readonly skillSettings?: SkillSettingsFn;
17
+ readonly skillEnv?: SkillEnvFn;
17
18
  readonly realpath?: Realpath;
18
19
  readonly deniedRoots?: readonly string[];
19
20
  readonly skillsDir?: string;
@@ -23,4 +24,6 @@ export interface ScriptRun {
23
24
  trusted: boolean;
24
25
  }
25
26
  export declare function resolveScriptRun(rawArgs: Record<string, unknown>, skillsDir: string | undefined): ScriptRun | ExecError | null;
26
- export declare function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide, pythonNetwork, credentialStore, skillSettings, realpath, deniedRoots, skillsDir }?: ExecToolOptions): ToolDefinition;
27
+ export type SkillEnvFn = (skill: string) => Readonly<Record<string, string>> | undefined;
28
+ export declare function declaredEnv(declared: Readonly<Record<string, string>> | undefined, session: string | undefined): Record<string, string>;
29
+ export declare function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide, pythonNetwork, credentialStore, skillSettings, skillEnv, realpath, deniedRoots, skillsDir }?: ExecToolOptions): ToolDefinition;
@@ -6,7 +6,7 @@ import { namedHosts } from './exec/adapters/python/named-hosts.js';
6
6
  import { pythonReachesNetwork } from './exec/adapters/python/net-shim.js';
7
7
  import { createShellAdapter } from './exec/adapters/shell/shell-adapter.js';
8
8
  import { routeObsidianCommand } from './exec/obsidian/route.js';
9
- import { MAX_OUTPUT_CHARS_LIMIT } from './exec/runner.js';
9
+ import { MAX_OUTPUT_CHARS, MAX_OUTPUT_CHARS_LIMIT, TRUNCATED_OUTPUT_SUFFIX } from './exec/runner.js';
10
10
  import { bareBin, firstCommandToken, sanitizeCommand } from './exec/scope.js';
11
11
  import { FILES_NEED_PYTHON_ERROR, isPythonSourceShaped, PACKAGES_NEED_PYTHON_ERROR, pythonRedirectError, startsLikePython, videoToolError } from './exec/shell-guidance.js';
12
12
  import { shellArgv } from './exec/shell-argv.js';
@@ -47,6 +47,30 @@ function shellEffect(command, toolOps) {
47
47
  }
48
48
  return matched.every((op) => op.annotation(argv) === 'read') ? 'read' : 'write';
49
49
  }
50
+ function shellOutputCap(command, toolOps) {
51
+ const { ops, argv } = resolveShellOps(command, toolOps);
52
+ const caps = ops.map((op) => op.outputCap(argv)).filter((cap) => cap !== undefined);
53
+ return caps.length > 0 ? Math.min(...caps) : undefined;
54
+ }
55
+ function noteCeiling(result, ceiling) {
56
+ if (!result || typeof result !== 'object')
57
+ return result;
58
+ const out = { ...result };
59
+ for (const stream of ['stdout', 'stderr']) {
60
+ const text = out[stream];
61
+ if (typeof text === 'string' && text.endsWith(TRUNCATED_OUTPUT_SUFFIX)) {
62
+ out[stream] =
63
+ text.slice(0, -TRUNCATED_OUTPUT_SUFFIX.length) +
64
+ `… [truncated at this skill's ${ceiling}-character limit for this command; a larger maxOutputChars does not apply]`;
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+ function cappedOutput(requested, ceiling) {
70
+ if (ceiling === undefined)
71
+ return requested;
72
+ return Math.min(requested ?? MAX_OUTPUT_CHARS, ceiling);
73
+ }
50
74
  function resolveExecution({ adapters, fallback }, args, ctx) {
51
75
  for (const adapter of adapters) {
52
76
  const plan = adapter.resolve(args, ctx);
@@ -289,18 +313,33 @@ function checkExecutionScope(args, { toolOps, disablePyodide, sandbox, network }
289
313
  }
290
314
  return checkShellScope(command, toolOps);
291
315
  }
292
- async function skillsEnv(skillSettings, ctx) {
316
+ async function skillsEnv(skillSettings, skillEnv, ctx) {
293
317
  const skills = ctx?.skills ?? [];
294
318
  if (skills.length === 0) {
295
319
  return undefined;
296
320
  }
297
321
  const env = {};
298
322
  for (const skill of skills) {
299
- Object.assign(env, settingsEnv(skill, await skillSettings(skill, ctx?.agentId)));
323
+ if (skillSettings) {
324
+ Object.assign(env, settingsEnv(skill, await skillSettings(skill, ctx?.agentId)));
325
+ }
326
+ Object.assign(env, declaredEnv(skillEnv?.(skill), ctx?.session));
300
327
  }
301
328
  return Object.keys(env).length > 0 ? env : undefined;
302
329
  }
303
- export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide = false, pythonNetwork = !disablePyodide && pythonReachesNetwork(), credentialStore, skillSettings, realpath = defaultRealpath, deniedRoots, skillsDir } = {}) {
330
+ export function declaredEnv(declared, session) {
331
+ const env = {};
332
+ for (const [name, value] of Object.entries(declared ?? {})) {
333
+ if (!value.includes('${session}')) {
334
+ env[name] = value;
335
+ }
336
+ else if (session) {
337
+ env[name] = value.replaceAll('${session}', session);
338
+ }
339
+ }
340
+ return env;
341
+ }
342
+ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide = false, pythonNetwork = !disablePyodide && pythonReachesNetwork(), credentialStore, skillSettings, skillEnv, realpath = defaultRealpath, deniedRoots, skillsDir } = {}) {
304
343
  const adapters = [createPythonAdapter(disablePyodide, loadPyodide, pythonNetwork)];
305
344
  const fallback = createShellAdapter({ spawn, killProcess, loadRuntime: bareRuntime });
306
345
  return {
@@ -349,7 +388,7 @@ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide
349
388
  },
350
389
  maxOutputChars: {
351
390
  type: 'number',
352
- description: 'Optional stdout/stderr cap in characters (default 8192, max 65536). Raise when reading long content, e.g. a full note.'
391
+ description: 'Optional stdout/stderr cap in characters (default 8192, max 65536). Raise when reading long content, e.g. a full note. A skill may cap one of its commands lower; the truncation note says so, and raising this does not lift that cap.'
353
392
  },
354
393
  script: {
355
394
  type: 'string',
@@ -564,10 +603,12 @@ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide
564
603
  timeout = Math.min(timeout, remaining);
565
604
  }
566
605
  }
567
- const maxOutputChars = parsePositiveInt(rawArgs.maxOutputChars, 'maxOutputChars', MAX_OUTPUT_CHARS_LIMIT);
568
- if (maxOutputChars && typeof maxOutputChars === 'object') {
569
- return maxOutputChars;
606
+ const requestedCap = parsePositiveInt(rawArgs.maxOutputChars, 'maxOutputChars', MAX_OUTPUT_CHARS_LIMIT);
607
+ if (requestedCap && typeof requestedCap === 'object') {
608
+ return requestedCap;
570
609
  }
610
+ const ceiling = shellOutputCap(command, ctx?.toolOps);
611
+ const maxOutputChars = cappedOutput(requestedCap, ceiling);
571
612
  if (!isPythonLanguage(rawArgs.language)) {
572
613
  const obsidian = await routeObsidianCommand(command, ctx, {
573
614
  credentialStore,
@@ -581,7 +622,7 @@ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide
581
622
  return obsidian;
582
623
  }
583
624
  }
584
- const skillEnv = skillSettings ? await skillsEnv(skillSettings, ctx) : undefined;
625
+ const childEnv = await skillsEnv(skillSettings, skillEnv, ctx);
585
626
  try {
586
627
  const execution = resolveExecution({ adapters, fallback }, rawArgs, {
587
628
  attach: ctx?.attach,
@@ -593,13 +634,16 @@ export function createExecTool({ spawn, killProcess, loadPyodide, disablePyodide
593
634
  approval: ctx?.approval,
594
635
  confirmConnect: ctx?.askUser ? connectAsker(ctx.askUser) : undefined
595
636
  });
596
- return await execution.run({
637
+ const result = await execution.run({
597
638
  cwd,
598
639
  timeoutMs: timeout,
599
640
  maxOutputChars,
600
641
  signal: ctx?.signal,
601
- env: skillEnv
642
+ env: childEnv
602
643
  });
644
+ return ceiling !== undefined && maxOutputChars === ceiling
645
+ ? noteCeiling(result, ceiling)
646
+ : result;
603
647
  }
604
648
  catch (err) {
605
649
  const error = err instanceof Error ? err : new Error(String(err));
@@ -3,6 +3,7 @@ import type { ToolDefinition } from '../capabilities/completion.ts';
3
3
  import type { ModelSrc } from '../models/context.ts';
4
4
  import { type ImageClamps } from '../models/model-catalog.ts';
5
5
  import { type LoadTracker, type TrackedLoad } from '../models/model-pool.ts';
6
+ import type { HoldFn } from '../models/download-gate.ts';
6
7
  import { type ModelRegistrySdk } from '../models/model-source.ts';
7
8
  export declare const MAX_DIM = 1024;
8
9
  export declare const DIM_STEP = 64;
@@ -68,7 +69,8 @@ export interface DiffusionRuntimeOptions extends ImageModelSet {
68
69
  loadSdk: () => Promise<DiffusionSdk>;
69
70
  onProgress?: (model: string, progress: Record<string, unknown>) => void;
70
71
  onLoaded?: (model: string) => void;
71
- onBeforeLoad?: (signal?: BareAbortSignal) => Promise<void>;
72
+ onBeforeLoad?: (signal: BareAbortSignal | undefined, srcs: ReadonlyArray<string>) => Promise<void>;
73
+ hold?: HoldFn;
72
74
  loads?: LoadTracker;
73
75
  stallMs?: number;
74
76
  }
@@ -87,7 +89,7 @@ export interface DiffusionRuntime {
87
89
  }
88
90
  export declare function generationPercentTick(onPercent: (percentage: number) => void, encodePhases?: number): (tick: DiffusionTick) => void;
89
91
  export declare const IMAGE_LOAD_STALL_ERROR = "image model download stalled";
90
- export declare function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBeforeLoad, loads, stallMs, ...base }: DiffusionRuntimeOptions): DiffusionRuntime;
92
+ export declare function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBeforeLoad, hold, loads, stallMs, ...base }: DiffusionRuntimeOptions): DiffusionRuntime;
91
93
  export declare const BUSY_ERROR = "an image generation is already running; try again when it finishes";
92
94
  export interface ImageToolError {
93
95
  error: string;
@@ -39,7 +39,7 @@ function diffusionProgressName(set) {
39
39
  return entry?.name ?? set.model;
40
40
  }
41
41
  export const IMAGE_LOAD_STALL_ERROR = 'image model download stalled';
42
- export function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBeforeLoad, loads = createLoadTracker(), stallMs = IMAGE_LOAD_STALL_MS, ...base }) {
42
+ export function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBeforeLoad, hold, loads = createLoadTracker(), stallMs = IMAGE_LOAD_STALL_MS, ...base }) {
43
43
  let loading = null;
44
44
  let residentFingerprint = null;
45
45
  let busy = false;
@@ -76,13 +76,16 @@ export function createDiffusionRuntime({ loadSdk, onProgress, onLoaded, onBefore
76
76
  ensureLoaded: async (sdk, signal, modelChoice) => {
77
77
  const set = (modelChoice && imageModelFor(modelChoice)) || base;
78
78
  const fingerprint = fingerprintOf(set);
79
+ if (hold && !(loading && residentFingerprint === fingerprint)) {
80
+ await hold(diffusionProgressName(set), 'diffusion', signal);
81
+ }
79
82
  await lock.lock();
80
83
  try {
81
84
  if (loading && residentFingerprint !== fingerprint) {
82
85
  await doUnload();
83
86
  }
84
87
  if (!loading && onBeforeLoad) {
85
- await onBeforeLoad(signal);
88
+ await onBeforeLoad(signal, [set.model, set.llmModel, set.vaeModel].filter((src) => src !== undefined));
86
89
  }
87
90
  residentFingerprint = fingerprint;
88
91
  loading ??= (async () => {
@@ -2,6 +2,7 @@ import type { AbortSignal as BareAbortSignal } from 'bare-abort-controller';
2
2
  import type { MusicSettings, ToolDefinition, ToolExecuteContext } from '../capabilities/completion.ts';
3
3
  import type { ModelSrc } from '../models/context.ts';
4
4
  import { type LoadTracker, type TrackedLoad } from '../models/model-pool.ts';
5
+ import type { HoldFn } from '../models/download-gate.ts';
5
6
  import { type ModelRegistrySdk } from '../models/model-source.ts';
6
7
  export declare const MAX_DURATION_SECONDS = 300;
7
8
  export declare const MIN_DURATION_SECONDS = 1;
@@ -75,7 +76,8 @@ export interface MusicRuntimeOptions extends MusicModelStages {
75
76
  inferenceSteps?: number;
76
77
  useGpu?: boolean;
77
78
  onProgress?: (model: string, progress: Record<string, unknown>) => void;
78
- onBeforeLoad?: (signal?: BareAbortSignal) => Promise<void>;
79
+ onBeforeLoad?: (signal: BareAbortSignal | undefined, srcs: ReadonlyArray<string>) => Promise<void>;
80
+ hold?: HoldFn;
79
81
  loads?: LoadTracker;
80
82
  }
81
83
  export interface LoadedMusicModel {
@@ -97,7 +99,7 @@ export interface MusicRuntime {
97
99
  release(): void;
98
100
  }
99
101
  export declare function musicPercentTick(onPercent: (percentage: number) => void): (tick: MusicTick) => void;
100
- export declare function createMusicRuntime({ loadSdk, onProgress, onBeforeLoad, loads, inferenceSteps: baseSteps, useGpu, ...base }: MusicRuntimeOptions): MusicRuntime;
102
+ export declare function createMusicRuntime({ loadSdk, onProgress, onBeforeLoad, hold, loads, inferenceSteps: baseSteps, useGpu, ...base }: MusicRuntimeOptions): MusicRuntime;
101
103
  export declare const BUSY_ERROR = "a music generation is already running; try again when it finishes";
102
104
  export interface MusicToolError {
103
105
  error: string;
@@ -49,7 +49,7 @@ function musicProgressName(dit) {
49
49
  const entry = AUDIOGEN_CATALOG.find((candidate) => candidate.dit.src === dit || candidate.dit.name === dit);
50
50
  return entry?.name ?? dit;
51
51
  }
52
- export function createMusicRuntime({ loadSdk, onProgress, onBeforeLoad, loads = createLoadTracker(), inferenceSteps: baseSteps, useGpu = true, ...base }) {
52
+ export function createMusicRuntime({ loadSdk, onProgress, onBeforeLoad, hold, loads = createLoadTracker(), inferenceSteps: baseSteps, useGpu = true, ...base }) {
53
53
  let loading = null;
54
54
  let residentFingerprint = null;
55
55
  let busy = false;
@@ -89,13 +89,16 @@ export function createMusicRuntime({ loadSdk, onProgress, onBeforeLoad, loads =
89
89
  const stages = stagesFor(modelChoice, base);
90
90
  const effectiveSteps = steps ?? baseSteps;
91
91
  const fingerprint = fingerprintOf(stages, effectiveSteps);
92
+ if (hold && !(loading && residentFingerprint === fingerprint)) {
93
+ await hold(musicProgressName(stages.dit), 'audiogen', signal);
94
+ }
92
95
  await lock.lock();
93
96
  try {
94
97
  if (loading && residentFingerprint !== fingerprint) {
95
98
  await doUnload();
96
99
  }
97
100
  if (!loading && onBeforeLoad) {
98
- await onBeforeLoad(signal);
101
+ await onBeforeLoad(signal, [stages.dit, stages.textEnc, stages.lm, stages.vae]);
99
102
  }
100
103
  residentFingerprint = fingerprint;
101
104
  loading ??= (async () => {