@lampdotdev/lamp 0.36.2-nightly.41 → 0.36.2-nightly.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/lamps/lamp.js CHANGED
@@ -138,8 +138,20 @@
138
138
  delete asked[m.id];
139
139
  waiting({ status: m.status, body: m.body });
140
140
  };
141
- bridge = { postMessage: function (m) {
142
- return new Promise(function (resolve) { var id = ++askId; asked[id] = resolve; gate.postMessage(JSON.stringify({ id: id, path: m.path, body: m.body, timeout: m.timeout })); });
141
+ bridge = { postMessage: function (m, signal) {
142
+ return new Promise(function (resolve) {
143
+ var id = ++askId;
144
+ asked[id] = resolve;
145
+ gate.postMessage(JSON.stringify({ id: id, path: m.path, body: m.body, timeout: m.timeout }));
146
+ // A page that stops waiting tells the host, which can stop the answer (lamp dev does), and hears nothing after.
147
+ function stop() {
148
+ if (!asked[id]) return;
149
+ delete asked[id];
150
+ gate.postMessage(JSON.stringify({ id: id, cancel: true }));
151
+ resolve({ status: 499, body: '{}' });
152
+ }
153
+ if (signal) { if (signal.aborted) stop(); else signal.addEventListener('abort', stop); }
154
+ });
143
155
  } };
144
156
  }
145
157
  var preferred = null;
@@ -305,14 +317,14 @@
305
317
  // with a little more for the answer to come back.
306
318
  function AI_WAIT(seconds) { return (typeof seconds === 'number' && seconds > 0 ? Math.min(seconds, 600) : 600) * 1000 + 15000; }
307
319
 
308
- function post(path, body, timeout, deadline) {
320
+ function post(path, body, timeout, deadline, signal) {
309
321
  timeout = timeout || 240000;
310
322
  var done;
311
323
  if (!bridge && shared()) {
312
324
  done = nearby(path, body, timeout, deadline);
313
325
  } else if (bridge) {
314
326
  // The TV preview on the Mac answers for Lamp itself.
315
- done = bridge.postMessage({ path: path, body: JSON.stringify(body), timeout: timeout }).then(function (r) {
327
+ done = bridge.postMessage({ path: path, body: JSON.stringify(body), timeout: timeout }, signal).then(function (r) {
316
328
  var reply = {};
317
329
  try { reply = JSON.parse(r.body) || {}; } catch (e) {}
318
330
  return { status: r.status, reply: reply };
@@ -328,10 +340,15 @@
328
340
  });
329
341
  }
330
342
  return done.then(function (answer) {
343
+ // Stopped by the page: no failure of the AI's, so nothing is said about it.
344
+ if (signal && signal.aborted) throw stoppedFailure();
331
345
  if (answer.status === 200) return answer.reply;
332
346
  var message = answer.reply.error || 'The AI request failed.';
333
347
  if (window.console) console.error('LampAI ' + path + ' failed: ' + message);
334
- throw new Failure(message, answer.reply.needs || null);
348
+ var failed = new Failure(message, answer.reply.needs || null);
349
+ // What was billed before it failed (a retry, a timeout), for a turn's `cost`.
350
+ if (typeof answer.reply.cost === 'number' && answer.reply.cost > 0) failed.cost = answer.reply.cost;
351
+ throw failed;
335
352
  });
336
353
  }
337
354
 
@@ -346,14 +363,14 @@
346
363
  });
347
364
  }
348
365
 
349
- /** Downscales to at most 1568px on the long side (the most the model uses) and JPEG-encodes. Takes an <img>, a
350
- * <canvas>, or a picture's address (a data: URL or a file of the app's). */
351
- function encode(images) {
366
+ /** Downscales to at most 1568px on the long side (the most the model uses), or `side`, and JPEG-encodes. Takes an
367
+ * <img>, a <canvas>, or a picture's address (a data: URL or a file of the app's). */
368
+ function encode(images, side) {
352
369
  return Promise.all((images || []).map(function (image) {
353
370
  var ready = typeof image === 'string' ? loadImage(image) : Promise.resolve(image);
354
371
  return ready.then(function (source) {
355
372
  var w = source.naturalWidth || source.width, h = source.naturalHeight || source.height;
356
- var scale = Math.min(1, 1568 / Math.max(w, h, 1));
373
+ var scale = Math.min(1, (side || 1568) / Math.max(w, h, 1));
357
374
  var canvas = document.createElement('canvas');
358
375
  canvas.width = Math.max(1, Math.round(w * scale));
359
376
  canvas.height = Math.max(1, Math.round(h * scale));
@@ -483,21 +500,44 @@
483
500
  score: function (question, levels) { return { type: 'score', instructions: question, criteria: levels }; }
484
501
  };
485
502
 
503
+ function stoppedFailure() { var f = new Failure('Stopped.'); f.stopped = true; return f; }
504
+
505
+ // A promise the page can stop waiting for with an AbortSignal: it fails at once with `stopped`, whatever comes after.
506
+ function stoppable(promise, signal) {
507
+ if (!signal) return promise;
508
+ return new Promise(function (resolve, reject) {
509
+ function stop() { reject(stoppedFailure()); }
510
+ // Settled either way, so a stopped ask's own failure is never left unhandled.
511
+ if (signal.aborted) { promise.then(null, function () {}); return stop(); }
512
+ signal.addEventListener('abort', stop);
513
+ promise.then(function (v) { signal.removeEventListener('abort', stop); if (!signal.aborted) resolve(v); },
514
+ function (e) { signal.removeEventListener('abort', stop); if (!signal.aborted) reject(e); });
515
+ });
516
+ }
517
+
486
518
  function call(prompt, options) {
487
- var o = options || {}, text = prompt, rules = [o.system || '', present()];
519
+ var o = options || {}, text = prompt, rules = [o.system || '', present()], started = Date.now(), cost = 0;
520
+ var schema = o.schema && typeof o.schema === 'object' ? o.schema : null, json = !!o.json || !!schema;
488
521
  if (o.data) { var f = fenced(prompt, o.data); text = f.prompt; rules.push(f.rules); }
489
- return encode(o.images).then(function (images) {
490
- var body = { prompt: text, system: rules.filter(function (r) { return r; }).join('\n\n'), json: !!o.json, images: images };
522
+ return stoppable(encode(o.images).then(function (images) {
523
+ // Stopped before or while its pictures were read: nothing is asked.
524
+ if (o.signal && o.signal.aborted) throw stoppedFailure();
525
+ var body = { prompt: text, system: rules.filter(function (r) { return r; }).join('\n\n'), json: json, images: images };
526
+ if (schema) body.schema = schema;
491
527
  if (o.model) body.model = o.model;
492
528
  // No limit of the page's own unless it asks for one: Lamp's guard ends only a call that stopped answering.
493
529
  if (typeof o.seconds === 'number' && o.seconds > 0) body.seconds = o.seconds;
494
- return post('/ai', body, AI_WAIT(body.seconds));
530
+ return post('/ai', body, AI_WAIT(body.seconds), null, o.signal);
495
531
  }).then(function (reply) {
496
532
  if (typeof reply.text !== 'string') throw new Failure('The AI request failed.');
497
- if (!o.json) return reply.text;
498
- try { return JSON.parse(reply.text.replace(/^\s*```(?:json)?\s*|\s*```\s*$/g, '')); }
499
- catch (e) { throw new Failure('The AI\'s answer wasn\'t in the expected format. Try again.'); }
500
- });
533
+ cost = typeof reply.cost === 'number' && reply.cost > 0 ? reply.cost : 0;
534
+ var answer = reply.text;
535
+ if (json) {
536
+ try { answer = JSON.parse(reply.text.replace(/^\s*```(?:json)?\s*|\s*```\s*$/g, '')); }
537
+ catch (e) { throw new Failure('The AI\'s answer wasn\'t in the expected format. Try again.'); }
538
+ }
539
+ return o.details ? { answer: answer, seconds: Math.round((Date.now() - started) / 100) / 10, cost: cost } : answer;
540
+ }), o.signal);
501
541
  }
502
542
 
503
543
  function image(prompt, images, aspectRatio) {
@@ -538,6 +578,12 @@
538
578
  // What a turn's reads give back in all, which every later step carries again: kept under what a step may carry
539
579
  // (`MAX_BODY` in lamp's app-ai.mjs), with room for the request, the tools and the steps' own words.
540
580
  var ACT_TURN_RESULTS = 1200000;
581
+ // The pictures a step carries from its tools' results (`LampAI.picture`): the newest few, each at most this many
582
+ // pixels on its long side, together at most this many bytes, beside the text results under the step's limit
583
+ // (`MAX_PICTURES`, `MAX_BODY` in lamp's app-ai.mjs). Older ones the model has seen go as no longer shown.
584
+ var ACT_PICTURES = 6, ACT_PICTURE_SIDE = 1024, ACT_PICTURE_BYTES = 1600000;
585
+ /** A picture in a tool's result, for the AI to look at (`LampAI.picture`). */
586
+ function ActPicture(source) { this.source = source; }
541
587
  var ACT_STEP_SECONDS = 600;
542
588
  var actRan = {}; // turn id -> call key -> the result a write gave, while this copy of the page is open
543
589
  function actId() { return 't' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }
@@ -596,8 +642,50 @@
596
642
  }
597
643
  var ran = actRan[id];
598
644
  var started = Date.now(), deadline = asked ? started + Math.max(1, asked) * 1000 : Infinity;
599
- var out = { turn: id, reply: '', ask: false, choices: [], done: [], failed: [], skipped: [], reads: [], stopped: null, error: '' };
600
- var steps = [], route = null, writes = 0, over = false, settle;
645
+ var out = { turn: id, reply: '', ask: false, choices: [], done: [], failed: [], skipped: [], reads: [], stopped: null, error: '', seconds: 0, cost: 0 };
646
+ var steps = [], route = null, writes = 0, over = false, settle, pictureCount = 0;
647
+ // A result with its pictures taken out: each one sent beside it, scaled down, and named in its place
648
+ // ("[picture 2]", numbered across the turn), so the model sees what the app drew in the middle of the turn.
649
+ function pictured(value) {
650
+ var found = [];
651
+ function walk(v, depth) {
652
+ if (v instanceof ActPicture) { found.push({ source: v.source, number: ++pictureCount }); return '[picture ' + pictureCount + ']'; }
653
+ if (!v || typeof v !== 'object' || depth > 8) return v;
654
+ if (isList(v)) return v.map(function (x) { return walk(x, depth + 1); });
655
+ var proto = Object.getPrototypeOf(v);
656
+ if (proto !== Object.prototype && proto !== null) return v;
657
+ var copy = {};
658
+ for (var k in v) if (Object.prototype.hasOwnProperty.call(v, k)) copy[k] = walk(v[k], depth + 1);
659
+ return copy;
660
+ }
661
+ var kept = walk(value, 0);
662
+ if (!found.length) return Promise.resolve({ value: value });
663
+ // A picture that never loads costs the turn nothing: after a few seconds the result goes without it.
664
+ var slow = new Promise(function (resolve, reject) { browserTimeout(function () { reject(new Error('slow')); }, 10000); });
665
+ return Promise.race([encode(found.map(function (f) { return f.source; }), ACT_PICTURE_SIDE), slow]).then(function (encoded) {
666
+ return { value: kept, pictures: encoded.map(function (e, i) { return { media_type: e.media_type, data: e.data, number: found[i].number }; }) };
667
+ }, function () {
668
+ return { value: { result: kept, note: 'Its pictures couldn\'t be read, so they aren\'t shown.' } };
669
+ });
670
+ }
671
+ // The steps as sent: only the newest pictures go with their data, within the step's room for them.
672
+ function sentSteps() {
673
+ // Chosen newest first by number across every result, whatever order the results are in.
674
+ var room = ACT_PICTURE_BYTES, chosen = {}, all = [];
675
+ steps.forEach(function (s) { s.results.forEach(function (r) { (r.pictures || []).forEach(function (p) { all.push(p); }); }); });
676
+ all.sort(function (a, b) { return b.number - a.number; }).forEach(function (p) {
677
+ if (Object.keys(chosen).length < ACT_PICTURES && p.data.length <= room) { chosen[p.number] = true; room -= p.data.length; }
678
+ });
679
+ return steps.map(function (s) {
680
+ return { calls: s.calls, results: s.results.map(function (r) {
681
+ if (!r.pictures) return r;
682
+ var copy = {};
683
+ for (var k in r) if (Object.prototype.hasOwnProperty.call(r, k)) copy[k] = r[k];
684
+ copy.pictures = r.pictures.map(function (p) { return { media_type: p.media_type, number: p.number, data: chosen[p.number] ? p.data : null }; });
685
+ return copy;
686
+ }) };
687
+ });
688
+ }
601
689
  var declared = Object.keys(tools).map(function (name) {
602
690
  var t = tools[name];
603
691
  return { name: name, about: t.about || '', reads: !!t.reads, args: t.args || {} };
@@ -608,6 +696,7 @@
608
696
  over = true;
609
697
  out.stopped = reason || null;
610
698
  if (error) out.error = error;
699
+ out.seconds = Math.round((Date.now() - started) / 100) / 10;
611
700
  settle(out);
612
701
  }
613
702
  // Over, or past the deadline by the clock even if its timer hasn't run yet (a slow write can carry the turn past
@@ -622,72 +711,92 @@
622
711
 
623
712
  // Reads run together; then the writes, one after another, each checked right before it runs. A write that
624
713
  // fails stops the writes after it in this step, which the model hears were skipped.
625
- function runCalls(calls, step) {
626
- var results = {}, reads = [], changes = [];
627
- calls.forEach(function (c) { (tools[c.name] && tools[c.name].reads ? reads : changes).push(c); });
628
- return Promise.all(reads.map(function (c) {
714
+ // A step's calls, run as they come (`stream`) and in the order the AI wrote them: reads side by side run together,
715
+ // a write waits for every call before it, and a read after a write sees it. A write that fails skips the writes
716
+ // after it in the step. A call Lamp held back as not fitting (`problem`) isn't run: the AI hears why, and a change of
717
+ // that kind fails in its place.
718
+ function stepRunner(step) {
719
+ var results = {}, chain = Promise.resolve(), reading = [], stopped = null;
720
+ function read(c) {
629
721
  if (late()) return null;
630
722
  var tool = tools[c.name], bad = actProblem(tool, c.name, c.args);
631
723
  if (bad) { results[c.id] = { id: c.id, ok: false, error: bad }; return null; }
632
724
  tell({ kind: 'read', name: c.name, args: c.args });
633
725
  var NOT_RUN = {};
634
726
  return Promise.resolve().then(function () {
635
- // Reads start together, but each one only while there's time: an earlier one can be slow.
727
+ // Each read starts only while there's time: an earlier one can be slow.
636
728
  return late() ? NOT_RUN : tool.run(c.args, { turn: id, step: step, id: c.id });
637
729
  }).then(function (value) {
638
730
  if (value === NOT_RUN) return;
639
731
  out.reads.push({ name: c.name, args: c.args });
640
- results[c.id] = { id: c.id, ok: true, value: sized(value) };
732
+ return pictured(value).then(function (p) {
733
+ results[c.id] = { id: c.id, ok: true, value: sized(p.value) };
734
+ if (p.pictures) results[c.id].pictures = p.pictures;
735
+ });
641
736
  }, function (e) { results[c.id] = { id: c.id, ok: false, error: refused(e) }; });
642
- })).then(function () {
643
- var stopped = null;
644
- function next(i) {
645
- if (i >= changes.length) return results;
646
- var c = changes[i], tool = tools[c.name];
647
- function skip(reason) {
648
- out.skipped.push({ name: c.name, args: c.args, reason: reason });
649
- results[c.id] = { id: c.id, ok: false, skipped: reason };
650
- return next(i + 1);
651
- }
652
- function fail(reason) {
653
- out.failed.push({ name: c.name, args: c.args, reason: reason });
654
- results[c.id] = { id: c.id, ok: false, error: reason };
655
- stopped = stopped || 'An earlier change in this step didn\'t go through.';
656
- return next(i + 1);
657
- }
658
- if (late()) return results;
659
- if (stopped) return skip(stopped);
660
- var bad = actProblem(tool, c.name, c.args);
661
- if (bad) return fail(bad);
662
- var key = 'k' + hash(id + '|' + c.name + '|' + canonical(c.args));
663
- // The same change asked again in this turn gets its first result, and doesn't run again.
664
- // Its result is in an earlier step, which every request carries: pointed to, not sent again.
665
- if (ran.hasOwnProperty(key)) { results[c.id] = { id: c.id, ok: true, repeat: true, value: { note: 'Already done in this request: its result is in an earlier step.' } }; return next(i + 1); }
666
- if (writes >= limits.writes) return fail('Over this turn\'s limit of ' + limits.writes + ' changes.');
667
- var call = { turn: id, key: key, step: step, id: c.id };
668
- return Promise.resolve().then(function () { return tool.check ? tool.check(c.args, call) : null; }).then(function (problem) {
669
- if (problem) return fail(String(problem));
670
- // Cancel and the time are looked at again right before the change, never in the middle of it.
671
- if (late()) return results;
672
- tell({ kind: 'write', name: c.name, args: c.args });
673
- if (late()) return results;
674
- writes++;
675
- var running;
676
- try { running = Promise.resolve(tool.run(c.args, call)); } catch (e) { running = Promise.reject(e); }
677
- return running.then(function (value) {
678
- var kept = sized(value);
737
+ }
738
+ function write(c) {
739
+ var tool = tools[c.name];
740
+ function skip(reason) {
741
+ out.skipped.push({ name: c.name, args: c.args, reason: reason });
742
+ results[c.id] = { id: c.id, ok: false, skipped: reason };
743
+ }
744
+ function fail(reason) {
745
+ out.failed.push({ name: c.name, args: c.args, reason: reason });
746
+ results[c.id] = { id: c.id, ok: false, error: reason };
747
+ stopped = stopped || 'An earlier change in this step didn\'t go through.';
748
+ }
749
+ if (late()) return;
750
+ if (stopped) return skip(stopped);
751
+ // A change Lamp held back as not fitting failed, in its place.
752
+ if (typeof c.problem === 'string') return fail(c.problem);
753
+ var bad = actProblem(tool, c.name, c.args);
754
+ if (bad) return fail(bad);
755
+ var key = 'k' + hash(id + '|' + c.name + '|' + canonical(c.args));
756
+ // The same change asked again in this turn gets its first result, and doesn't run again.
757
+ // Its result is in an earlier step, which every request carries: pointed to, not sent again.
758
+ if (ran.hasOwnProperty(key)) { results[c.id] = { id: c.id, ok: true, repeat: true, value: { note: 'Already done in this request: its result is in an earlier step.' } }; return; }
759
+ if (writes >= limits.writes) return fail('Over this turn\'s limit of ' + limits.writes + ' changes.');
760
+ var call = { turn: id, key: key, step: step, id: c.id };
761
+ return Promise.resolve().then(function () { return tool.check ? tool.check(c.args, call) : null; }).then(function (problem) {
762
+ if (problem) return fail(String(problem));
763
+ // Cancel and the time are looked at again right before the change, never in the middle of it.
764
+ if (late()) return;
765
+ tell({ kind: 'write', name: c.name, args: c.args });
766
+ if (late()) return;
767
+ writes++;
768
+ var running;
769
+ try { running = Promise.resolve(tool.run(c.args, call)); } catch (e) { running = Promise.reject(e); }
770
+ return running.then(function (value) {
771
+ // The change is done: it's recorded now, whatever happens to its pictures after.
772
+ ran[key] = { note: 'Done.' };
773
+ var entry = { name: c.name, args: c.args, result: value };
774
+ if (value && typeof value === 'object' && value.partly) entry.partly = String(value.partly);
775
+ out.done.push(entry);
776
+ tell({ kind: 'write', name: c.name, args: c.args, result: value });
777
+ return pictured(value).then(function (p) {
778
+ var kept = sized(p.value);
679
779
  ran[key] = kept;
680
- var entry = { name: c.name, args: c.args, result: value };
681
- if (value && typeof value === 'object' && value.partly) entry.partly = String(value.partly);
682
- out.done.push(entry);
683
780
  results[c.id] = { id: c.id, ok: true, value: kept };
684
- tell({ kind: 'write', name: c.name, args: c.args, result: value });
685
- return next(i + 1);
686
- }, function (e) { return fail(refused(e)); });
687
- }, function (e) { return fail(refused(e)); });
688
- }
689
- return next(0);
690
- });
781
+ if (p.pictures) results[c.id].pictures = p.pictures;
782
+ });
783
+ }, function (e) { fail(refused(e)); });
784
+ }, function (e) { fail(refused(e)); });
785
+ }
786
+ return {
787
+ add: function (c) {
788
+ if (typeof c.problem === 'string' && tools[c.name] && tools[c.name].reads) { results[c.id] = { id: c.id, ok: false, error: c.problem }; return; }
789
+ if (tools[c.name] && tools[c.name].reads) {
790
+ var going = chain.then(function () { return read(c); });
791
+ reading.push(going);
792
+ } else {
793
+ var before = [chain].concat(reading);
794
+ reading = [];
795
+ chain = Promise.all(before).then(function () { return write(c); });
796
+ }
797
+ },
798
+ finish: function () { return Promise.all([chain].concat(reading)).then(function () { return results; }); }
799
+ };
691
800
  }
692
801
 
693
802
  function step() {
@@ -696,23 +805,35 @@
696
805
  if (left <= 500) return stop('time');
697
806
  // The step's own time: what's left of an app's turn, or the step's guard against a call that has stopped.
698
807
  var stepTime = Math.min(left, ACT_STEP_SECONDS * 1000);
699
- var body = { request: String(request), about: o.about || '', now: present(), tools: declared, steps: steps,
808
+ var body = { request: String(request), about: o.about || '', now: present(), tools: declared, steps: sentSteps(),
700
809
  history: (o.history || []).slice(-3), seconds: Math.floor(stepTime / 1000) };
701
810
  if (o.context !== undefined) body.context = jsonState(o.context);
702
811
  if (route) body.route = route;
703
812
  if (steps.length >= limits.steps - 1) body.final = true;
704
813
  if (o.model) body.model = o.model;
705
- post('/ai/act', body, stepTime, Date.now() + stepTime).then(function (answer) {
814
+ // Streamed: each call comes as soon as the AI has written it, and runs then; the page follows the step for the
815
+ // rest. A Lamp from before streaming answers the whole step at once, which runs the same way.
816
+ body.stream = true;
817
+ var n = steps.length + 1, calls = [], ids = {}, runner = stepRunner(n), until = Date.now() + stepTime;
818
+ function heard(answer) {
819
+ // What the step cost through a key, paid for whether or not its answer still runs.
820
+ if (typeof answer.cost === 'number' && answer.cost > 0) out.cost += answer.cost;
706
821
  // Cancelled, out of time or already over: an answer that comes late runs nothing.
707
822
  if (late()) return;
708
823
  route = answer.route || route;
709
- if (answer.calls && answer.calls.length) {
710
- var calls = answer.calls, n = steps.length + 1, ids = {};
711
- for (var i = 0; i < calls.length; i++) {
712
- if (ids[calls[i].id]) return stop('error', 'The AI gave two calls the same id.');
713
- ids[calls[i].id] = true;
714
- }
715
- return runCalls(calls, n).then(function (results) {
824
+ var given = answer.calls || [];
825
+ for (var i = 0; i < given.length; i++) {
826
+ if (ids[given[i].id]) return stop('error', 'The AI gave two calls the same id.');
827
+ ids[given[i].id] = true;
828
+ calls.push(given[i]);
829
+ runner.add(given[i]);
830
+ }
831
+ if (answer.more === true && typeof answer.stream === 'string') {
832
+ var have = calls.length;
833
+ return post('/ai/act', { follow: answer.stream, have: have }, Math.max(1000, until - Date.now()), until).then(heard, failed);
834
+ }
835
+ if (calls.length) {
836
+ return runner.finish().then(function (results) {
716
837
  if (late()) return;
717
838
  steps.push({ calls: calls, results: calls.map(function (c) { return results[c.id] || { id: c.id, ok: false, skipped: 'Not run.' }; }) });
718
839
  if (steps.length >= limits.steps) return stop('steps');
@@ -727,10 +848,13 @@
727
848
  return typeof c === 'string' && c.trim() && c.length <= 24 && !/[<>*_`#\[\]{}|\\\n\r\t]/.test(c);
728
849
  }) ? offered.map(function (c) { return c.trim(); }) : [];
729
850
  stop(null);
730
- }, function (e) {
851
+ }
852
+ function failed(e) {
853
+ if (e && typeof e.cost === 'number' && e.cost > 0) out.cost += e.cost;
731
854
  var message = refused(e);
732
855
  stop(/reach Lamp|Lamp isn't|wasn't put here|put it on this device/.test(message) ? 'unreachable' : 'error', message);
733
- });
856
+ }
857
+ post('/ai/act', body, stepTime, until).then(heard, failed);
734
858
  }
735
859
  step();
736
860
  return { id: id, result: result, cancel: function () { stop('cancelled'); } };
@@ -928,8 +1052,12 @@
928
1052
 
929
1053
  /** options: data (text from outside the app, for the AI to work on, not obey), images (pictures to look at),
930
1054
  * system, json (true asks for JSON and gives back the parsed value: describe its exact shape in the prompt),
931
- * model (an id `LampAI.models()` lists, such as "engine:claude:opus" or "openrouter:sonnet"; a bare "opus" is
932
- * Opus through whatever answers by default). */
1055
+ * schema (a JSON Schema the answer must fit, which holds every AI to it, for large JSON: implies json; each object
1056
+ * lists every property in `required` and has `additionalProperties: false`), model (an id `LampAI.models()` lists,
1057
+ * such as "engine:claude:opus" or "openrouter:sonnet"; a bare "opus" is Opus through whatever answers by
1058
+ * default), signal (an AbortSignal: aborting it fails the ask at once with `error.stopped`), details (true
1059
+ * resolves { answer, seconds, cost }: `cost` in US dollars, 0 when the person's own Claude Code or Codex
1060
+ * answered). */
933
1061
  ask: function (prompt, options) { return call(prompt, options); },
934
1062
 
935
1063
  /**
@@ -957,24 +1085,34 @@
957
1085
  * model: picker.value, // optional: an id from LampAI.models()
958
1086
  * onStep: function (s) { … } // { kind, name, args[, result] }
959
1087
  * });
960
- * turn.result.then(function (r) { r.reply; r.ask; r.choices; r.done; r.failed; r.skipped; r.stopped; });
1088
+ * turn.result.then(function (r) { r.reply; r.ask; r.choices; r.done; r.failed; r.skipped; r.stopped; r.seconds; r.cost; });
961
1089
  * turn.cancel();
962
1090
  *
963
1091
  * An argument's shape is { type: 'string' | 'integer' | 'number' | 'boolean' | 'date' | 'datetime' | 'list',
964
- * oneOf: [strings], min, max, optional, about, of (a list's item) }. A tool with `reads: true` changes nothing;
965
- * reads in one step run together. Writes run one after another, each with its `check` right before it: a write
966
- * that fails stops the writes after it in that step (`skipped`). Cancel and the time stop the turn between
967
- * calls, never in the middle of a write; an answer that comes after runs nothing.
1092
+ * oneOf: [strings], min, max, optional, about, of (a list's item) }. A tool with `reads: true` changes nothing.
1093
+ * Each call runs as soon as the AI has written it, before the rest of its answer has come, so a build shows as
1094
+ * it's made; calls run in the order the AI wrote them. Reads side by side run together; a write waits for the
1095
+ * calls before it, with its `check` right before it, and a read after it sees it. A write that fails stops the
1096
+ * writes after it in that step (`skipped`). Cancel and the time stop the turn between calls, never in the middle
1097
+ * of a write; an answer that comes after runs nothing.
968
1098
  *
969
1099
  * Say what happened from `done`, `failed` and `skipped`, never from `reply`: they're what the page ran. A question
970
1100
  * back (`ask`) may come with `choices`, two to four short answers to offer as buttons; with none, any answer goes.
971
1101
  * `call.key` is the same for the same write in the same turn: make the ids of what a write creates from it, or
972
1102
  * keep it on the change, so a write asked again (or a turn tried again with `turn: r.turn`) finds it instead of
973
1103
  * adding a second. The same write in one turn while the page is open isn't run twice; nothing promises more.
974
- * Throw `LampAI.refuse('sentence')` from `check` or `run` to say why something can't be done.
1104
+ * A tool's result can show the AI what the app drew, in the middle of the turn: put `LampAI.picture(canvas)` in it
1105
+ * (`{ rendered: [LampAI.picture(front), LampAI.picture(side)] }`), and the AI sees each picture in its place, so it
1106
+ * can build, look and fix in one turn. The newest few are shown, smaller than drawn; older ones the AI has seen.
1107
+ * Throw `LampAI.refuse('sentence')` from `check` or `run` to say why something can't be done. `seconds` is how long
1108
+ * the turn took and `cost` what it cost in US dollars (0 when the person's own Claude Code or Codex answered).
975
1109
  */
976
1110
  act: function (request, options) { return act(request, options); },
977
1111
 
1112
+ /** A picture for the AI to look at, in what a tool of `act` returns: a <canvas>, an <img>, or a picture's address
1113
+ * (a data: URL or a file of the app's). */
1114
+ picture: function (source) { return new ActPicture(source); },
1115
+
978
1116
  /**
979
1117
  * What the person said, as text: `audio` is a WAV in base64 as `LampVoice.listen` gives it (16 kHz mono 16-bit,
980
1118
  * at most 15 seconds), transcribed by Lamp on the Mac with the AI set up there (OpenRouter's Scribe), which
@@ -1 +1 @@
1
- {"name":"Zqx App Name","instance":"81942121-46A7-4886-9515-F2C5D44CC566","exe":"ZqxAppName","platform":"mac"}
1
+ {"name":"Zqx App Name","exe":"ZqxAppName","platform":"mac","instance":"15F847FE-ACF1-4030-9204-0E3DC6630322"}
@@ -12297,23 +12297,25 @@
12297
12297
  func callInfo(_ call: [String: Any], key: String) -> LampActCall {
12298
12298
  LampActCall(key: key, id: call["id"] as! String, request: op.request, deadline: deadline)
12299
12299
  }
12300
- for call in calls where tool(call)?.reads != false {
12301
- let id = call["id"] as! String
12302
- if let stop = over() { out.stopped = stop; return results }
12303
- guard let tool = tool(call) else { results[id] = ["id": id, "ok": false, "error": "There's no tool called \(call["name"] as? String ?? "")."]; continue }
12304
- if let bad = tool.problem(args(call)) { results[id] = ["id": id, "ok": false, "error": bad]; continue }
12305
- onStep(.reading(tool.name))
12306
- do {
12307
- let value = try await tool.perform(args(call), callInfo(call, key: ""))
12308
- let kept = Self.cut(value, max: max(1024, min(resultBytes, op.resultsLeft)))
12309
- op.resultsLeft = max(0, op.resultsLeft - ((try? JSONSerialization.data(withJSONObject: [kept]))?.count ?? 0))
12310
- results[id] = ["id": id, "ok": true, "value": kept]
12311
- } catch {
12312
- results[id] = ["id": id, "ok": false, "error": Self.words(error)]
12313
- }
12314
- }
12300
+ // In the order the AI wrote them, so a read after a change sees it (the rule it's given, cli/app-ai.mjs).
12315
12301
  var stopped: String?
12316
- for call in calls where tool(call)?.reads == false {
12302
+ for call in calls {
12303
+ if tool(call)?.reads != false {
12304
+ let id = call["id"] as! String
12305
+ if let stop = over() { out.stopped = stop; return results }
12306
+ guard let tool = tool(call) else { results[id] = ["id": id, "ok": false, "error": "There's no tool called \(call["name"] as? String ?? "")."]; continue }
12307
+ if let bad = tool.problem(args(call)) { results[id] = ["id": id, "ok": false, "error": bad]; continue }
12308
+ onStep(.reading(tool.name))
12309
+ do {
12310
+ let value = try await tool.perform(args(call), callInfo(call, key: ""))
12311
+ let kept = Self.cut(value, max: max(1024, min(resultBytes, op.resultsLeft)))
12312
+ op.resultsLeft = max(0, op.resultsLeft - ((try? JSONSerialization.data(withJSONObject: [kept]))?.count ?? 0))
12313
+ results[id] = ["id": id, "ok": true, "value": kept]
12314
+ } catch {
12315
+ results[id] = ["id": id, "ok": false, "error": Self.words(error)]
12316
+ }
12317
+ continue
12318
+ }
12317
12319
  let id = call["id"] as! String, tool = tool(call)!, given = args(call)
12318
12320
  func miss(_ reason: String, skipped: Bool = false, partly: Bool = false) {
12319
12321
  let entry = LampActMiss(tool: tool.name, reason: reason, partly: partly)