@lampdotdev/lamp 0.36.2-nightly.25 → 0.36.2-nightly.26

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.
@@ -75,7 +75,7 @@ needs Lamp (for AI, `LampAI.status`'s `reason`), never an error or an empty scre
75
75
 
76
76
  `LampAI.act(request, { about, context, tools, history, onStep })` runs a short loop: the AI picks calls among the
77
77
  app's tools, the page runs them, the AI sees the results, until it replies (at most 4 AI calls and 6 writes a
78
- turn, and no time limit unless the app sets one with `limits: { seconds }`, for a feature that should answer quickly). It works with
78
+ turn, and no time limit unless the app sets one with `limits: { seconds }`, for a feature that should answer quickly; each read gives the AI up to 32 KB of its result, and an app whose state is bigger raises it with `limits: { result }`, up to 256 KB, rather than having the AI page through it). It works with
79
79
  whichever AI the person set up for apps' text, so don't tie it to voice: typing works wherever `act` does. Its comment in lamp.js has the shapes. This is the one recipe for it:
80
80
 
81
81
  - Show the feature where `LampAI.status('act')` says it's available, and keep the app's own buttons for everything:
package/cli/app-ai.mjs CHANGED
@@ -14,7 +14,7 @@
14
14
  // (OpenRouter: `chatBody`, `readMessage`), or one prompt whose answer must fit the step's JSON shape (Claude Code, Codex:
15
15
  // `planPrompt`, `planSchema`, `readPlan`).
16
16
 
17
- export const MAX_TOOLS = 24, MAX_CALLS = 8, MAX_STEPS = 4, MAX_BODY = 256 * 1024, MAX_CHOICES = 4, MAX_CHOICE = 24;
17
+ export const MAX_TOOLS = 24, MAX_CALLS = 8, MAX_STEPS = 4, MAX_BODY = 2 * 1024 * 1024, MAX_CHOICES = 4, MAX_CHOICE = 24;
18
18
  /** The longest a step may take, whatever its route, before its call is taken to have stopped answering (`ACT_STEP_SECONDS`
19
19
  * in lamp.js, `AppAct.turnSeconds`). A turn has no time of its own unless the app gives it one. */
20
20
  export const MAX_TURN_SECONDS = 600;
@@ -66,7 +66,7 @@ function parseArg(raw, path) {
66
66
  * steps, route, seconds, final}`, each tool's `args` as [name, shape] pairs by name. Throws `Refused`.
67
67
  */
68
68
  export function readStep(text) {
69
- if (Buffer.byteLength(text) > MAX_BODY) throw new Refused(413, "A step can carry at most 256 KB.");
69
+ if (Buffer.byteLength(text) > MAX_BODY) throw new Refused(413, "A step can carry at most 2 MB.");
70
70
  let body;
71
71
  try { body = JSON.parse(text); } catch {}
72
72
  if (!body || typeof body !== "object" || Array.isArray(body)) throw new Refused(400, "A step needs a JSON object.");
@@ -521,16 +521,33 @@
521
521
  // given ACT_STEP_SECONDS, which only a call that has stopped answering reaches. An app that wants a quicker turn
522
522
  // asks for one (`limits: { seconds }`); steps and writes it may only lower.
523
523
  var ACT_LIMITS = { steps: 4, writes: 6 };
524
+ // How much of one read goes back to the model: room for a real app's state (a 184-piece model, a week's calendar)
525
+ // in one step, since paging through it costs steps a turn needs for its work. An app with more raises it
526
+ // (`limits: { result }`), up to ACT_RESULT_MAX.
527
+ var ACT_RESULT = 32768, ACT_RESULT_MAX = 262144;
528
+ // What a turn's reads give back in all, which every later step carries again: kept under what a step may carry
529
+ // (`MAX_BODY` in lamp's app-ai.mjs), with room for the request, the tools and the steps' own words.
530
+ var ACT_TURN_RESULTS = 1200000;
524
531
  var ACT_STEP_SECONDS = 600;
525
532
  var actRan = {}; // turn id -> call key -> the result a write gave, while this copy of the page is open
526
533
  function actId() { return 't' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }
527
534
  function isList(v) { return Object.prototype.toString.call(v) === '[object Array]'; }
528
- /** At most 4 KB of a result goes back to the model, cut with a note. */
529
- function actValue(value) {
535
+ /** A text's size in UTF-8 bytes, as a step is measured where it's checked. */
536
+ function utf8Bytes(text) { try { return unescape(encodeURIComponent(text)).length; } catch (e) { return text.length * 3; } }
537
+ /** At most `max` bytes of a result go back to the model, as it's sent (UTF-8, its cut wrapper included), cut with a note. */
538
+ function actValue(value, max) {
530
539
  var json = jsonState(value === undefined ? null : value), text;
531
540
  try { text = JSON.stringify(json); } catch (e) { return { note: 'The result couldn\'t be read.' }; }
532
- if (text && text.length > 4096) return { cut: text.slice(0, 4000), note: 'Cut short: ask for less.' };
533
- return json;
541
+ if (!text || utf8Bytes(text) <= max) return json;
542
+ // The cut is JSON inside JSON, so its quotes are escaped again: shrink it until the whole wrapper fits.
543
+ var cut = text.slice(0, max);
544
+ for (var i = 0; i < 24; i++) {
545
+ var wrapped = { cut: cut, note: 'Cut short: ask for less.' };
546
+ var size = utf8Bytes(JSON.stringify(wrapped));
547
+ if (size <= max) return wrapped;
548
+ cut = cut.slice(0, Math.max(0, Math.floor(cut.length * max / size) - 16));
549
+ }
550
+ return { note: 'Cut short: ask for less.' };
534
551
  }
535
552
  /** The page's own check of a call before it runs: a tool it declared, its arguments an object with nothing
536
553
  * undeclared and nothing required missing. Lamp has checked the shapes; this is what the page itself can hold to. */
@@ -552,6 +569,15 @@
552
569
  limits[k] = Math.max(1, Math.min(asked, ACT_LIMITS[k]));
553
570
  });
554
571
  var asked = o.limits && typeof o.limits.seconds === 'number' && o.limits.seconds > 0 ? o.limits.seconds : null;
572
+ var resultMax = o.limits && typeof o.limits.result === 'number' && o.limits.result > 0
573
+ ? Math.min(Math.max(1024, Math.floor(o.limits.result)), ACT_RESULT_MAX) : ACT_RESULT;
574
+ // Each result takes from the turn's allowance, so later reads get less rather than the step growing past it.
575
+ var resultsLeft = ACT_TURN_RESULTS;
576
+ function sized(value) {
577
+ var kept = actValue(value, Math.max(1024, Math.min(resultMax, resultsLeft)));
578
+ try { resultsLeft = Math.max(0, resultsLeft - utf8Bytes(JSON.stringify(kept))); } catch (e) {}
579
+ return kept;
580
+ }
555
581
  var id = o.turn || actId();
556
582
  if (!actRan[id]) {
557
583
  var kept = Object.keys(actRan);
@@ -601,7 +627,7 @@
601
627
  }).then(function (value) {
602
628
  if (value === NOT_RUN) return;
603
629
  out.reads.push({ name: c.name, args: c.args });
604
- results[c.id] = { id: c.id, ok: true, value: actValue(value) };
630
+ results[c.id] = { id: c.id, ok: true, value: sized(value) };
605
631
  }, function (e) { results[c.id] = { id: c.id, ok: false, error: refused(e) }; });
606
632
  })).then(function () {
607
633
  var stopped = null;
@@ -638,7 +664,7 @@
638
664
  var running;
639
665
  try { running = Promise.resolve(tool.run(c.args, call)); } catch (e) { running = Promise.reject(e); }
640
666
  return running.then(function (value) {
641
- var kept = actValue(value);
667
+ var kept = sized(value);
642
668
  ran[key] = kept;
643
669
  var entry = { name: c.name, args: c.args, result: value };
644
670
  if (value && typeof value === 'object' && value.partly) entry.partly = String(value.partly);
package/cli/lamps/lamp.js CHANGED
@@ -521,16 +521,33 @@
521
521
  // given ACT_STEP_SECONDS, which only a call that has stopped answering reaches. An app that wants a quicker turn
522
522
  // asks for one (`limits: { seconds }`); steps and writes it may only lower.
523
523
  var ACT_LIMITS = { steps: 4, writes: 6 };
524
+ // How much of one read goes back to the model: room for a real app's state (a 184-piece model, a week's calendar)
525
+ // in one step, since paging through it costs steps a turn needs for its work. An app with more raises it
526
+ // (`limits: { result }`), up to ACT_RESULT_MAX.
527
+ var ACT_RESULT = 32768, ACT_RESULT_MAX = 262144;
528
+ // What a turn's reads give back in all, which every later step carries again: kept under what a step may carry
529
+ // (`MAX_BODY` in lamp's app-ai.mjs), with room for the request, the tools and the steps' own words.
530
+ var ACT_TURN_RESULTS = 1200000;
524
531
  var ACT_STEP_SECONDS = 600;
525
532
  var actRan = {}; // turn id -> call key -> the result a write gave, while this copy of the page is open
526
533
  function actId() { return 't' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }
527
534
  function isList(v) { return Object.prototype.toString.call(v) === '[object Array]'; }
528
- /** At most 4 KB of a result goes back to the model, cut with a note. */
529
- function actValue(value) {
535
+ /** A text's size in UTF-8 bytes, as a step is measured where it's checked. */
536
+ function utf8Bytes(text) { try { return unescape(encodeURIComponent(text)).length; } catch (e) { return text.length * 3; } }
537
+ /** At most `max` bytes of a result go back to the model, as it's sent (UTF-8, its cut wrapper included), cut with a note. */
538
+ function actValue(value, max) {
530
539
  var json = jsonState(value === undefined ? null : value), text;
531
540
  try { text = JSON.stringify(json); } catch (e) { return { note: 'The result couldn\'t be read.' }; }
532
- if (text && text.length > 4096) return { cut: text.slice(0, 4000), note: 'Cut short: ask for less.' };
533
- return json;
541
+ if (!text || utf8Bytes(text) <= max) return json;
542
+ // The cut is JSON inside JSON, so its quotes are escaped again: shrink it until the whole wrapper fits.
543
+ var cut = text.slice(0, max);
544
+ for (var i = 0; i < 24; i++) {
545
+ var wrapped = { cut: cut, note: 'Cut short: ask for less.' };
546
+ var size = utf8Bytes(JSON.stringify(wrapped));
547
+ if (size <= max) return wrapped;
548
+ cut = cut.slice(0, Math.max(0, Math.floor(cut.length * max / size) - 16));
549
+ }
550
+ return { note: 'Cut short: ask for less.' };
534
551
  }
535
552
  /** The page's own check of a call before it runs: a tool it declared, its arguments an object with nothing
536
553
  * undeclared and nothing required missing. Lamp has checked the shapes; this is what the page itself can hold to. */
@@ -552,6 +569,15 @@
552
569
  limits[k] = Math.max(1, Math.min(asked, ACT_LIMITS[k]));
553
570
  });
554
571
  var asked = o.limits && typeof o.limits.seconds === 'number' && o.limits.seconds > 0 ? o.limits.seconds : null;
572
+ var resultMax = o.limits && typeof o.limits.result === 'number' && o.limits.result > 0
573
+ ? Math.min(Math.max(1024, Math.floor(o.limits.result)), ACT_RESULT_MAX) : ACT_RESULT;
574
+ // Each result takes from the turn's allowance, so later reads get less rather than the step growing past it.
575
+ var resultsLeft = ACT_TURN_RESULTS;
576
+ function sized(value) {
577
+ var kept = actValue(value, Math.max(1024, Math.min(resultMax, resultsLeft)));
578
+ try { resultsLeft = Math.max(0, resultsLeft - utf8Bytes(JSON.stringify(kept))); } catch (e) {}
579
+ return kept;
580
+ }
555
581
  var id = o.turn || actId();
556
582
  if (!actRan[id]) {
557
583
  var kept = Object.keys(actRan);
@@ -601,7 +627,7 @@
601
627
  }).then(function (value) {
602
628
  if (value === NOT_RUN) return;
603
629
  out.reads.push({ name: c.name, args: c.args });
604
- results[c.id] = { id: c.id, ok: true, value: actValue(value) };
630
+ results[c.id] = { id: c.id, ok: true, value: sized(value) };
605
631
  }, function (e) { results[c.id] = { id: c.id, ok: false, error: refused(e) }; });
606
632
  })).then(function () {
607
633
  var stopped = null;
@@ -638,7 +664,7 @@
638
664
  var running;
639
665
  try { running = Promise.resolve(tool.run(c.args, call)); } catch (e) { running = Promise.reject(e); }
640
666
  return running.then(function (value) {
641
- var kept = actValue(value);
667
+ var kept = sized(value);
642
668
  ran[key] = kept;
643
669
  var entry = { name: c.name, args: c.args, result: value };
644
670
  if (value && typeof value === 'object' && value.partly) entry.partly = String(value.partly);
@@ -1 +1 @@
1
- {"instance":"99F1CF1A-EC7E-4440-8F31-0B2D19F376A5","platform":"mac","name":"Zqx App Name","exe":"ZqxAppName"}
1
+ {"exe":"ZqxAppName","name":"Zqx App Name","platform":"mac","instance":"10DC6CFC-3873-414E-B724-6AA904DC37A0"}
@@ -12017,6 +12017,9 @@
12017
12017
  let request: String
12018
12018
  var answers: [(question: String, answer: String)] = []
12019
12019
  var writes = 0
12020
+ /// What its reads may still give back in all: every later step carries them again, under what a step may
12021
+ /// carry (`MAX_BODY` in lamp's app-ai.mjs).
12022
+ var resultsLeft = 1_200_000
12020
12023
  }
12021
12024
  private var operation: Operation?
12022
12025
  /// Each change's result by its key, so the same change asked again gets it without running again.
@@ -12028,14 +12031,19 @@
12028
12031
  private let send: ([String: Any], TimeInterval) async throws -> [String: Any]
12029
12032
  /// The longest a request may take, when the app wants a quick answer; nil for no limit.
12030
12033
  let seconds: Double?
12034
+ /// How much of one read goes back to the AI, in bytes: room for an app's real state in one step, since paging
12035
+ /// through it costs steps a request needs for its work.
12036
+ let resultBytes: Int
12031
12037
 
12032
12038
  /// `seconds` limits each request's time, for a feature that should answer quickly; leave it out for no limit.
12039
+ /// `resultBytes` raises how much of a read the AI sees (32 KB unless given, 256 KB at most).
12033
12040
  /// `send` stands in for Lamp in a test; leave it out.
12034
- init(about: String = "", tools: [LampActTool], seconds: Double? = nil,
12041
+ init(about: String = "", tools: [LampActTool], seconds: Double? = nil, resultBytes: Int? = nil,
12035
12042
  send: (([String: Any], TimeInterval) async throws -> [String: Any])? = nil) {
12036
12043
  self.about = about
12037
12044
  self.tools = tools
12038
12045
  self.seconds = seconds.map { max(1, $0) }
12046
+ self.resultBytes = min(max(resultBytes ?? 32_768, 1024), 262_144)
12039
12047
  self.send = send ?? { body, timeout in try await LampAI.post("/ai/act", body, timeout: timeout) }
12040
12048
  }
12041
12049
 
@@ -12192,7 +12200,9 @@
12192
12200
  onStep(.reading(tool.name))
12193
12201
  do {
12194
12202
  let value = try await tool.perform(args(call), callInfo(call, key: ""))
12195
- results[id] = ["id": id, "ok": true, "value": Self.cut(value)]
12203
+ let kept = Self.cut(value, max: max(1024, min(resultBytes, op.resultsLeft)))
12204
+ op.resultsLeft = max(0, op.resultsLeft - ((try? JSONSerialization.data(withJSONObject: [kept]))?.count ?? 0))
12205
+ results[id] = ["id": id, "ok": true, "value": kept]
12196
12206
  } catch {
12197
12207
  results[id] = ["id": id, "ok": false, "error": Self.words(error)]
12198
12208
  }
@@ -12251,13 +12261,21 @@
12251
12261
 
12252
12262
  private static func words(_ error: Error) -> String { (error as? LampAI.Failure)?.message ?? error.localizedDescription }
12253
12263
 
12254
- /// At most 4 KB of a read goes back to the AI, cut with a note.
12255
- private static func cut(_ value: Any) -> Any {
12264
+ /// At most `max` bytes of a read go back to the AI, cut with a note.
12265
+ private static func cut(_ value: Any, max: Int) -> Any {
12256
12266
  guard JSONSerialization.isValidJSONObject([value]), let data = try? JSONSerialization.data(withJSONObject: [value]) else {
12257
12267
  return ["note": "The result couldn't be read."]
12258
12268
  }
12259
- guard data.count > 4096 else { return value }
12260
- return ["cut": String(String(decoding: data, as: UTF8.self).prefix(4000)), "note": "Cut short: ask for less."]
12269
+ guard data.count > max else { return value }
12270
+ // The cut is JSON inside JSON, so its quotes are escaped again: shrink it until the whole wrapper fits.
12271
+ var cut = String(decoding: data.prefix(max), as: UTF8.self)
12272
+ for _ in 0..<24 {
12273
+ let wrapped: [String: Any] = ["cut": cut, "note": "Cut short: ask for less."]
12274
+ let size = (try? JSONSerialization.data(withJSONObject: [wrapped]))?.count ?? Int.max
12275
+ if size <= max { return wrapped }
12276
+ cut = String(cut.prefix(Swift.max(0, cut.count * max / Swift.max(size, 1) - 16)))
12277
+ }
12278
+ return ["note": "Cut short: ask for less."]
12261
12279
  }
12262
12280
 
12263
12281
  private static func canonical(_ value: Any) -> String {
@@ -521,16 +521,33 @@
521
521
  // given ACT_STEP_SECONDS, which only a call that has stopped answering reaches. An app that wants a quicker turn
522
522
  // asks for one (`limits: { seconds }`); steps and writes it may only lower.
523
523
  var ACT_LIMITS = { steps: 4, writes: 6 };
524
+ // How much of one read goes back to the model: room for a real app's state (a 184-piece model, a week's calendar)
525
+ // in one step, since paging through it costs steps a turn needs for its work. An app with more raises it
526
+ // (`limits: { result }`), up to ACT_RESULT_MAX.
527
+ var ACT_RESULT = 32768, ACT_RESULT_MAX = 262144;
528
+ // What a turn's reads give back in all, which every later step carries again: kept under what a step may carry
529
+ // (`MAX_BODY` in lamp's app-ai.mjs), with room for the request, the tools and the steps' own words.
530
+ var ACT_TURN_RESULTS = 1200000;
524
531
  var ACT_STEP_SECONDS = 600;
525
532
  var actRan = {}; // turn id -> call key -> the result a write gave, while this copy of the page is open
526
533
  function actId() { return 't' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }
527
534
  function isList(v) { return Object.prototype.toString.call(v) === '[object Array]'; }
528
- /** At most 4 KB of a result goes back to the model, cut with a note. */
529
- function actValue(value) {
535
+ /** A text's size in UTF-8 bytes, as a step is measured where it's checked. */
536
+ function utf8Bytes(text) { try { return unescape(encodeURIComponent(text)).length; } catch (e) { return text.length * 3; } }
537
+ /** At most `max` bytes of a result go back to the model, as it's sent (UTF-8, its cut wrapper included), cut with a note. */
538
+ function actValue(value, max) {
530
539
  var json = jsonState(value === undefined ? null : value), text;
531
540
  try { text = JSON.stringify(json); } catch (e) { return { note: 'The result couldn\'t be read.' }; }
532
- if (text && text.length > 4096) return { cut: text.slice(0, 4000), note: 'Cut short: ask for less.' };
533
- return json;
541
+ if (!text || utf8Bytes(text) <= max) return json;
542
+ // The cut is JSON inside JSON, so its quotes are escaped again: shrink it until the whole wrapper fits.
543
+ var cut = text.slice(0, max);
544
+ for (var i = 0; i < 24; i++) {
545
+ var wrapped = { cut: cut, note: 'Cut short: ask for less.' };
546
+ var size = utf8Bytes(JSON.stringify(wrapped));
547
+ if (size <= max) return wrapped;
548
+ cut = cut.slice(0, Math.max(0, Math.floor(cut.length * max / size) - 16));
549
+ }
550
+ return { note: 'Cut short: ask for less.' };
534
551
  }
535
552
  /** The page's own check of a call before it runs: a tool it declared, its arguments an object with nothing
536
553
  * undeclared and nothing required missing. Lamp has checked the shapes; this is what the page itself can hold to. */
@@ -552,6 +569,15 @@
552
569
  limits[k] = Math.max(1, Math.min(asked, ACT_LIMITS[k]));
553
570
  });
554
571
  var asked = o.limits && typeof o.limits.seconds === 'number' && o.limits.seconds > 0 ? o.limits.seconds : null;
572
+ var resultMax = o.limits && typeof o.limits.result === 'number' && o.limits.result > 0
573
+ ? Math.min(Math.max(1024, Math.floor(o.limits.result)), ACT_RESULT_MAX) : ACT_RESULT;
574
+ // Each result takes from the turn's allowance, so later reads get less rather than the step growing past it.
575
+ var resultsLeft = ACT_TURN_RESULTS;
576
+ function sized(value) {
577
+ var kept = actValue(value, Math.max(1024, Math.min(resultMax, resultsLeft)));
578
+ try { resultsLeft = Math.max(0, resultsLeft - utf8Bytes(JSON.stringify(kept))); } catch (e) {}
579
+ return kept;
580
+ }
555
581
  var id = o.turn || actId();
556
582
  if (!actRan[id]) {
557
583
  var kept = Object.keys(actRan);
@@ -601,7 +627,7 @@
601
627
  }).then(function (value) {
602
628
  if (value === NOT_RUN) return;
603
629
  out.reads.push({ name: c.name, args: c.args });
604
- results[c.id] = { id: c.id, ok: true, value: actValue(value) };
630
+ results[c.id] = { id: c.id, ok: true, value: sized(value) };
605
631
  }, function (e) { results[c.id] = { id: c.id, ok: false, error: refused(e) }; });
606
632
  })).then(function () {
607
633
  var stopped = null;
@@ -638,7 +664,7 @@
638
664
  var running;
639
665
  try { running = Promise.resolve(tool.run(c.args, call)); } catch (e) { running = Promise.reject(e); }
640
666
  return running.then(function (value) {
641
- var kept = actValue(value);
667
+ var kept = sized(value);
642
668
  ran[key] = kept;
643
669
  var entry = { name: c.name, args: c.args, result: value };
644
670
  if (value && typeof value === 'object' && value.partly) entry.partly = String(value.partly);
package/cli/local-ai.mjs CHANGED
@@ -21,7 +21,7 @@ import { answer as answerAI, engine } from "./ai.mjs";
21
21
  /** The port lamp.js on a shared page asks (`LOCAL_AI` there): one, fixed, since the page can't be told another. */
22
22
  export const LOCAL_AI_PORT = 47423;
23
23
  const PLAY_HOST = "play.lamp.dev";
24
- const MOST = 2_000_000, AT_ONCE = 3, ASKING_FOR = 120_000;
24
+ const MOST = 4_000_000, AT_ONCE = 3, ASKING_FOR = 120_000;
25
25
  const ROUTES = new Set(["/ai/status", "/ai", "/ai/act", "/ai/decide", "/ai/hear", "/ai/image"]);
26
26
 
27
27
  const keptFile = () => join(process.env.LAMP_DEV_HOME || join(homedir(), ".lamp", "dev"), "shared-pages-ai.json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lampdotdev/lamp",
3
- "version": "0.36.2-nightly.25",
3
+ "version": "0.36.2-nightly.26",
4
4
  "description": "Runs a web app or game from its folder and puts each saved change into the open page.",
5
5
  "type": "module",
6
6
  "bin": {