@lampdotdev/lamp 0.36.2-nightly.43 → 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/ai.mjs CHANGED
@@ -9,8 +9,8 @@ import { accessSync, closeSync, constants, fstatSync, mkdtempSync, openSync, rea
9
9
  import { tmpdir } from "node:os";
10
10
  import { delimiter, join } from "node:path";
11
11
  import { randomBytes } from "node:crypto";
12
- import { chatBody, decidePrompt, planPrompt, planSchema, readDecision, readMessage, readPlan, readStep } from "./app-ai.mjs";
13
- import { DEFAULT_MODEL, IMAGE_MODELS, OpenRouterTrouble, chat as openRouterChat, image as openRouterImage, text as openRouterText } from "./openrouter.mjs";
12
+ import { afterStream, chatBody, decidePrompt, planPrompt, planSchema, readDecision, readMessage, readPlan, readStep, stepReader } from "./app-ai.mjs";
13
+ import { DEFAULT_MODEL, IMAGE_MODELS, OpenRouterTrouble, chat as openRouterChat, chatStream as openRouterChatStream, image as openRouterImage, text as openRouterText } from "./openrouter.mjs";
14
14
 
15
15
 
16
16
  /**
@@ -57,9 +57,10 @@ export class EngineTrouble extends Error {
57
57
  * request itself: to Claude Code in its input, to Codex as attachments, never through a tool that reads files.
58
58
  * Lamp's app runs it this way too (`engineAnswer` in calls.mjs, `AppAI.viaEngine`), naming the program (`tool`) and
59
59
  * what it needs set (`env`); on its own lamp finds the person's own on the PATH. `spent` hears what a call through
60
- * OpenRouter cost; one through the person's own sign-in costs nothing through Lamp.
60
+ * OpenRouter cost; one through the person's own sign-in costs nothing through Lamp. `onPartial` hears the answer to a
61
+ * `schema` as it's written, the JSON so far, where the engine says it as it goes (Claude Code; Codex says it at the end).
61
62
  */
62
- export function complete({ system, prompt, schema, seconds = 60, which = engine(), tool, env = {}, model = process.env.LAMP_AI_MODEL || "", images = [], signal, spent } = {}) {
63
+ export function complete({ system, prompt, schema, seconds = 60, which = engine(), tool, env = {}, model = process.env.LAMP_AI_MODEL || "", images = [], signal, spent, onPartial } = {}) {
63
64
  if (which === "openrouter") return viaOpenRouter({ system, prompt, schema, seconds, model, images, signal, spent });
64
65
  if (which !== "claude" && which !== "codex") return Promise.reject(new EngineTrouble("missing", missing));
65
66
  const folder = mkdtempSync(join(tmpdir(), "lamp-ai-"));
@@ -73,6 +74,7 @@ export function complete({ system, prompt, schema, seconds = 60, which = engine(
73
74
  "--strict-mcp-config", "--disable-slash-commands", "--tools", "", "--system-prompt", sayer];
74
75
  if (model) args.push("--model", model);
75
76
  if (schema) args.push("--json-schema", JSON.stringify(schema));
77
+ if (schema && onPartial) args.push("--include-partial-messages");
76
78
  if (pictures.length) {
77
79
  args.push("--input-format", "stream-json");
78
80
  const content = [{ type: "text", text: prompt }, ...pictures.map((image) => ({ type: "image",
@@ -105,7 +107,7 @@ export function complete({ system, prompt, schema, seconds = 60, which = engine(
105
107
  delete childEnv.BUN_BE_BUN;
106
108
  return new Promise((resolve, reject) => {
107
109
  const child = spawn(tool || onPath(which) || which, args, { cwd: folder, env: childEnv, detached: true, stdio: [input ? "pipe" : "ignore", "pipe", "pipe"] });
108
- let text = "", trouble = null, rest = "", said = "";
110
+ let text = "", trouble = null, rest = "", said = "", partial = "";
109
111
  // The whole group, since an engine may leave a child holding its output.
110
112
  const stop = (kind, words) => { if (!trouble) trouble = new EngineTrouble(kind, words); try { process.kill(-child.pid, "SIGKILL"); } catch { child.kill("SIGKILL"); } };
111
113
  const timer = setTimeout(() => stop("timeout", `${named(which)} took too long to answer.`), Math.max(1, seconds) * 1000);
@@ -119,6 +121,16 @@ export function complete({ system, prompt, schema, seconds = 60, which = engine(
119
121
  else if (codexFailure(event)) trouble ??= new EngineTrouble("failed", codexFailure(event));
120
122
  return;
121
123
  }
124
+ // The schema's answer as it's written: its JSON, a piece at a time.
125
+ if (event.type === "stream_event" && onPartial) {
126
+ const inner = event.event ?? {};
127
+ if (inner.type === "content_block_start") partial = "";
128
+ else if (inner.type === "content_block_delta" && inner.delta?.type === "input_json_delta" && typeof inner.delta.partial_json === "string") {
129
+ partial += inner.delta.partial_json;
130
+ try { onPartial(partial); } catch {}
131
+ }
132
+ return;
133
+ }
122
134
  if (event.type !== "result") return;
123
135
  if (event.is_error) trouble ??= new EngineTrouble("failed", String(event.result || "Claude Code couldn't answer."));
124
136
  else if (schema && event.structured_output !== undefined) text = JSON.stringify(event.structured_output);
@@ -366,8 +378,29 @@ export async function decide(body) {
366
378
  // MARK: Actions (`LampAI.act`)
367
379
 
368
380
  /** `/ai/act`: one step of the page's agent loop, told and checked as Lamp's app does it (app-ai.mjs). */
381
+ // A step being streamed (`stream` on lamp.js's act), by its id: its calls so far, and its answer once it's done. The page
382
+ // asks for what's new (`follow`) until it has the answer; a step nobody follows for a few minutes is let go.
383
+ const live = new Map();
384
+ const FOLLOW_WAIT = 50_000;
385
+
386
+ /** What a streamed step has that the page hasn't (`have` calls): waits for something new, or the end, a while at most. */
387
+ async function following(id, have) {
388
+ const step = live.get(id);
389
+ if (!step) return [404, { error: "This step is over." }];
390
+ step.seen = Date.now();
391
+ if (step.calls.length <= have && !step.done) {
392
+ await new Promise((woken) => { const timer = setTimeout(woken, FOLLOW_WAIT); step.waiting.add(() => { clearTimeout(timer); woken(); }); });
393
+ }
394
+ const calls = step.calls.slice(have);
395
+ if (!step.done) return [200, { stream: id, more: true, calls }];
396
+ live.delete(id);
397
+ const [status, json] = step.done;
398
+ return [status, { ...json, stream: id, more: false, calls: [...calls, ...(Array.isArray(json.calls) ? json.calls : [])] }];
399
+ }
400
+
369
401
  export async function act(body, here = engines()) {
370
402
  const first = here[0] ?? null;
403
+ if (typeof body?.follow === "string") return following(body.follow, Math.max(0, Math.floor(Number(body.have)) || 0));
371
404
  if (body?.probe === true) {
372
405
  // A key OpenRouter refuses can't answer a step, as `/ai/status` says too.
373
406
  const refused = first === "openrouter" ? await keyProblem() : null;
@@ -382,21 +415,44 @@ export async function act(body, here = engines()) {
382
415
  const seconds = 600;
383
416
  const deadline = Date.now() + Math.min(r.seconds, seconds) * 1000;
384
417
  const tag = "outside-text-" + randomBytes(4).toString("hex");
418
+ // Streamed: each call goes to the page as soon as it's whole, and the page follows the step for the rest.
419
+ if (body?.stream === true) {
420
+ const id = "st" + randomBytes(6).toString("hex");
421
+ const step = { calls: [], done: null, waiting: new Set(), seen: Date.now() };
422
+ const wake = () => { for (const woken of step.waiting) woken(); step.waiting.clear(); };
423
+ live.set(id, step);
424
+ for (const [other, kept] of live) if (Date.now() - kept.seen > 5 * 60_000) live.delete(other);
425
+ runStep(r, { which, model, tag, deadline, seconds, release: (call) => { step.calls.push(call); wake(); } })
426
+ .then((done) => { step.done = done; wake(); });
427
+ return following(id, 0);
428
+ }
429
+ return runStep(r, { which, model, tag, deadline, seconds });
430
+ }
431
+
432
+ /** A step asked of its route, asked again once when its answer can't be used: [status, json]. With `release`, calls go
433
+ * out as they're written, and once one has gone the answer isn't asked again. */
434
+ async function runStep(r, { which, model, tag, deadline, seconds, release }) {
385
435
  let retry = null, cost = 0;
386
436
  for (let attempt = 0; attempt < 2; attempt++) {
387
437
  const left = (deadline - Date.now()) / 1000;
388
438
  if (left < 1) return [504, { error: "This step ran out of time.", cost }];
389
439
  let read;
440
+ const reader = release ? stepReader(r, release) : null;
390
441
  try {
391
442
  if (which === "openrouter") {
392
443
  const body = chatBody(r, tag, retry, model);
393
- const answered = await openRouterChat({ key: process.env.OPENROUTER_API_KEY, body, seconds: left });
444
+ const answered = reader
445
+ ? await openRouterChatStream({ key: process.env.OPENROUTER_API_KEY, body, seconds: left, onCall: (name, args) => reader.call(name, args) })
446
+ : await openRouterChat({ key: process.env.OPENROUTER_API_KEY, body, seconds: left });
394
447
  cost += answered.cost;
395
448
  read = readMessage(answered.message, r);
396
449
  } else {
397
450
  const { system, prompt, images } = planPrompt(r, tag, retry);
398
- read = readPlan(await complete({ system, prompt, images, schema: planSchema(r), seconds: left, which, model }), r);
451
+ const text = await complete({ system, prompt, images, schema: planSchema(r), seconds: left, which, model, onPartial: reader ? (so) => reader.planned(so) : undefined });
452
+ reader?.planned(text);
453
+ read = readPlan(text, r);
399
454
  }
455
+ if (reader) read = afterStream(reader, read);
400
456
  } catch (error) {
401
457
  cost += Number(error?.cost) || 0;
402
458
  const words = error instanceof OpenRouterTrouble && error.kind === "refused" ? openRouterWords(error) : error?.reply?.error?.message || error.message;
package/cli/app-ai.mjs CHANGED
@@ -315,12 +315,102 @@ export function readPlan(text, r) {
315
315
  return readMessage({ tool_calls: [{ function: { name: REPLY, arguments: JSON.stringify(args) } }] }, r);
316
316
  }
317
317
 
318
+ // MARK: Calls as they're written
319
+
320
+ /**
321
+ * The calls finished so far in an answer in `planSchema`'s shape that's still being written: each element of its `calls`
322
+ * whose closing brace has come, parsed, in order. What's after them (an element half written, the reply) waits.
323
+ */
324
+ export function plannedCalls(text) {
325
+ const found = [];
326
+ const key = text.search(/"calls"\s*:\s*\[/);
327
+ if (key < 0) return found;
328
+ let i = text.indexOf("[", key) + 1, depth = 0, start = -1, inString = false, escaped = false;
329
+ for (; i < text.length; i++) {
330
+ const c = text[i];
331
+ if (inString) {
332
+ if (escaped) escaped = false;
333
+ else if (c === "\\") escaped = true;
334
+ else if (c === '"') inString = false;
335
+ continue;
336
+ }
337
+ if (c === '"') inString = true;
338
+ else if (c === "{" || c === "[") { if (depth === 0) start = i; depth++; }
339
+ else if (c === "}" || c === "]") {
340
+ if (depth === 0) break;
341
+ depth--;
342
+ if (depth === 0 && start >= 0) {
343
+ try { found.push(JSON.parse(text.slice(start, i + 1))); } catch { break; }
344
+ start = -1;
345
+ }
346
+ }
347
+ }
348
+ return found;
349
+ }
350
+
351
+ /**
352
+ * Reads a step's calls one at a time as the model writes them (`stream` on `LampAI.act`), with the checks `readMessage`
353
+ * gives a whole answer: `release` gets each call that fits as soon as it's whole, with the id it would have had. One that
354
+ * doesn't fit is held, with why (`problem`), until one that fits comes after it, which it goes before, in its place, as
355
+ * a call that failed: the page never runs it, and a change that failed stops the changes after it. One left held at the
356
+ * end goes last when calls were released (`afterStream`), or, when none was, the whole answer is read and asked again
357
+ * as before. Calls past `MAX_CALLS` are left out.
358
+ */
359
+ export function stepReader(r, release) {
360
+ const step = r.steps.length + 1;
361
+ let given = 0, seen = 0;
362
+ const reader = {
363
+ released: 0,
364
+ held: [],
365
+ /** One whole call, `args` its arguments as an object or as the JSON text the model wrote. */
366
+ call(name, raw) {
367
+ if (name === REPLY) return;
368
+ const id = `s${step}c${++given}`;
369
+ let args = raw;
370
+ if (typeof raw === "string") { try { args = JSON.parse(raw || "{}"); } catch { args = undefined; } }
371
+ const hold = (problem) => reader.held.push({ id, name: String(name), args: args && typeof args === "object" && !Array.isArray(args) ? args : {}, problem });
372
+ // Past the most a step holds, a call is left out: the AI sees what ran, and writes the rest next.
373
+ if (given > MAX_CALLS) return;
374
+ if (typeof name !== "string") return hold("A call had no name.");
375
+ if (!args || typeof args !== "object" || Array.isArray(args)) return hold(`${name}'s arguments weren't a JSON object.`);
376
+ if (r.final) return hold("Only `reply` is possible now.");
377
+ const tool = r.tools.find((t) => t.name === name);
378
+ if (!tool) return hold(`There's no tool called ${name}.`);
379
+ const bad = toolProblem(tool, args);
380
+ if (bad) return hold(bad);
381
+ reader.released++;
382
+ for (const held of reader.held.splice(0)) release(held);
383
+ release({ id, name, args });
384
+ },
385
+ /** The answer in `planSchema`'s shape as written so far: calls finished since last time are read. An optional
386
+ * argument left null is left out, as `readPlan` leaves it. */
387
+ planned(text) {
388
+ const found = plannedCalls(text);
389
+ for (const call of found.slice(seen)) {
390
+ const args = call?.args && typeof call.args === "object" && !Array.isArray(call.args) ? Object.fromEntries(Object.entries(call.args).filter(([, v]) => v !== null)) : call?.args;
391
+ reader.call(call?.name, args);
392
+ }
393
+ seen = found.length;
394
+ },
395
+ };
396
+ return reader;
397
+ }
398
+
399
+ /**
400
+ * A streamed step's answer once the model is done: when calls were released, only what's left (the calls that didn't
401
+ * fit, as failures); else the whole answer read as before (`read`, from `readMessage` or `readPlan`).
402
+ */
403
+ export function afterStream(reader, read) {
404
+ if (!reader.released) return read;
405
+ return { answer: { calls: reader.held.map((c) => ({ id: c.id, name: c.name, args: c.args, problem: c.problem })) } };
406
+ }
407
+
318
408
  // MARK: What the model is told
319
409
 
320
410
  const RULES = `You act for the person inside this app, only through its tools.
321
411
  - Use a tool to look something up before acting on it, and use ids exactly as the tools gave them. Never invent an id.
322
412
  - Change only what the person asked for. When the request is unclear (which one, who, what day or time), ask back in the reply instead of guessing.
323
- - Several look-ups can go in one answer. Changes run one after another, each checked right before it runs; a change that fails stops the ones after it in that answer.
413
+ - Several calls can go in one answer, and they run in the order you write them: look-ups side by side run together, changes run one after another, each checked right before it runs, and a change that fails stops the changes after it in that answer. A look-up after a change sees it.
324
414
  - Never say something was done unless its result says it was. If something failed or was skipped, say so.
325
415
  - When the request is met, or you need to ask, finish with \`reply\`: one or two short sentences that can be spoken aloud, no lists, no emoji, no markdown. Set \`ask\` to true only when the reply is a question for the person.
326
416
  - When your question has a few short answers (yes or no, which of two or three), give them in \`choices\`: two to four, each a word or a few, plain text, in the order to offer them. Leave \`choices\` out when any answer could do.`;
package/cli/calls.mjs CHANGED
@@ -32,7 +32,7 @@ import { tile } from "./png.mjs";
32
32
  import { Problem } from "./state.mjs";
33
33
  import { allowed as pagesAllowed, forgetPage, serveLocalAI } from "./local-ai.mjs";
34
34
  import { EngineTrouble, complete } from "./ai.mjs";
35
- import { OpenRouterTrouble, chat as openRouterChat, decide as openRouterDecide, image as openRouterImage, text as openRouterText } from "./openrouter.mjs";
35
+ import { OpenRouterTrouble, chat as openRouterChat, chatStream as openRouterChatStream, decide as openRouterDecide, image as openRouterImage, text as openRouterText } from "./openrouter.mjs";
36
36
 
37
37
  /** OpenRouter's answer, or what kept it from one, as Lamp's app reads it. */
38
38
  async function openRouter(asking) {
@@ -41,7 +41,7 @@ async function openRouter(asking) {
41
41
  return { trouble: { kind: error.kind, message: error.message, status: error.status, reply: error.reply, cost: error.cost } };
42
42
  }
43
43
  }
44
- import { Refused as StepRefused, chatBody, decidePrompt, planPrompt, planSchema, readDecision, readMessage, readPlan, readStep } from "./app-ai.mjs";
44
+ import { Refused as StepRefused, afterStream, chatBody, decidePrompt, planPrompt, planSchema, readDecision, readMessage, readPlan, readStep, stepReader } from "./app-ai.mjs";
45
45
 
46
46
  // Pages shared on lamp.dev asking for their AI, answered by Lamp's app (`sharedPages`): what each is waiting on from it.
47
47
  const sharedPagesWaiting = new Map();
@@ -383,9 +383,17 @@ export const CALLS = {
383
383
  model: text(given, "model"), json: given.json === true, schema: given.schema ?? undefined, seconds: seconds(given.seconds, 600, 600), signal: stopped }));
384
384
  },
385
385
 
386
- /** One chat completion from OpenRouter, its request as `body` (`actAsk`'s): `{message, cost}`, or `{trouble}`. */
387
- async openRouterChat(given, { stopped } = {}) {
388
- return openRouter(() => openRouterChat({ key: text(given, "key"), body: given.body ?? {}, seconds: seconds(given.seconds, 60, 600), signal: stopped }));
386
+ /**
387
+ * One chat completion from OpenRouter, its request as `body` (`actAsk`'s): `{message, cost}`, or `{trouble}`. With the
388
+ * act `step` it's for, streamed: each call that fits is an event `{call}` as soon as it's whole, and the answer says
389
+ * what was `released` and what was `held` (`stepReader`), for `actRead`.
390
+ */
391
+ async openRouterChat(given, { stopped, event = () => {} } = {}) {
392
+ const asked = { key: text(given, "key"), body: given.body ?? {}, seconds: seconds(given.seconds, 60, 600), signal: stopped };
393
+ if (!given.step) return openRouter(() => openRouterChat(asked));
394
+ const reader = stepReader(given.step, (call) => event({ call }));
395
+ const done = await openRouter(() => openRouterChatStream({ ...asked, onCall: (name, args) => reader.call(name, args) }));
396
+ return done.trouble ? { ...done, released: reader.released } : { ...done, released: reader.released, held: reader.held };
389
397
  },
390
398
 
391
399
  /** Decisions from OpenRouter's decision model (`AppAI.decide`): `key`, `model`, `state`, `questions`. `{answers, cost}` or `{trouble}`. */
@@ -405,14 +413,19 @@ export const CALLS = {
405
413
  * the program Lamp chose (`tool`) and what it needs set (`env`), `system`, `prompt`, optional `schema`, `model`,
406
414
  * `images` and `seconds`. `{text}`, or `{trouble: {kind, message}}`; stopping the call stops the engine.
407
415
  */
408
- async engineAnswer(given, { stopped } = {}) {
416
+ async engineAnswer(given, { stopped, event = () => {} } = {}) {
417
+ // With the act `step` it answers, streamed as `openRouterChat` streams it.
418
+ const reader = given.step ? stepReader(given.step, (call) => event({ call })) : null;
409
419
  try {
410
- return { text: await complete({ which: text(given, "engine"), tool: typeof given.tool === "string" && given.tool ? given.tool : undefined,
420
+ const answer = await complete({ which: text(given, "engine"), tool: typeof given.tool === "string" && given.tool ? given.tool : undefined,
411
421
  env: given.env && typeof given.env === "object" ? given.env : {}, system: typeof given.system === "string" ? given.system : "",
412
422
  prompt: text(given, "prompt"), schema: given.schema ?? undefined, model: typeof given.model === "string" ? given.model : "",
413
- images: Array.isArray(given.images) ? given.images : [], seconds: seconds(given.seconds, 600, 600), signal: stopped }) };
423
+ images: Array.isArray(given.images) ? given.images : [], seconds: seconds(given.seconds, 600, 600), signal: stopped,
424
+ onPartial: reader ? (so) => reader.planned(so) : undefined });
425
+ reader?.planned(answer);
426
+ return reader ? { text: answer, released: reader.released, held: reader.held } : { text: answer };
414
427
  } catch (error) {
415
- if (error instanceof EngineTrouble) return { trouble: { kind: error.kind, message: error.message } };
428
+ if (error instanceof EngineTrouble) return { trouble: { kind: error.kind, message: error.message }, ...(reader ? { released: reader.released } : {}) };
416
429
  throw error;
417
430
  }
418
431
  },
@@ -438,9 +451,12 @@ export const CALLS = {
438
451
  return { ...planPrompt(step, tag, retry), schema: planSchema(step) };
439
452
  },
440
453
 
441
- /** A step's answer read and checked: `message` from a chat with tools, or `plan`'s text. `{answer}` or `{problem}`. */
454
+ /** A step's answer read and checked: `message` from a chat with tools, or `plan`'s text. `{answer}` or `{problem}`; for a
455
+ * streamed one, with what was `released` and `held`, only the held calls, as failures. */
442
456
  async actRead(given) {
443
- return given.message !== undefined ? readMessage(given.message, given.step) : readPlan(String(given.plan ?? ""), given.step);
457
+ const read = given.message !== undefined ? readMessage(given.message, given.step) : readPlan(String(given.plan ?? ""), given.step);
458
+ // A streamed answer (`released` calls went out already): only what was held is left.
459
+ return Number(given.released) > 0 ? afterStream({ released: Number(given.released), held: Array.isArray(given.held) ? given.held : [] }, read) : read;
444
460
  },
445
461
 
446
462
  /** A decision asked of a text model: `{system, prompt, schema}`. */
@@ -711,17 +711,20 @@
711
711
 
712
712
  // Reads run together; then the writes, one after another, each checked right before it runs. A write that
713
713
  // fails stops the writes after it in this step, which the model hears were skipped.
714
- function runCalls(calls, step) {
715
- var results = {}, reads = [], changes = [];
716
- calls.forEach(function (c) { (tools[c.name] && tools[c.name].reads ? reads : changes).push(c); });
717
- 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) {
718
721
  if (late()) return null;
719
722
  var tool = tools[c.name], bad = actProblem(tool, c.name, c.args);
720
723
  if (bad) { results[c.id] = { id: c.id, ok: false, error: bad }; return null; }
721
724
  tell({ kind: 'read', name: c.name, args: c.args });
722
725
  var NOT_RUN = {};
723
726
  return Promise.resolve().then(function () {
724
- // 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.
725
728
  return late() ? NOT_RUN : tool.run(c.args, { turn: id, step: step, id: c.id });
726
729
  }).then(function (value) {
727
730
  if (value === NOT_RUN) return;
@@ -731,60 +734,69 @@
731
734
  if (p.pictures) results[c.id].pictures = p.pictures;
732
735
  });
733
736
  }, function (e) { results[c.id] = { id: c.id, ok: false, error: refused(e) }; });
734
- })).then(function () {
735
- var stopped = null;
736
- function next(i) {
737
- if (i >= changes.length) return results;
738
- var c = changes[i], tool = tools[c.name];
739
- function skip(reason) {
740
- out.skipped.push({ name: c.name, args: c.args, reason: reason });
741
- results[c.id] = { id: c.id, ok: false, skipped: reason };
742
- return next(i + 1);
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
- return next(i + 1);
749
- }
750
- if (late()) return results;
751
- if (stopped) return skip(stopped);
752
- var bad = actProblem(tool, c.name, c.args);
753
- if (bad) return fail(bad);
754
- var key = 'k' + hash(id + '|' + c.name + '|' + canonical(c.args));
755
- // The same change asked again in this turn gets its first result, and doesn't run again.
756
- // Its result is in an earlier step, which every request carries: pointed to, not sent again.
757
- 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); }
758
- if (writes >= limits.writes) return fail('Over this turn\'s limit of ' + limits.writes + ' changes.');
759
- var call = { turn: id, key: key, step: step, id: c.id };
760
- return Promise.resolve().then(function () { return tool.check ? tool.check(c.args, call) : null; }).then(function (problem) {
761
- if (problem) return fail(String(problem));
762
- // Cancel and the time are looked at again right before the change, never in the middle of it.
763
- if (late()) return results;
764
- tell({ kind: 'write', name: c.name, args: c.args });
765
- if (late()) return results;
766
- writes++;
767
- var running;
768
- try { running = Promise.resolve(tool.run(c.args, call)); } catch (e) { running = Promise.reject(e); }
769
- return running.then(function (value) {
770
- // The change is done: it's recorded now, whatever happens to its pictures after.
771
- ran[key] = { note: 'Done.' };
772
- var entry = { name: c.name, args: c.args, result: value };
773
- if (value && typeof value === 'object' && value.partly) entry.partly = String(value.partly);
774
- out.done.push(entry);
775
- tell({ kind: 'write', name: c.name, args: c.args, result: value });
776
- return pictured(value).then(function (p) { return { p: p }; });
777
- }).then(function (got) {
778
- var kept = sized(got.p.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);
779
779
  ran[key] = kept;
780
780
  results[c.id] = { id: c.id, ok: true, value: kept };
781
- if (got.p.pictures) results[c.id].pictures = got.p.pictures;
782
- return next(i + 1);
783
- }, function (e) { return fail(refused(e)); });
784
- }, function (e) { return fail(refused(e)); });
785
- }
786
- return next(0);
787
- });
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
+ };
788
800
  }
789
801
 
790
802
  function step() {
@@ -799,19 +811,29 @@
799
811
  if (route) body.route = route;
800
812
  if (steps.length >= limits.steps - 1) body.final = true;
801
813
  if (o.model) body.model = o.model;
802
- 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) {
803
819
  // What the step cost through a key, paid for whether or not its answer still runs.
804
820
  if (typeof answer.cost === 'number' && answer.cost > 0) out.cost += answer.cost;
805
821
  // Cancelled, out of time or already over: an answer that comes late runs nothing.
806
822
  if (late()) return;
807
823
  route = answer.route || route;
808
- if (answer.calls && answer.calls.length) {
809
- var calls = answer.calls, n = steps.length + 1, ids = {};
810
- for (var i = 0; i < calls.length; i++) {
811
- if (ids[calls[i].id]) return stop('error', 'The AI gave two calls the same id.');
812
- ids[calls[i].id] = true;
813
- }
814
- 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) {
815
837
  if (late()) return;
816
838
  steps.push({ calls: calls, results: calls.map(function (c) { return results[c.id] || { id: c.id, ok: false, skipped: 'Not run.' }; }) });
817
839
  if (steps.length >= limits.steps) return stop('steps');
@@ -826,11 +848,13 @@
826
848
  return typeof c === 'string' && c.trim() && c.length <= 24 && !/[<>*_`#\[\]{}|\\\n\r\t]/.test(c);
827
849
  }) ? offered.map(function (c) { return c.trim(); }) : [];
828
850
  stop(null);
829
- }, function (e) {
851
+ }
852
+ function failed(e) {
830
853
  if (e && typeof e.cost === 'number' && e.cost > 0) out.cost += e.cost;
831
854
  var message = refused(e);
832
855
  stop(/reach Lamp|Lamp isn't|wasn't put here|put it on this device/.test(message) ? 'unreachable' : 'error', message);
833
- });
856
+ }
857
+ post('/ai/act', body, stepTime, until).then(heard, failed);
834
858
  }
835
859
  step();
836
860
  return { id: id, result: result, cancel: function () { stop('cancelled'); } };
@@ -1065,10 +1089,12 @@
1065
1089
  * turn.cancel();
1066
1090
  *
1067
1091
  * An argument's shape is { type: 'string' | 'integer' | 'number' | 'boolean' | 'date' | 'datetime' | 'list',
1068
- * oneOf: [strings], min, max, optional, about, of (a list's item) }. A tool with `reads: true` changes nothing;
1069
- * reads in one step run together. Writes run one after another, each with its `check` right before it: a write
1070
- * that fails stops the writes after it in that step (`skipped`). Cancel and the time stop the turn between
1071
- * 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.
1072
1098
  *
1073
1099
  * Say what happened from `done`, `failed` and `skipped`, never from `reply`: they're what the page ran. A question
1074
1100
  * back (`ask`) may come with `choices`, two to four short answers to offer as buttons; with none, any answer goes.
package/cli/lamps/lamp.js CHANGED
@@ -711,17 +711,20 @@
711
711
 
712
712
  // Reads run together; then the writes, one after another, each checked right before it runs. A write that
713
713
  // fails stops the writes after it in this step, which the model hears were skipped.
714
- function runCalls(calls, step) {
715
- var results = {}, reads = [], changes = [];
716
- calls.forEach(function (c) { (tools[c.name] && tools[c.name].reads ? reads : changes).push(c); });
717
- 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) {
718
721
  if (late()) return null;
719
722
  var tool = tools[c.name], bad = actProblem(tool, c.name, c.args);
720
723
  if (bad) { results[c.id] = { id: c.id, ok: false, error: bad }; return null; }
721
724
  tell({ kind: 'read', name: c.name, args: c.args });
722
725
  var NOT_RUN = {};
723
726
  return Promise.resolve().then(function () {
724
- // 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.
725
728
  return late() ? NOT_RUN : tool.run(c.args, { turn: id, step: step, id: c.id });
726
729
  }).then(function (value) {
727
730
  if (value === NOT_RUN) return;
@@ -731,60 +734,69 @@
731
734
  if (p.pictures) results[c.id].pictures = p.pictures;
732
735
  });
733
736
  }, function (e) { results[c.id] = { id: c.id, ok: false, error: refused(e) }; });
734
- })).then(function () {
735
- var stopped = null;
736
- function next(i) {
737
- if (i >= changes.length) return results;
738
- var c = changes[i], tool = tools[c.name];
739
- function skip(reason) {
740
- out.skipped.push({ name: c.name, args: c.args, reason: reason });
741
- results[c.id] = { id: c.id, ok: false, skipped: reason };
742
- return next(i + 1);
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
- return next(i + 1);
749
- }
750
- if (late()) return results;
751
- if (stopped) return skip(stopped);
752
- var bad = actProblem(tool, c.name, c.args);
753
- if (bad) return fail(bad);
754
- var key = 'k' + hash(id + '|' + c.name + '|' + canonical(c.args));
755
- // The same change asked again in this turn gets its first result, and doesn't run again.
756
- // Its result is in an earlier step, which every request carries: pointed to, not sent again.
757
- 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); }
758
- if (writes >= limits.writes) return fail('Over this turn\'s limit of ' + limits.writes + ' changes.');
759
- var call = { turn: id, key: key, step: step, id: c.id };
760
- return Promise.resolve().then(function () { return tool.check ? tool.check(c.args, call) : null; }).then(function (problem) {
761
- if (problem) return fail(String(problem));
762
- // Cancel and the time are looked at again right before the change, never in the middle of it.
763
- if (late()) return results;
764
- tell({ kind: 'write', name: c.name, args: c.args });
765
- if (late()) return results;
766
- writes++;
767
- var running;
768
- try { running = Promise.resolve(tool.run(c.args, call)); } catch (e) { running = Promise.reject(e); }
769
- return running.then(function (value) {
770
- // The change is done: it's recorded now, whatever happens to its pictures after.
771
- ran[key] = { note: 'Done.' };
772
- var entry = { name: c.name, args: c.args, result: value };
773
- if (value && typeof value === 'object' && value.partly) entry.partly = String(value.partly);
774
- out.done.push(entry);
775
- tell({ kind: 'write', name: c.name, args: c.args, result: value });
776
- return pictured(value).then(function (p) { return { p: p }; });
777
- }).then(function (got) {
778
- var kept = sized(got.p.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);
779
779
  ran[key] = kept;
780
780
  results[c.id] = { id: c.id, ok: true, value: kept };
781
- if (got.p.pictures) results[c.id].pictures = got.p.pictures;
782
- return next(i + 1);
783
- }, function (e) { return fail(refused(e)); });
784
- }, function (e) { return fail(refused(e)); });
785
- }
786
- return next(0);
787
- });
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
+ };
788
800
  }
789
801
 
790
802
  function step() {
@@ -799,19 +811,29 @@
799
811
  if (route) body.route = route;
800
812
  if (steps.length >= limits.steps - 1) body.final = true;
801
813
  if (o.model) body.model = o.model;
802
- 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) {
803
819
  // What the step cost through a key, paid for whether or not its answer still runs.
804
820
  if (typeof answer.cost === 'number' && answer.cost > 0) out.cost += answer.cost;
805
821
  // Cancelled, out of time or already over: an answer that comes late runs nothing.
806
822
  if (late()) return;
807
823
  route = answer.route || route;
808
- if (answer.calls && answer.calls.length) {
809
- var calls = answer.calls, n = steps.length + 1, ids = {};
810
- for (var i = 0; i < calls.length; i++) {
811
- if (ids[calls[i].id]) return stop('error', 'The AI gave two calls the same id.');
812
- ids[calls[i].id] = true;
813
- }
814
- 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) {
815
837
  if (late()) return;
816
838
  steps.push({ calls: calls, results: calls.map(function (c) { return results[c.id] || { id: c.id, ok: false, skipped: 'Not run.' }; }) });
817
839
  if (steps.length >= limits.steps) return stop('steps');
@@ -826,11 +848,13 @@
826
848
  return typeof c === 'string' && c.trim() && c.length <= 24 && !/[<>*_`#\[\]{}|\\\n\r\t]/.test(c);
827
849
  }) ? offered.map(function (c) { return c.trim(); }) : [];
828
850
  stop(null);
829
- }, function (e) {
851
+ }
852
+ function failed(e) {
830
853
  if (e && typeof e.cost === 'number' && e.cost > 0) out.cost += e.cost;
831
854
  var message = refused(e);
832
855
  stop(/reach Lamp|Lamp isn't|wasn't put here|put it on this device/.test(message) ? 'unreachable' : 'error', message);
833
- });
856
+ }
857
+ post('/ai/act', body, stepTime, until).then(heard, failed);
834
858
  }
835
859
  step();
836
860
  return { id: id, result: result, cancel: function () { stop('cancelled'); } };
@@ -1065,10 +1089,12 @@
1065
1089
  * turn.cancel();
1066
1090
  *
1067
1091
  * An argument's shape is { type: 'string' | 'integer' | 'number' | 'boolean' | 'date' | 'datetime' | 'list',
1068
- * oneOf: [strings], min, max, optional, about, of (a list's item) }. A tool with `reads: true` changes nothing;
1069
- * reads in one step run together. Writes run one after another, each with its `check` right before it: a write
1070
- * that fails stops the writes after it in that step (`skipped`). Cancel and the time stop the turn between
1071
- * 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.
1072
1098
  *
1073
1099
  * Say what happened from `done`, `failed` and `skipped`, never from `reply`: they're what the page ran. A question
1074
1100
  * back (`ask`) may come with `choices`, two to four short answers to offer as buttons; with none, any answer goes.
@@ -1 +1 @@
1
- {"exe":"ZqxAppName","platform":"mac","instance":"B2538EDF-E84E-4703-8A9C-65006F6279AA","name":"Zqx App Name"}
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)
@@ -711,17 +711,20 @@
711
711
 
712
712
  // Reads run together; then the writes, one after another, each checked right before it runs. A write that
713
713
  // fails stops the writes after it in this step, which the model hears were skipped.
714
- function runCalls(calls, step) {
715
- var results = {}, reads = [], changes = [];
716
- calls.forEach(function (c) { (tools[c.name] && tools[c.name].reads ? reads : changes).push(c); });
717
- 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) {
718
721
  if (late()) return null;
719
722
  var tool = tools[c.name], bad = actProblem(tool, c.name, c.args);
720
723
  if (bad) { results[c.id] = { id: c.id, ok: false, error: bad }; return null; }
721
724
  tell({ kind: 'read', name: c.name, args: c.args });
722
725
  var NOT_RUN = {};
723
726
  return Promise.resolve().then(function () {
724
- // 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.
725
728
  return late() ? NOT_RUN : tool.run(c.args, { turn: id, step: step, id: c.id });
726
729
  }).then(function (value) {
727
730
  if (value === NOT_RUN) return;
@@ -731,60 +734,69 @@
731
734
  if (p.pictures) results[c.id].pictures = p.pictures;
732
735
  });
733
736
  }, function (e) { results[c.id] = { id: c.id, ok: false, error: refused(e) }; });
734
- })).then(function () {
735
- var stopped = null;
736
- function next(i) {
737
- if (i >= changes.length) return results;
738
- var c = changes[i], tool = tools[c.name];
739
- function skip(reason) {
740
- out.skipped.push({ name: c.name, args: c.args, reason: reason });
741
- results[c.id] = { id: c.id, ok: false, skipped: reason };
742
- return next(i + 1);
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
- return next(i + 1);
749
- }
750
- if (late()) return results;
751
- if (stopped) return skip(stopped);
752
- var bad = actProblem(tool, c.name, c.args);
753
- if (bad) return fail(bad);
754
- var key = 'k' + hash(id + '|' + c.name + '|' + canonical(c.args));
755
- // The same change asked again in this turn gets its first result, and doesn't run again.
756
- // Its result is in an earlier step, which every request carries: pointed to, not sent again.
757
- 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); }
758
- if (writes >= limits.writes) return fail('Over this turn\'s limit of ' + limits.writes + ' changes.');
759
- var call = { turn: id, key: key, step: step, id: c.id };
760
- return Promise.resolve().then(function () { return tool.check ? tool.check(c.args, call) : null; }).then(function (problem) {
761
- if (problem) return fail(String(problem));
762
- // Cancel and the time are looked at again right before the change, never in the middle of it.
763
- if (late()) return results;
764
- tell({ kind: 'write', name: c.name, args: c.args });
765
- if (late()) return results;
766
- writes++;
767
- var running;
768
- try { running = Promise.resolve(tool.run(c.args, call)); } catch (e) { running = Promise.reject(e); }
769
- return running.then(function (value) {
770
- // The change is done: it's recorded now, whatever happens to its pictures after.
771
- ran[key] = { note: 'Done.' };
772
- var entry = { name: c.name, args: c.args, result: value };
773
- if (value && typeof value === 'object' && value.partly) entry.partly = String(value.partly);
774
- out.done.push(entry);
775
- tell({ kind: 'write', name: c.name, args: c.args, result: value });
776
- return pictured(value).then(function (p) { return { p: p }; });
777
- }).then(function (got) {
778
- var kept = sized(got.p.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);
779
779
  ran[key] = kept;
780
780
  results[c.id] = { id: c.id, ok: true, value: kept };
781
- if (got.p.pictures) results[c.id].pictures = got.p.pictures;
782
- return next(i + 1);
783
- }, function (e) { return fail(refused(e)); });
784
- }, function (e) { return fail(refused(e)); });
785
- }
786
- return next(0);
787
- });
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
+ };
788
800
  }
789
801
 
790
802
  function step() {
@@ -799,19 +811,29 @@
799
811
  if (route) body.route = route;
800
812
  if (steps.length >= limits.steps - 1) body.final = true;
801
813
  if (o.model) body.model = o.model;
802
- 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) {
803
819
  // What the step cost through a key, paid for whether or not its answer still runs.
804
820
  if (typeof answer.cost === 'number' && answer.cost > 0) out.cost += answer.cost;
805
821
  // Cancelled, out of time or already over: an answer that comes late runs nothing.
806
822
  if (late()) return;
807
823
  route = answer.route || route;
808
- if (answer.calls && answer.calls.length) {
809
- var calls = answer.calls, n = steps.length + 1, ids = {};
810
- for (var i = 0; i < calls.length; i++) {
811
- if (ids[calls[i].id]) return stop('error', 'The AI gave two calls the same id.');
812
- ids[calls[i].id] = true;
813
- }
814
- 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) {
815
837
  if (late()) return;
816
838
  steps.push({ calls: calls, results: calls.map(function (c) { return results[c.id] || { id: c.id, ok: false, skipped: 'Not run.' }; }) });
817
839
  if (steps.length >= limits.steps) return stop('steps');
@@ -826,11 +848,13 @@
826
848
  return typeof c === 'string' && c.trim() && c.length <= 24 && !/[<>*_`#\[\]{}|\\\n\r\t]/.test(c);
827
849
  }) ? offered.map(function (c) { return c.trim(); }) : [];
828
850
  stop(null);
829
- }, function (e) {
851
+ }
852
+ function failed(e) {
830
853
  if (e && typeof e.cost === 'number' && e.cost > 0) out.cost += e.cost;
831
854
  var message = refused(e);
832
855
  stop(/reach Lamp|Lamp isn't|wasn't put here|put it on this device/.test(message) ? 'unreachable' : 'error', message);
833
- });
856
+ }
857
+ post('/ai/act', body, stepTime, until).then(heard, failed);
834
858
  }
835
859
  step();
836
860
  return { id: id, result: result, cancel: function () { stop('cancelled'); } };
@@ -1065,10 +1089,12 @@
1065
1089
  * turn.cancel();
1066
1090
  *
1067
1091
  * An argument's shape is { type: 'string' | 'integer' | 'number' | 'boolean' | 'date' | 'datetime' | 'list',
1068
- * oneOf: [strings], min, max, optional, about, of (a list's item) }. A tool with `reads: true` changes nothing;
1069
- * reads in one step run together. Writes run one after another, each with its `check` right before it: a write
1070
- * that fails stops the writes after it in that step (`skipped`). Cancel and the time stop the turn between
1071
- * 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.
1072
1098
  *
1073
1099
  * Say what happened from `done`, `failed` and `skipped`, never from `reply`: they're what the page ran. A question
1074
1100
  * back (`ask`) may come with `choices`, two to four short answers to offer as buttons; with none, any answer goes.
@@ -81,6 +81,84 @@ export async function chat({ key, body, seconds = 120, signal, fetcher = fetch }
81
81
  throw new OpenRouterTrouble("refused", "The AI didn't answer.", { cost });
82
82
  }
83
83
 
84
+ /**
85
+ * One chat completion streamed (`stream` on `LampAI.act`): `onCall(name, argumentsText)` hears each tool call, in order,
86
+ * as soon as its arguments are whole (they read as a JSON object, which can't go on after its closing brace), or when the
87
+ * answer ends. Providers may send several calls' pieces interleaved, so a later call starting says nothing. Resolves as `chat` does,
88
+ * `{message, cost}`, the whole message. Asked once: a provider's filter cutting it off is `filtered`, since calls may
89
+ * already have gone to the page.
90
+ */
91
+ export async function chatStream({ key, body, seconds = 120, signal, fetcher = fetch, onCall = () => {} }) {
92
+ const timeout = AbortSignal.timeout(Math.max(1000, Math.floor(seconds * 1000)));
93
+ let answer;
94
+ try {
95
+ answer = await fetcher(ENDPOINT, { method: "POST", body: JSON.stringify({ ...body, stream: true }),
96
+ headers: { Authorization: `Bearer ${key || ""}`, "Content-Type": "application/json", "X-Title": "Lamp" },
97
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
98
+ } catch (error) {
99
+ if (signal?.aborted) throw new OpenRouterTrouble("stopped", "Stopped.");
100
+ if (error?.name === "TimeoutError") throw new OpenRouterTrouble("timeout", "This ran out of time.");
101
+ throw new OpenRouterTrouble("refused", "Couldn't reach OpenRouter.");
102
+ }
103
+ if (answer.status !== 200 || !answer.body) {
104
+ let reply = {};
105
+ try { reply = await answer.json(); } catch {}
106
+ throw new OpenRouterTrouble("refused", "OpenRouter didn't answer.", { status: answer.status, reply });
107
+ }
108
+ const calls = [], told = new Set();
109
+ let content = "", cost = 0, finish = null, rest = "";
110
+ const whole = (call) => { try { const args = JSON.parse(call.function.arguments); return !!call.function.name && !!args && typeof args === "object" && !Array.isArray(args); } catch { return false; } };
111
+ // In order: a call waits for every one before it, so they run as they were written.
112
+ const tell = (all = false) => {
113
+ for (let i = told.size; i < calls.length; i++) {
114
+ if (!calls[i] || (!all && !whole(calls[i]))) return;
115
+ told.add(i);
116
+ onCall(calls[i].function.name, calls[i].function.arguments);
117
+ }
118
+ };
119
+ const take = (line) => {
120
+ if (!line.startsWith("data:")) return;
121
+ const data = line.slice(5).trim();
122
+ if (!data || data === "[DONE]") return;
123
+ let chunk;
124
+ try { chunk = JSON.parse(data); } catch { return; }
125
+ if (chunk.usage) cost += Number(chunk.usage.cost) || 0;
126
+ if (chunk.error) throw new OpenRouterTrouble("refused", "OpenRouter didn't answer.", { status: Number(chunk.error.code) || 0, reply: { error: chunk.error }, cost });
127
+ const choice = Array.isArray(chunk.choices) ? chunk.choices[0] : null;
128
+ if (!choice) return;
129
+ if (choice.finish_reason) finish = choice.finish_reason;
130
+ const delta = choice.delta ?? {};
131
+ if (typeof delta.content === "string") content += delta.content;
132
+ for (const part of Array.isArray(delta.tool_calls) ? delta.tool_calls : []) {
133
+ const i = Number.isInteger(part.index) ? part.index : calls.length;
134
+ if (!calls[i]) calls[i] = { id: part.id || `call_${i}`, type: "function", function: { name: "", arguments: "" } };
135
+ if (part.id) calls[i].id = part.id;
136
+ if (part.function?.name) calls[i].function.name += part.function.name;
137
+ if (part.function?.arguments) calls[i].function.arguments += part.function.arguments;
138
+ }
139
+ tell();
140
+ };
141
+ try {
142
+ const decoder = new TextDecoder();
143
+ for await (const bytes of answer.body) {
144
+ rest += decoder.decode(bytes, { stream: true });
145
+ const lines = rest.split("\n");
146
+ rest = lines.pop();
147
+ for (const line of lines) take(line.trim());
148
+ }
149
+ if (rest.trim()) take(rest.trim());
150
+ } catch (error) {
151
+ if (error instanceof OpenRouterTrouble) throw error;
152
+ if (signal?.aborted) throw new OpenRouterTrouble("stopped", "Stopped.", { cost });
153
+ if (error?.name === "TimeoutError" || timeout.aborted) throw new OpenRouterTrouble("timeout", "This ran out of time.", { cost });
154
+ throw new OpenRouterTrouble("refused", "OpenRouter stopped answering.", { cost });
155
+ }
156
+ if (finish === "content_filter") throw new OpenRouterTrouble("filtered", FILTERED, { cost });
157
+ tell(true);
158
+ const message = { content, ...(calls.length ? { tool_calls: calls.filter(Boolean) } : {}) };
159
+ return { message, cost };
160
+ }
161
+
84
162
  /** A text answer: `{text, cost}`. */
85
163
  export async function text(given) {
86
164
  const { message, cost } = await chat({ ...given, body: textBody(given) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lampdotdev/lamp",
3
- "version": "0.36.2-nightly.43",
3
+ "version": "0.36.2-nightly.44",
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": {