@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 +63 -7
- package/cli/app-ai.mjs +91 -1
- package/cli/calls.mjs +27 -11
- package/cli/lamps/lamp-for-games.js +97 -71
- package/cli/lamps/lamp.js +97 -71
- package/cli/lamps/mac-starter/.lamp/meta.json +1 -1
- package/cli/lamps/mac-starter/Sources/LampSupport.swift +18 -16
- package/cli/lamps/web-starter/lamp.js +97 -71
- package/cli/openrouter.mjs +78 -0
- package/package.json +1 -1
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
//
|
|
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
|
-
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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 (
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
}
|
|
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
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
* calls,
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
//
|
|
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
|
-
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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 (
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
}
|
|
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
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
* calls,
|
|
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":"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
//
|
|
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
|
-
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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 (
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
}
|
|
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
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
* calls,
|
|
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/openrouter.mjs
CHANGED
|
@@ -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) });
|