@vibes.diy/api-svc 14.3.85 → 14.3.87
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/create-handler.js +2 -0
- package/create-handler.js.map +1 -1
- package/index.d.ts +3 -1
- package/index.js +3 -1
- package/index.js.map +1 -1
- package/intern/agent-account-tools.d.ts +1 -0
- package/intern/agent-account-tools.js +1 -0
- package/intern/agent-account-tools.js.map +1 -1
- package/intern/agent-assembly.js +5 -1
- package/intern/agent-assembly.js.map +1 -1
- package/intern/agent-belt.d.ts +1 -3
- package/intern/agent-belt.js +25 -21
- package/intern/agent-belt.js.map +1 -1
- package/intern/agent-build-tools.d.ts +7 -0
- package/intern/agent-build-tools.js +6 -2
- package/intern/agent-build-tools.js.map +1 -1
- package/intern/agent-closing-options.d.ts +26 -0
- package/intern/agent-closing-options.js +130 -0
- package/intern/agent-closing-options.js.map +1 -0
- package/intern/agent-dispatcher.d.ts +1 -0
- package/intern/agent-dispatcher.js +17 -3
- package/intern/agent-dispatcher.js.map +1 -1
- package/intern/agent-session-instruction.d.ts +3 -2
- package/intern/agent-session-instruction.js +9 -7
- package/intern/agent-session-instruction.js.map +1 -1
- package/intern/agent-turn.d.ts +1 -0
- package/intern/agent-turn.js +1 -0
- package/intern/agent-turn.js.map +1 -1
- package/intern/alt-first-build.d.ts +48 -0
- package/intern/alt-first-build.js +179 -0
- package/intern/alt-first-build.js.map +1 -0
- package/intern/alt-landing.d.ts +13 -0
- package/intern/alt-landing.js +55 -0
- package/intern/alt-landing.js.map +1 -0
- package/intern/first-brief.d.ts +1 -1
- package/intern/first-brief.js +1 -1
- package/intern/first-brief.js.map +1 -1
- package/intern/first-build-classifier.d.ts +4 -4
- package/intern/first-build-classifier.js +15 -17
- package/intern/first-build-classifier.js.map +1 -1
- package/intern/first-build-race.d.ts +71 -0
- package/intern/first-build-race.js +257 -0
- package/intern/first-build-race.js.map +1 -0
- package/intern/instruction-variant.d.ts +23 -2
- package/intern/instruction-variant.js +159 -2
- package/intern/instruction-variant.js.map +1 -1
- package/intern/keeps-to-themselves.d.ts +1 -0
- package/intern/keeps-to-themselves.js +10 -0
- package/intern/keeps-to-themselves.js.map +1 -0
- package/intern/llm-tool-loop.d.ts +5 -1
- package/intern/llm-tool-loop.js +48 -15
- package/intern/llm-tool-loop.js.map +1 -1
- package/intern/mercury-capability-triggers.d.ts +1 -0
- package/intern/mercury-capability-triggers.js +95 -0
- package/intern/mercury-capability-triggers.js.map +1 -0
- package/intern/mercury-first-build-seam.d.ts +40 -5
- package/intern/mercury-first-build-seam.js +201 -14
- package/intern/mercury-first-build-seam.js.map +1 -1
- package/intern/mercury-first-build.d.ts +591 -21
- package/intern/mercury-first-build.js +2998 -287
- package/intern/mercury-first-build.js.map +1 -1
- package/intern/mercury-resilience.d.ts +70 -0
- package/intern/mercury-resilience.js +122 -0
- package/intern/mercury-resilience.js.map +1 -0
- package/intern/mint-decision.d.ts +1 -0
- package/intern/mint-decision.js +3 -0
- package/intern/mint-decision.js.map +1 -1
- package/intern/option-line-hold.d.ts +5 -0
- package/intern/option-line-hold.js +39 -0
- package/intern/option-line-hold.js.map +1 -0
- package/intern/pouf-room-catalog.d.ts +2 -0
- package/intern/pouf-room-catalog.js +11 -0
- package/intern/pouf-room-catalog.js.map +1 -0
- package/intern/retire-offer-options.d.ts +3 -0
- package/intern/retire-offer-options.js +91 -0
- package/intern/retire-offer-options.js.map +1 -0
- package/intern/seed-chat-section.d.ts +13 -0
- package/intern/seed-chat-section.js +62 -0
- package/intern/seed-chat-section.js.map +1 -1
- package/intern/server-first-build.js +1 -0
- package/intern/server-first-build.js.map +1 -1
- package/intern/vibe-font-catalog.d.ts +2 -0
- package/intern/vibe-font-catalog.js +8 -0
- package/intern/vibe-font-catalog.js.map +1 -0
- package/package.json +14 -14
- package/public/agent-turn-action.d.ts +1 -0
- package/public/agent-turn-action.js +256 -62
- package/public/agent-turn-action.js.map +1 -1
- package/public/agent-turn-assembly.js +5 -2
- package/public/agent-turn-assembly.js.map +1 -1
- package/public/prompt-chat-section.js +7 -5
- package/public/prompt-chat-section.js.map +1 -1
- package/public/prompt-turn-terminal.js +3 -1
- package/public/prompt-turn-terminal.js.map +1 -1
- package/public/report-activity.js +18 -2
- package/public/report-activity.js.map +1 -1
- package/public/vibe-traffic-source.js +1 -1
- package/public/vibe-traffic-source.js.map +1 -1
- package/types.d.ts +1 -0
- package/types.js.map +1 -1
- package/usage-report/new-user-alert-util.d.ts +6 -0
- package/usage-report/new-user-alert-util.js +30 -1
- package/usage-report/new-user-alert-util.js.map +1 -1
- package/intern/agent-options-tool.d.ts +0 -27
- package/intern/agent-options-tool.js +0 -213
- package/intern/agent-options-tool.js.map +0 -1
|
@@ -1,21 +1,48 @@
|
|
|
1
1
|
import { Result, exception2Result } from "@adviser/cement";
|
|
2
|
+
import { and, eq } from "drizzle-orm";
|
|
2
3
|
import { type } from "arktype";
|
|
3
4
|
import { lazyType } from "@vibes.diy/api-types/lazy-type";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { sanitizeRoomShortlist } from "@vibes.diy/prompts";
|
|
6
|
+
import { KNOWN_VALUES_DIRECT_CONTROLS, ONE_TREE_PER_PERSON } from "./agent-session-instruction.js";
|
|
7
|
+
import { keepsToThemselves as keepsToThemselvesWords } from "./keeps-to-themselves.js";
|
|
8
|
+
import { poufRoomCatalog, poufRoomSlugs } from "./pouf-room-catalog.js";
|
|
9
|
+
import { vibeFontCatalog, vibeFontSlugs } from "./vibe-font-catalog.js";
|
|
10
|
+
import { slugWordBag } from "./ensure-slug-binding.js";
|
|
11
|
+
import { MERCURY_CAPABILITY_TRIGGERS } from "./mercury-capability-triggers.js";
|
|
12
|
+
import { parseRetryAfterMs, readFailureBody, waitMs } from "../public/provider-errors.js";
|
|
13
|
+
import { MERCURY_BREAKER, MERCURY_MIN_ATTEMPT_MS, MERCURY_STAGE_BUDGET_MS, MercuryCircuitBreaker, MercuryStageError, mercuryBudget, mercuryRetryWaitMs, mercuryTesterRetryWaitMs, MERCURY_TESTER_FANOUT, MERCURY_TESTER_REQUEST_DEADLINE_MS, MERCURY_TESTER_STAGE_BUDGET_MS, } from "./mercury-resilience.js";
|
|
7
14
|
export const MERCURY_FIRST_BUILD_MODEL = "inception/mercury-2.5";
|
|
8
15
|
export const MERCURY_FIRST_BUILD_REASONING = { effort: "none" };
|
|
16
|
+
export const MERCURY_OPUS_PLANNER_MODEL = "~anthropic/claude-opus-latest";
|
|
17
|
+
export const MERCURY_OPUS_PLANNER_DEADLINE_MS = 240_000;
|
|
18
|
+
export const MERCURY_OPUS_PLANNER_BUDGET_MS = 300_000;
|
|
19
|
+
export const MERCURY_OPUS_PLANNER_FANOUT = 1;
|
|
20
|
+
export const MERCURY_OPUS_PLANNER_MAX_TOKENS = 8_000;
|
|
21
|
+
export const MERCURY_OPUS_PLANNER_BREAKER = new MercuryCircuitBreaker();
|
|
22
|
+
export const MERCURY_HAIKU_PLANNER_MODEL = "anthropic/claude-haiku-5.5";
|
|
23
|
+
export const MERCURY_HAIKU_PLANNER_DEADLINE_MS = 40_000;
|
|
24
|
+
export const MERCURY_HAIKU_PLANNER_BUDGET_MS = 50_000;
|
|
25
|
+
export const MERCURY_HAIKU_PLANNER_BREAKER = new MercuryCircuitBreaker();
|
|
26
|
+
function plannerModel(planner) {
|
|
27
|
+
if (planner === "opus")
|
|
28
|
+
return { id: MERCURY_OPUS_PLANNER_MODEL };
|
|
29
|
+
if (planner === "haiku")
|
|
30
|
+
return { id: MERCURY_HAIKU_PLANNER_MODEL, reasoning: { enabled: false } };
|
|
31
|
+
return { id: MERCURY_FIRST_BUILD_MODEL, reasoning: MERCURY_FIRST_BUILD_REASONING };
|
|
32
|
+
}
|
|
33
|
+
function plannerAskedModel(planner) {
|
|
34
|
+
return plannerModel(planner ?? "mercury").id;
|
|
35
|
+
}
|
|
9
36
|
export const MERCURY_CALL_DEADLINE_MS = 8_000;
|
|
10
37
|
export const MERCURY_CALL_ATTEMPTS = 2;
|
|
11
38
|
export const MERCURY_TRANSIENT_RETRIES = 3;
|
|
12
|
-
export const MERCURY_TRANSIENT_BACKOFF_MS = 400;
|
|
13
|
-
const MERCURY_TRANSIENT_MAX_WAIT_MS = 2_000;
|
|
14
39
|
const MERCURY_TRANSIENT_STATUSES = new Set([429, 502, 503, 504]);
|
|
15
40
|
class MercuryTransientError extends Error {
|
|
41
|
+
status;
|
|
16
42
|
retryAfterMs;
|
|
17
|
-
constructor(message, retryAfterMs) {
|
|
43
|
+
constructor(message, status, retryAfterMs) {
|
|
18
44
|
super(message);
|
|
45
|
+
this.status = status;
|
|
19
46
|
this.retryAfterMs = retryAfterMs;
|
|
20
47
|
}
|
|
21
48
|
}
|
|
@@ -73,18 +100,10 @@ export function mercuryShapeSchema() {
|
|
|
73
100
|
},
|
|
74
101
|
};
|
|
75
102
|
}
|
|
76
|
-
export function mercuryNonce() {
|
|
77
|
-
return String(1000 + Math.floor(Math.random() * 9000));
|
|
78
|
-
}
|
|
79
103
|
export function mercuryShapeMessages(o) {
|
|
80
104
|
return [
|
|
81
105
|
{ role: "system", content: [{ type: "text", text: MERCURY_SHAPE_SYSTEM }] },
|
|
82
|
-
{
|
|
83
|
-
role: "user",
|
|
84
|
-
content: [
|
|
85
|
-
{ type: "text", text: `Person's words: "${o.personWords}"\n(variant #${o.nonce}: a seed for wording, never written)` },
|
|
86
|
-
],
|
|
87
|
-
},
|
|
106
|
+
{ role: "user", content: [{ type: "text", text: `Person's words: "${o.personWords}"` }] },
|
|
88
107
|
];
|
|
89
108
|
}
|
|
90
109
|
const shapeParsed = lazyType(() => type({
|
|
@@ -134,11 +153,14 @@ const COMPOSER_WORDS = /paste|photo|picture|own words|however they|repl(y|ies)|i
|
|
|
134
153
|
const BACKEND_WORDS = /remind|notif|email|digest|schedul|every (day|week|morning|monday)|nag|feed|sync|server|secret|deal|webhook|cron|poll/;
|
|
135
154
|
const PRIVATE_WORDS = /\b(private|just for me|only me|for myself)\b/;
|
|
136
155
|
const MULTIPLAYER_WORDS = /\b(friends? can join|multiplayer|same world)\b/;
|
|
137
|
-
export function fixMercuryShape(shape, personWords) {
|
|
156
|
+
export function fixMercuryShape(shape, personWords, opts = {}) {
|
|
138
157
|
const m = personWords.toLowerCase();
|
|
139
158
|
const composer = shape.flags.ai && shape.flags.composer && COMPOSER_WORDS.test(m);
|
|
140
|
-
const backend = shape.flags.backend && BACKEND_WORDS.test(m);
|
|
141
|
-
const
|
|
159
|
+
const backend = shape.flags.backend && (opts.keepBackend === true || BACKEND_WORDS.test(m));
|
|
160
|
+
const privateAsk = PRIVATE_WORDS.test(m);
|
|
161
|
+
const keepsToThemselves = keepsToThemselvesWords(personWords);
|
|
162
|
+
const keepsPersonal = keepsToThemselves && shape.archetype !== "toy" && shape.archetype !== "personal";
|
|
163
|
+
const archetype = keepsPersonal || (privateAsk && (shape.archetype === "open" || shape.archetype === "publish"))
|
|
142
164
|
? "personal"
|
|
143
165
|
: shape.archetype === "toy" && MULTIPLAYER_WORDS.test(m)
|
|
144
166
|
? "open"
|
|
@@ -151,11 +173,280 @@ export function fixMercuryShape(shape, personWords) {
|
|
|
151
173
|
.filter((r) => r.length > 0)
|
|
152
174
|
.slice(0, 5),
|
|
153
175
|
flags: { ...shape.flags, composer, backend },
|
|
176
|
+
...(keepsToThemselves ? { keepsToThemselves: true } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const draftShapeParsed = lazyType(() => type({
|
|
180
|
+
archetype: type.enumerated(...MERCURY_ARCHETYPES),
|
|
181
|
+
coreAction: "string",
|
|
182
|
+
recordNames: "string[]",
|
|
183
|
+
capabilities: type.enumerated(...MERCURY_FLAGS).array(),
|
|
184
|
+
}));
|
|
185
|
+
export function mercuryBackendWordsNamed(personWords) {
|
|
186
|
+
return BACKEND_WORDS.test(personWords.toLowerCase());
|
|
187
|
+
}
|
|
188
|
+
export function parseMercuryDraftShape(raw, personWords) {
|
|
189
|
+
const p = draftShapeParsed(raw);
|
|
190
|
+
if (p instanceof type.errors)
|
|
191
|
+
return Result.Err(`mercury classification: ${p.summary}`);
|
|
192
|
+
if (p.coreAction.trim().length === 0)
|
|
193
|
+
return Result.Err("mercury classification: empty coreAction");
|
|
194
|
+
const caps = new Set(p.capabilities);
|
|
195
|
+
const flags = Object.fromEntries(MERCURY_FLAGS.map((f) => [f, caps.has(f)]));
|
|
196
|
+
return Result.Ok(fixMercuryShape({ archetype: p.archetype, coreAction: p.coreAction.trim(), kind: "", who: "", recordNames: p.recordNames, flags }, personWords, { keepBackend: true }));
|
|
197
|
+
}
|
|
198
|
+
function setFlags(flags) {
|
|
199
|
+
return MERCURY_FLAGS.filter((f) => flags[f]);
|
|
200
|
+
}
|
|
201
|
+
const TITLE_LABEL = /^\s*titles?\s*:\s*/i;
|
|
202
|
+
const SLUGS_LABEL = /^\s*slugs?\s*:\s*/i;
|
|
203
|
+
function splitMercurySlugs(v) {
|
|
204
|
+
return v
|
|
205
|
+
.replace(SLUGS_LABEL, "")
|
|
206
|
+
.split(/\s*[/|,\n]\s*/)
|
|
207
|
+
.map((t) => t.trim())
|
|
208
|
+
.filter((t) => t.length > 0);
|
|
209
|
+
}
|
|
210
|
+
export function mercuryAddressEncoding() {
|
|
211
|
+
const strings = (v) => Array.isArray(v) && v.every((t) => typeof t === "string");
|
|
212
|
+
return {
|
|
213
|
+
properties: { title: { type: "string" }, slugs: { type: "array", items: { type: "string" } } },
|
|
214
|
+
rule: "The title is the app's name in Title Case, two or three words in its own vocabulary. The slugs are three short kebab-case addresses for that one app, as an array of three strings; each becomes a web address and they need not repeat the title.",
|
|
215
|
+
ask: "title: the app's name, two or three words; slugs: an array of three short kebab-case addresses",
|
|
216
|
+
hints: {
|
|
217
|
+
title: "the app's name, two or three words in its own vocabulary",
|
|
218
|
+
slugs: "three short kebab-case addresses for the app",
|
|
219
|
+
},
|
|
220
|
+
example: (title, slugs) => [`title: ${title}`, `slugs: ${JSON.stringify(slugs)}`],
|
|
221
|
+
decode: (reply) => {
|
|
222
|
+
const repairs = new Set();
|
|
223
|
+
let title;
|
|
224
|
+
if (typeof reply.title === "string") {
|
|
225
|
+
if (TITLE_LABEL.test(reply.title))
|
|
226
|
+
repairs.add("label");
|
|
227
|
+
title = reply.title.replace(TITLE_LABEL, "").trim();
|
|
228
|
+
}
|
|
229
|
+
else if (reply.title !== undefined && reply.title !== null)
|
|
230
|
+
return Result.Err("title-shape");
|
|
231
|
+
const v = reply.slugs;
|
|
232
|
+
let slugs;
|
|
233
|
+
if (v === undefined || v === null)
|
|
234
|
+
slugs = [];
|
|
235
|
+
else if (typeof v === "string") {
|
|
236
|
+
repairs.add("split");
|
|
237
|
+
if (SLUGS_LABEL.test(v))
|
|
238
|
+
repairs.add("label");
|
|
239
|
+
slugs = splitMercurySlugs(v);
|
|
240
|
+
}
|
|
241
|
+
else if (strings(v)) {
|
|
242
|
+
if (v.some((t) => SLUGS_LABEL.test(t)))
|
|
243
|
+
repairs.add("label");
|
|
244
|
+
slugs = v.map((t) => t.replace(SLUGS_LABEL, "").trim());
|
|
245
|
+
}
|
|
246
|
+
else
|
|
247
|
+
return Result.Err("slugs-shape");
|
|
248
|
+
return Result.Ok({ ...(title === undefined ? {} : { title }), slugs, repairs: [...repairs] });
|
|
249
|
+
},
|
|
250
|
+
salvage: (json) => (strings(json) && json.length === 3 ? { slugs: json } : undefined),
|
|
154
251
|
};
|
|
155
252
|
}
|
|
156
|
-
export const
|
|
253
|
+
export const MERCURY_ADDRESS = mercuryAddressEncoding();
|
|
254
|
+
export function mercurySlugWordsSentence(words) {
|
|
255
|
+
return words.length === 0
|
|
256
|
+
? ""
|
|
257
|
+
: `Fold a different one of these words into each slug, picking the ones that best suit the app; the title stays in the app's own words: ${words.join(", ")}.`;
|
|
258
|
+
}
|
|
259
|
+
const MERCURY_SLOT_HINTS = {
|
|
260
|
+
opening: "one sentence: what it is, who it is for, and the moment that makes it fun to open",
|
|
261
|
+
access: "who owns the object, who else is let in, what a visitor sees; the archetype's shape",
|
|
262
|
+
seed: "demo: a lively cast and where each one stands, in words: who is ahead, what is overdue, what just happened",
|
|
263
|
+
look: "one scene: surface, palette, card shape, one gesture of mood",
|
|
264
|
+
icon: "one object from the app's world",
|
|
265
|
+
};
|
|
266
|
+
export const MERCURY_OWN_WORLD_SEED_HINT = "demo: a lively cast and where each one stands, in words, in states this app's own records hold";
|
|
267
|
+
function replyKey(f) {
|
|
268
|
+
return f === "seed" ? "demo" : f;
|
|
269
|
+
}
|
|
270
|
+
export const MERCURY_ROOMS_HINT = "an array of 2 or 3 room slugs from the room list";
|
|
271
|
+
export const MERCURY_ROOMS_ASK = `Also rooms: ${MERCURY_ROOMS_HINT}.`;
|
|
272
|
+
export const MERCURY_FONT_HINT = "one font slug from the font list";
|
|
273
|
+
export const MERCURY_FONT_ASK = `Also font: ${MERCURY_FONT_HINT}.`;
|
|
274
|
+
export const MERCURY_CLASSIFY_HINTS = {
|
|
275
|
+
archetype: "one of personal, group, publish, intake, open, toy: the ownership shape that fits the person's words",
|
|
276
|
+
coreAction: "the one thing the person does most in this app, a short verb phrase in their own words",
|
|
277
|
+
recordNames: "an array of 1 to 5 short singular record type names the app stores",
|
|
278
|
+
capabilities: "an array naming only the capabilities this request needs, from the capability list; empty when none",
|
|
279
|
+
};
|
|
280
|
+
function withClassifyFirst(obj, classify, value) {
|
|
281
|
+
if (!classify)
|
|
282
|
+
return obj;
|
|
283
|
+
return { ...Object.fromEntries(Object.keys(MERCURY_CLASSIFY_HINTS).map((k) => [k, value(k)])), ...obj };
|
|
284
|
+
}
|
|
285
|
+
export function mercuryAnswerSkeleton(fields, opts = {}) {
|
|
286
|
+
const lead = (o) => withClassifyFirst(o, opts.classify === true, (k) => `<${MERCURY_CLASSIFY_HINTS[k]}>`);
|
|
287
|
+
const obj = {};
|
|
288
|
+
const recordsRules = opts.recordsRules === true;
|
|
289
|
+
const seedHint = recordsRules
|
|
290
|
+
? MERCURY_RECORDS_SEED_HINT
|
|
291
|
+
: opts.juice === "own-world"
|
|
292
|
+
? MERCURY_OWN_WORLD_SEED_HINT
|
|
293
|
+
: MERCURY_SLOT_HINTS.seed;
|
|
294
|
+
const screensHint = recordsRules ? MERCURY_RECORDS_SCREENS_HINT : MERCURY_DEPTH_SCREENS_HINT;
|
|
295
|
+
for (const f of fields) {
|
|
296
|
+
if (f === "pairs")
|
|
297
|
+
for (const [k, hint] of Object.entries(MERCURY_ADDRESS.hints))
|
|
298
|
+
obj[k] = `<${hint}>`;
|
|
299
|
+
else
|
|
300
|
+
obj[replyKey(f)] = `<${f === "seed" ? seedHint : MERCURY_SLOT_HINTS[f]}>`;
|
|
301
|
+
}
|
|
302
|
+
const withRecords = (o) => recordsRules ? withRecordsKeys(o, `<${MERCURY_RECORDS_HINT}>`, `<${MERCURY_RULES_HINT}>`) : o;
|
|
303
|
+
if (opts.story === true || opts.preamble === true) {
|
|
304
|
+
const outline = withRecords(opts.screens === true ? withScreensKey(obj, `<${screensHint}>`) : obj);
|
|
305
|
+
const whole = withWholeKeys(outline, {
|
|
306
|
+
...(opts.story === true ? { story: `<${MERCURY_STORY_HINT}>` } : {}),
|
|
307
|
+
...(opts.preamble === true ? { preamble: `<${MERCURY_WHOLE_PREAMBLE_HINT}>` } : {}),
|
|
308
|
+
});
|
|
309
|
+
if (opts.rooms === true)
|
|
310
|
+
whole.rooms = `<${MERCURY_ROOMS_HINT}>`;
|
|
311
|
+
if (opts.font === true)
|
|
312
|
+
whole.font = `<${MERCURY_FONT_HINT}>`;
|
|
313
|
+
return JSON.stringify(lead(whole), null, 1);
|
|
314
|
+
}
|
|
315
|
+
const out = withRecords(opts.screens === true ? withScreensKey(obj, `<${screensHint}>`) : obj);
|
|
316
|
+
if (opts.rooms === true)
|
|
317
|
+
out.rooms = `<${MERCURY_ROOMS_HINT}>`;
|
|
318
|
+
if (opts.font === true)
|
|
319
|
+
out.font = `<${MERCURY_FONT_HINT}>`;
|
|
320
|
+
return JSON.stringify(lead(out), null, 1);
|
|
321
|
+
}
|
|
322
|
+
const MERCURY_EXEMPLARS = [
|
|
323
|
+
{
|
|
324
|
+
archetype: "personal",
|
|
325
|
+
opening: "A glaze test notebook for a home potter, where opening the kiln means a fresh tile to rate.",
|
|
326
|
+
access: "Each signed-in person has their own notebook object (keyed on their handle, named for the app, renameable in settings); glazes and tiles carry the notebook id so a studio mate could be let in later. The creator reads and writes it alone.",
|
|
327
|
+
demo: "demo: the owner's notebook with Tenmoku the favourite glaze, Shino just mixed and Celadon still untested; tiles fired recently, the Chun blue one the best yet, an Ash tile cracked last week and still waiting for a good one.",
|
|
328
|
+
look: "Earthy studio: oatmeal background, iron-red accents, chunky rounded tile cards.",
|
|
329
|
+
title: "Glaze Ledger",
|
|
330
|
+
slugs: ["glaze-ledger", "kiln-tile-book", "glaze-notes"],
|
|
331
|
+
icon: "a glazed test tile beside a small kiln",
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
archetype: "group",
|
|
335
|
+
opening: "A tool library for one neighbourhood block, shared by the households on it, where a returned ladder lights up for the next borrower.",
|
|
336
|
+
access: "Each signed-in person starts with their own block library object. Households are let in with the platform's HandleInput invite screen, which writes a membership into the library; members read every tool and write their own loans. The library list shows shared ones with ViewerTag.",
|
|
337
|
+
demo: 'demo: the library "Maple Street Tools" with the owner\'s own membership plus @priya and @tomas; the ladder out with @priya and due back soon, the tile saw overdue with @tomas, the pressure washer just returned and waiting for its next loan.',
|
|
338
|
+
look: "Workshop pegboard: plywood tones, safety-orange accents, square tool cards with big status chips.",
|
|
339
|
+
title: "Block Tools",
|
|
340
|
+
slugs: ["block-tools", "maple-street-shelf", "borrow-board"],
|
|
341
|
+
icon: "a hand drill inside a house outline",
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
archetype: "publish",
|
|
345
|
+
opening: "A taco truck menu board the owner updates from their phone and customers read, where a sold-out stamp lands the moment the last plate goes.",
|
|
346
|
+
access: "The owner's menu object is keyed on their handle. Menu items are written by the owner and readable by everyone, signed in or not. Shaped so a co-owner could be let in later.",
|
|
347
|
+
demo: "demo: the owner's menu across tacos, sides and drinks; the al pastor sold out just now, the horchata nearly gone, a new salsa added this morning, and today's note from the owner.",
|
|
348
|
+
look: "Street-food bright: chalkboard dark with chalk-white type and chili-red price tags.",
|
|
349
|
+
title: "Taco Today",
|
|
350
|
+
slugs: ["taco-today", "truck-board", "menu-chalk"],
|
|
351
|
+
icon: "a chalkboard with a taco drawn on it",
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
archetype: "intake",
|
|
355
|
+
opening: "An appointment book for a dog grooming salon: clients book without signing up, and the groomer watches the day fill in.",
|
|
356
|
+
access: "Two sides. Clients book anonymously: anyone, signed in or not, can create a booking record on the salon object and reads back the confirmation they just made. The groomer, signed in as the salon's owner, reads and updates every booking.",
|
|
357
|
+
demo: "demo: the owner's salon with bookings in every state; Biscuit checked in just now, Juno booked for later today, Pepper still waiting on confirmation, Moss cancelled yesterday.",
|
|
358
|
+
look: "Clean salon: pale mint, charcoal type, generous slot buttons easy to tap one-handed.",
|
|
359
|
+
title: "Groom Book",
|
|
360
|
+
slugs: ["groom-book", "salon-slots", "paws-diary"],
|
|
361
|
+
icon: "a dog brush beside a clock face",
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
archetype: "open",
|
|
365
|
+
opening: "A lost-and-found board for a music festival that anyone with the link can post to, where a found tag turns a post green.",
|
|
366
|
+
access: "The owner creates the board and names it for the app. Any visitor with the link reads the whole board and writes their own posts; each poster edits or removes what they wrote, keyed on the record's creator. The board creator can remove anything.",
|
|
367
|
+
demo: "demo: the festival board with posts from a handful of handles; @lin's lost phone found just now, @ari's jacket still missing since last night, a set of keys claimed this morning.",
|
|
368
|
+
look: "Festival poster: cream paper, bold two-colour print, pinned-note cards with torn edges.",
|
|
369
|
+
title: "Festival Finds",
|
|
370
|
+
slugs: ["festival-finds", "lost-tent", "found-it"],
|
|
371
|
+
icon: "a tent peg tied with a ribbon",
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
archetype: "toy",
|
|
375
|
+
opening: "A hex-grid pong game with a slow-motion ball, where a long rally sets the board glowing.",
|
|
376
|
+
access: "Each signed-in player keeps a best score record keyed on their handle; a shared Top Scores list shows everyone's best with ViewerTag.",
|
|
377
|
+
demo: "demo: score records from a small cast; @rin on top, the owner close behind and climbing, @bo just joined with no score yet, so the Top Scores list opens filled.",
|
|
378
|
+
look: "Neon arcade: deep navy, electric cyan ball, glowing hex outlines.",
|
|
379
|
+
title: "Hex Pong",
|
|
380
|
+
slugs: ["hex-pong", "slow-rally", "honey-paddle"],
|
|
381
|
+
icon: "a hexagonal paddle and a glowing ball",
|
|
382
|
+
},
|
|
383
|
+
];
|
|
384
|
+
export function mercuryFieldArchetypes(archetype) {
|
|
385
|
+
return [archetype, ...MERCURY_ARCHETYPES.filter((a) => a !== archetype).slice(0, 2)];
|
|
386
|
+
}
|
|
387
|
+
const MERCURY_EXEMPLAR_CLASSIFICATION = {
|
|
388
|
+
personal: { coreAction: "rate a fresh test tile", records: ["glaze", "tile"] },
|
|
389
|
+
group: { coreAction: "borrow a tool", records: ["library", "membership", "tool", "loan"] },
|
|
390
|
+
publish: { coreAction: "mark a dish sold out", records: ["menu", "menu item"] },
|
|
391
|
+
intake: { coreAction: "book a grooming slot", records: ["salon", "booking"] },
|
|
392
|
+
open: { coreAction: "post a lost or found item", records: ["board", "post"] },
|
|
393
|
+
toy: { coreAction: "keep the rally going", records: ["score"] },
|
|
394
|
+
};
|
|
395
|
+
const MERCURY_EXEMPLAR_RECORDS = {
|
|
396
|
+
personal: {
|
|
397
|
+
records: "notebook (name); glaze (name, recipe, cone, notes); tile (glaze, fired on, rating, note)",
|
|
398
|
+
rules: "each glaze shows its best-rated tile; a cracked tile never counts toward its glaze's rating",
|
|
399
|
+
},
|
|
400
|
+
group: {
|
|
401
|
+
records: "library (name); membership (library, handle); tool (name, condition); loan (tool, borrower, due back, returned at)",
|
|
402
|
+
rules: "a tool is out with one borrower at a time; returning it frees it for the next borrower",
|
|
403
|
+
},
|
|
404
|
+
publish: {
|
|
405
|
+
records: "menu (name, today's note); menu item (name, price, section, sold out)",
|
|
406
|
+
rules: "a sold-out item stays on the board, stamped, until the owner clears it",
|
|
407
|
+
},
|
|
408
|
+
intake: {
|
|
409
|
+
records: "salon (name, hours); booking (dog, owner's name, service, slot, status)",
|
|
410
|
+
rules: "one booking per slot; a cancelled booking frees its slot",
|
|
411
|
+
},
|
|
412
|
+
open: {
|
|
413
|
+
records: "board (name); post (lost or found, item, where seen, note, found)",
|
|
414
|
+
rules: "each poster edits only their own posts; a found tag turns the post green",
|
|
415
|
+
},
|
|
416
|
+
toy: {
|
|
417
|
+
records: "score (player, best rally)",
|
|
418
|
+
rules: "only each player's best rally is kept",
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
function exemplarClassificationLine(a) {
|
|
422
|
+
const c = MERCURY_EXEMPLAR_CLASSIFICATION[a];
|
|
423
|
+
return `archetype: ${a}; core action: ${c.coreAction}; records: ${c.records.join(", ")}; capabilities: none`;
|
|
424
|
+
}
|
|
425
|
+
function exemplarBlock(e, address, classify = false, recordsRules = false) {
|
|
426
|
+
const rr = MERCURY_EXEMPLAR_RECORDS[e.archetype];
|
|
427
|
+
return [
|
|
428
|
+
`## Example [${e.archetype}]`,
|
|
429
|
+
"",
|
|
430
|
+
...(classify ? [`- ${exemplarClassificationLine(e.archetype)}`] : []),
|
|
431
|
+
`- opening: ${e.opening}`,
|
|
432
|
+
`- access: ${e.access}`,
|
|
433
|
+
...(recordsRules ? [`- records: ${rr.records}`, `- rules: ${rr.rules}`] : []),
|
|
434
|
+
`- ${e.demo}`,
|
|
435
|
+
`- look: ${e.look}`,
|
|
436
|
+
...address.example(e.title, e.slugs).map((line) => `- ${line}`),
|
|
437
|
+
`- icon: ${e.icon}`,
|
|
438
|
+
].join("\n");
|
|
439
|
+
}
|
|
440
|
+
export function mercuryExemplarField(archetype, address = MERCURY_ADDRESS, recordsRules = false) {
|
|
441
|
+
const byArchetype = new Map(MERCURY_EXEMPLARS.map((e) => [e.archetype, e]));
|
|
442
|
+
const blocks = mercuryFieldArchetypes(archetype).flatMap((a) => {
|
|
443
|
+
const e = byArchetype.get(a);
|
|
444
|
+
return e === undefined ? [] : [exemplarBlock(e, address, false, recordsRules)];
|
|
445
|
+
});
|
|
446
|
+
return [
|
|
447
|
+
`## How to read this field
|
|
157
448
|
|
|
158
|
-
|
|
449
|
+
Each example below is one whole brief, tagged with the ownership archetype it fits:
|
|
159
450
|
|
|
160
451
|
- [personal] one person's own tool: private, records keyed to their own object so a friend could be let in later.
|
|
161
452
|
- [group] a named set of people (friends, family, team, club, housemates): the owner invites them by handle, members read and write; the invite screen is part of this first build.
|
|
@@ -164,71 +455,170 @@ Every brief has the same parts, in this order. Below, each part is shown as a SP
|
|
|
164
455
|
- [open] anyone with the link joins and writes their own bit: a shared wall, a canvas, a leaderboard game. Each person edits what they made.
|
|
165
456
|
- [toy] a game or visual that keeps little or nothing: scores or a save slot at most.
|
|
166
457
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
- [
|
|
179
|
-
- [
|
|
180
|
-
- [
|
|
181
|
-
- [
|
|
182
|
-
- [toy]
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
export
|
|
230
|
-
|
|
231
|
-
|
|
458
|
+
Derive the person's app the way the example with their archetype derives its own, and write each part without its tag. The examples' subjects, record names and cast belong to the examples alone; every noun comes from the person's words.`,
|
|
459
|
+
...blocks,
|
|
460
|
+
].join("\n\n");
|
|
461
|
+
}
|
|
462
|
+
export const MERCURY_ONECALL_TRIPLES = [
|
|
463
|
+
["personal", "group", "publish"],
|
|
464
|
+
["intake", "open", "toy"],
|
|
465
|
+
["group", "intake", "toy"],
|
|
466
|
+
["personal", "publish", "open"],
|
|
467
|
+
];
|
|
468
|
+
const MERCURY_FIELD_TAGS = `- [personal] one person's own tool: private, records keyed to their own object so a friend could be let in later.
|
|
469
|
+
- [group] a named set of people (friends, family, team, club, housemates): the owner invites them by handle, members read and write; the invite screen is part of this first build.
|
|
470
|
+
- [publish] the owner writes, everyone reads: a blog, menu, gallery, zine, schedule.
|
|
471
|
+
- [intake] strangers submit without signing in (orders, bookings, requests, RSVPs); the owner sees and manages what came in.
|
|
472
|
+
- [open] anyone with the link joins and writes their own bit: a shared wall, a canvas, a leaderboard game. Each person edits what they made.
|
|
473
|
+
- [toy] a game or visual that keeps little or nothing: scores or a save slot at most.`;
|
|
474
|
+
export function mercuryOneCallField(draft, address = MERCURY_ADDRESS, recordsRules = false) {
|
|
475
|
+
const byArchetype = new Map(MERCURY_EXEMPLARS.map((e) => [e.archetype, e]));
|
|
476
|
+
const triple = MERCURY_ONECALL_TRIPLES[Math.abs(draft) % MERCURY_ONECALL_TRIPLES.length];
|
|
477
|
+
const blocks = triple.flatMap((a) => {
|
|
478
|
+
const e = byArchetype.get(a);
|
|
479
|
+
return e === undefined ? [] : [exemplarBlock(e, address, true, recordsRules)];
|
|
480
|
+
});
|
|
481
|
+
return [
|
|
482
|
+
`## How to read this field
|
|
483
|
+
|
|
484
|
+
Each example below is one whole brief, tagged with the ownership archetype it fits; three of the six archetypes are shown:
|
|
485
|
+
|
|
486
|
+
${MERCURY_FIELD_TAGS}
|
|
487
|
+
|
|
488
|
+
First decide which archetype fits the person's words, then derive their app the way the example with that archetype derives its own, or, when no example has it, the way the examples derive theirs, and write each part without its tag. The examples' subjects, record names and cast belong to the examples alone; every noun comes from the person's words.`,
|
|
489
|
+
...blocks,
|
|
490
|
+
].join("\n\n");
|
|
491
|
+
}
|
|
492
|
+
export const MERCURY_BRIEF_FLOORS = `Floors for every brief: each screen names at least two visible controls and one state that changes (a colour, a ribbon, a sort order, a chip, a badge); the demo names a lively cast and where each one stands (who is winning, what is overdue, what just landed, who has nothing yet); the opening names the one moment that makes the app fun to open. All of it in the person's own nouns and in words rather than numbers, and nothing their words did not imply.`;
|
|
493
|
+
export const MERCURY_DEMO_RULE = `The demo part starts with "demo:" and names a lively cast and where each one stands, in words: who is ahead, what is overdue, what just happened, what is still waiting; dates and counts are the builder's to pick, so the demo uses words like recently, just now, last week, nearly, still. Every demo record is one the app's own Add could write.`;
|
|
494
|
+
export const MERCURY_OWN_WORLD_DEMO_RULE = `The demo part starts with "demo:" and names a lively cast and where each one stands, in words, named in the app's own world: the states and progress this app's own records hold; dates and counts are the builder's to pick, so the demo uses words like recently, just now, last week, nearly, still. Every demo record is one the app's own Add could write.`;
|
|
495
|
+
export const MERCURY_OWN_WORLD_BRIEF_FLOORS = `Floors for every brief: each screen names at least two visible controls and one state that changes (a colour, a ribbon, a sort order, a chip, a badge); the demo names a lively cast and where each one stands, in the app's own world and in states its own records hold; the opening names the one moment that makes the app fun to open. All of it in the person's own nouns and in words rather than numbers, and nothing their words did not imply.`;
|
|
496
|
+
export const MERCURY_DEPTH_RULE = `Each screen also names one deeper interaction behind its main control, whichever this app's core action calls for: an undo, a long-press, a drag to reorder, an inline edit, or a filter chip.`;
|
|
497
|
+
export const MERCURY_DEPTH_SCREENS_HINT = `1. the core action as the biggest control, two visible controls, one state that changes, one deeper interaction behind the main control (an undo, a long-press, a drag to reorder, an inline edit or a filter chip), why it exists; 2. ...; 3. ...`;
|
|
498
|
+
export const MERCURY_RECORDS_RULE = `The records part names each record type the app stores, with its fields, as "volunteer (name, shift, note)". The rules part names the one to three computations or invariants the ask hinges on, in the person's terms — for example "close each tab as the fewest who-pays-whom transfers and save each payment", "a second sign-up under the same name updates that volunteer", "the band has a fixed lineup of five players and every player appears on each setlist", "every Friday email the summary to the coach's address". The screens build on the records and the rules.`;
|
|
499
|
+
export const MERCURY_RECORDS_SCREENS_RULE = `The screens part names two or three screens; each one says what it shows from the records and what the person does there for this ask, and the screen that carries out a rule says so. At most one screen adds a deeper interaction, and only where the ask implies one.`;
|
|
500
|
+
export const MERCURY_RECORDS_DEMO_RULE = `The demo part starts with "demo:" and is lively, in the app's own world: a small cast with names and the records the screens would show, in the states this app's own records hold. When the person typed data — items, amounts, names, a list — the demo carries every piece of it verbatim, quantities included ("half a bag" stays "half a bag"); dates and counts they did not give are the builder's to pick. Every demo record is one the app's own Add could write.`;
|
|
501
|
+
export const MERCURY_ASK_WORDS_RULE = `Keep the ask's genre and capability words exactly as the person wrote them — 3D, first-person, email, a named recipient, a schedule, a fixed number of people — in the opening, the rules or the screens.`;
|
|
502
|
+
export const MERCURY_RECORDS_BRIEF_FLOORS = `Floors for every brief: every record names its fields; the rules carry the computation or invariant the ask hinges on; each screen shows and does something this ask needs; the demo is lively and carries the person's own typed data word for word; the opening names the one moment that makes the app fun to open. All of it in the person's own nouns, and nothing their words did not imply.`;
|
|
503
|
+
export const MERCURY_RECORDS_HINT = `each record type the app stores, with its fields: "name (field, field, field); name (field, field)"`;
|
|
504
|
+
export const MERCURY_RULES_HINT = `the one to three computations or invariants the ask hinges on, in the person's terms`;
|
|
505
|
+
export const MERCURY_RECORDS_SCREENS_HINT = `1. the core action's screen: what it shows from the records and what the person does there for this ask, and the rule it carries out; 2. ...; 3. ...`;
|
|
506
|
+
export const MERCURY_RECORDS_SEED_HINT = "demo: a lively cast and the records the screens show, in the app's own world, with every item, amount and name the person typed, verbatim";
|
|
507
|
+
export const MERCURY_CHOPPED_RECORDS_SEED_LINE = `- seed: a small, lively world in the app's own words — a cast with quoted names and the records the screens would show, in the states this app's own records hold; any data the person typed (items, amounts, names, quantities) appears in it verbatim. Every seeded record is one the app's own Add could write.`;
|
|
508
|
+
const MERCURY_FIRST_PERSON_TRIGGER = " - a first-person walk around a place";
|
|
509
|
+
export const MERCURY_BLOCK_WORLD_TRIGGER = " - a Minecraft-style block world to build and break blocks in";
|
|
510
|
+
const MERCURY_BLOCK_WORLD_WORDS = /\b(minecraft|voxels?|block[- ]world|blocky|sandbox game)\b|\b(build|place|break|mine)\w*\b[^.]{0,30}\bblocks\b/i;
|
|
511
|
+
export function mercuryBlockWorldNamed(personWords) {
|
|
512
|
+
return MERCURY_BLOCK_WORLD_WORDS.test(personWords);
|
|
513
|
+
}
|
|
514
|
+
const MERCURY_STORY_FOOD = /soup|sauce|ferment|recipe|potluck|beverage|dinner|grocer|food|menu|cook|kitchen|meal|drink|cafe|restaurant|bake|snack|taco/i;
|
|
515
|
+
export const MERCURY_STORY_EXAMPLE_SOUP = "Mia opens the app to post today's soups by taking a picture of the kitchen recipe via the platform camera widget, right after the morning prep; she also sets the inventory level via an onscreen slider… customers at the queue browse the menu and make selections by clicking the quantity buttons on their phones.";
|
|
516
|
+
export const MERCURY_STORY_EXAMPLE_BIKE = "Dev opens the app to post today's repair queue by taking a picture of each bike's tag via the platform camera widget, right after the shop opens; he also sets each repair's progress via an onscreen slider… riders waiting at the counter browse the queue and pick their pickup time by turning the time picker on their phones.";
|
|
517
|
+
export function mercuryStoryExample(personWords) {
|
|
518
|
+
return MERCURY_STORY_FOOD.test(personWords) ? MERCURY_STORY_EXAMPLE_BIKE : MERCURY_STORY_EXAMPLE_SOUP;
|
|
519
|
+
}
|
|
520
|
+
export function mercuryStoryRule(example) {
|
|
521
|
+
return `The story part is a short scene of three to five sentences: one person from the demo cast uses the app through its core loop, and every step names its on-screen action: which control they use, the platform widget when one applies (the camera, a slider, a quantity stepper, a picker), and what changes on screen; then who else sees it and which control they use. The shape, shown for a different app: "${example}" That example's subject, cast, nouns and widgets belong to it alone; use a platform widget only when the person's app needs one. Every step of the story is something the screens offer, and the story stays inside the person's words.`;
|
|
522
|
+
}
|
|
523
|
+
export const MERCURY_STORY_HINT = "three to five sentences: one person from the demo cast using the app through its core loop, every step naming its on-screen action: which control they use, the platform widget if one applies (camera, slider, quantity stepper, picker), and what changes on screen; then who else sees it and which control they use";
|
|
524
|
+
export const MERCURY_WHOLE_PREAMBLE_RULE = `The preamble part is spoken to the person who asked, just before the app is built: plain sentences in their own words, starting with "I'm making" or "I'm building" — what you are making for them, its core action and the big control on its first screen that does it, then what else it does, its features named in their nouns. Make it vivid, like you can already see the app; it promises what the screens and the story carry.`;
|
|
525
|
+
export const MERCURY_WHOLE_PREAMBLE_HINT = `plain sentences to the person, starting with "I'm making" or "I'm building": what it is for them, the core action and its first-screen control, then what else it does`;
|
|
526
|
+
const ACCESS_SENTENCE = "The access part matches the archetype";
|
|
527
|
+
function briefRulesText(demoRule, floors) {
|
|
528
|
+
return `You write the settled parts of a build brief for a small web app (React, a local-first database, access rules). The brief is read by a builder, who works out the records, screens and mechanics from the person's own words. Write plain, concrete prose in the app's own nouns, as unformatted text in each JSON string.
|
|
529
|
+
|
|
530
|
+
The decisions already made are given in the user message; follow them (archetype, record names, capabilities). The access part matches the archetype: who owns the object, who else is let in, what visitors see. ${demoRule} Take from the field the SHAPE of a part (its length and how a cast, a look or a title is named); every noun comes from the person's words, and the app stays the one they asked for. ${MERCURY_ADDRESS.rule}
|
|
531
|
+
|
|
532
|
+
${floors}`;
|
|
533
|
+
}
|
|
534
|
+
function recordsBriefRulesText() {
|
|
535
|
+
return `You write the settled parts of a build brief for a small web app (React, a local-first database, access rules). The brief is read by a builder, who builds the records, rules and screens it names, from the person's own words. Write plain, concrete prose in the app's own nouns, as unformatted text in each JSON string.
|
|
536
|
+
|
|
537
|
+
The decisions already made are given in the user message; follow them (archetype, record names, capabilities). The access part matches the archetype: who owns the object, who else is let in, what visitors see. ${MERCURY_RECORDS_RULE} ${MERCURY_RECORDS_SCREENS_RULE} ${MERCURY_RECORDS_DEMO_RULE} ${MERCURY_ASK_WORDS_RULE} Take from the field the SHAPE of a part (its length and how a cast, a look or a title is named); every noun comes from the person's words, and the app stays the one they asked for. ${MERCURY_ADDRESS.rule}
|
|
538
|
+
|
|
539
|
+
${MERCURY_RECORDS_BRIEF_FLOORS}`;
|
|
540
|
+
}
|
|
541
|
+
export const MERCURY_BRIEF_RULES = briefRulesText(MERCURY_DEMO_RULE, MERCURY_BRIEF_FLOORS);
|
|
542
|
+
const MERCURY_DECIDED_SENTENCE = "The decisions already made are given in the user message; follow them (archetype, record names, capabilities).";
|
|
543
|
+
export const MERCURY_ONECALL_DECIDED = "The classification keys in your reply (archetype, core action, record names, capabilities) decide the brief; follow them.";
|
|
544
|
+
const MERCURY_SHAPE_ARCHETYPE_TEXT = (MERCURY_SHAPE_SYSTEM.split("archetype, exactly one:\n")[1] ?? "")
|
|
545
|
+
.split("\n\ncoreAction:")[0]
|
|
546
|
+
.trim();
|
|
547
|
+
const MERCURY_LEAN_ARCHETYPES = MERCURY_SHAPE_ARCHETYPE_TEXT.replace(/\nTie-breaks:[\s\S]*$/, "");
|
|
548
|
+
const MERCURY_LEAN_CAPABILITIES = MERCURY_CAPABILITY_TRIGGERS.replace(/^A trigger is an example, not a password: [^\n]*?composer is set only together with ai\./, "composer goes with ai.");
|
|
549
|
+
const MERCURY_LEAN_ADDRESS = MERCURY_ADDRESS.rule
|
|
550
|
+
.replace("as an array of three strings", "comma-separated")
|
|
551
|
+
.replace(" and they need not repeat the title", "");
|
|
552
|
+
export const MERCURY_ONECALL_CLASSIFY = `Your reply also classifies the person's request, in its own keys, and every part of the brief follows that classification.
|
|
553
|
+
|
|
554
|
+
archetype, exactly one:
|
|
555
|
+
${MERCURY_SHAPE_ARCHETYPE_TEXT}
|
|
556
|
+
|
|
557
|
+
coreAction: the one thing the person does most in this app, as a short verb phrase in their own words ("tap once per glass", "paste the group chat", "lasso cows"). It becomes the biggest control on the first screen.
|
|
558
|
+
recordNames: 1-5 short singular record type names the app stores (toys may have just "score" or none).
|
|
559
|
+
capabilities: an array naming each capability the request itself needs, from this list; empty when it needs none:
|
|
560
|
+
${MERCURY_CAPABILITY_TRIGGERS}
|
|
561
|
+
|
|
562
|
+
The brief follows the classification: the access part matches the archetype; the demo names the records; with no ai capability the app works from its own records and no part mentions AI or callAI; with composer, the records come in through a sentence composer (Add and Ask), named as the front door with its mechanics left to the builder; a toy with record names saves best scores or a save slot per signed-in player and nothing else, and a toy with none keeps its state in memory and its access part says it opens with no sign-in and no records.`;
|
|
563
|
+
export function mercuryBriefRules(o = {}) {
|
|
564
|
+
const own = o.juice === "own-world";
|
|
565
|
+
const floors = own ? MERCURY_OWN_WORLD_BRIEF_FLOORS : MERCURY_BRIEF_FLOORS;
|
|
566
|
+
const rules = o.recordsRules === true
|
|
567
|
+
? recordsBriefRulesText()
|
|
568
|
+
: briefRulesText(own ? MERCURY_OWN_WORLD_DEMO_RULE : MERCURY_DEMO_RULE, o.depth === "sub-feature" ? `${floors} ${MERCURY_DEPTH_RULE}` : floors);
|
|
569
|
+
if (o.reply !== "whole")
|
|
570
|
+
return rules;
|
|
571
|
+
const story = mercuryStoryRule(mercuryStoryExample(o.personWords ?? ""));
|
|
572
|
+
return rules.replace(ACCESS_SENTENCE, `${story} ${MERCURY_WHOLE_PREAMBLE_RULE} ${ACCESS_SENTENCE}`);
|
|
573
|
+
}
|
|
574
|
+
function withScreensKey(obj, value) {
|
|
575
|
+
const keys = Object.keys(obj);
|
|
576
|
+
const after = keys.includes("access") ? keys.indexOf("access") + 1 : keys.includes("opening") ? keys.indexOf("opening") + 1 : 0;
|
|
577
|
+
const out = {};
|
|
578
|
+
keys.slice(0, after).forEach((k) => (out[k] = obj[k]));
|
|
579
|
+
out.screens = value;
|
|
580
|
+
keys.slice(after).forEach((k) => (out[k] = obj[k]));
|
|
581
|
+
return out;
|
|
582
|
+
}
|
|
583
|
+
function withRecordsKeys(obj, records, rules) {
|
|
584
|
+
const keys = Object.keys(obj);
|
|
585
|
+
const before = keys.indexOf("screens");
|
|
586
|
+
const at = before >= 0
|
|
587
|
+
? before
|
|
588
|
+
: keys.includes("access")
|
|
589
|
+
? keys.indexOf("access") + 1
|
|
590
|
+
: keys.includes("opening")
|
|
591
|
+
? keys.indexOf("opening") + 1
|
|
592
|
+
: 0;
|
|
593
|
+
const out = {};
|
|
594
|
+
keys.slice(0, at).forEach((k) => (out[k] = obj[k]));
|
|
595
|
+
out.records = records;
|
|
596
|
+
out.rules = rules;
|
|
597
|
+
keys.slice(at).forEach((k) => (out[k] = obj[k]));
|
|
598
|
+
return out;
|
|
599
|
+
}
|
|
600
|
+
function withWholeKeys(obj, extra) {
|
|
601
|
+
const settings = ["look", ...Object.keys(MERCURY_ADDRESS.properties), "icon"];
|
|
602
|
+
const keys = Object.keys(obj);
|
|
603
|
+
const found = keys.findIndex((k) => settings.includes(k));
|
|
604
|
+
const at = found < 0 ? keys.length : found;
|
|
605
|
+
const out = {};
|
|
606
|
+
keys.slice(0, at).forEach((k) => (out[k] = obj[k]));
|
|
607
|
+
Object.entries(extra).forEach(([k, v]) => (out[k] = v));
|
|
608
|
+
keys.slice(at).forEach((k) => (out[k] = obj[k]));
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
export const MERCURY_CHOPPED_BRIEF_RULES = `These parts are turned up, in the person's own nouns, at the field's own lengths:
|
|
612
|
+
- opening: reads like you can already see the app — what it is, who it is for, and the one moment that makes it fun to open.
|
|
613
|
+
- seed: a small world in words — a lively cast with quoted names and where each one stands (one leading, one close behind, one just joined), what happened just now and last week, and a couple of records in the interesting states the screens would show (one overdue, one sold out, one on a streak). Every seeded record is one the app's own Add could write.
|
|
614
|
+
- look: a scene — a surface, a palette, a card shape and one gesture of mood — in under twenty words.
|
|
615
|
+
The access part, the title and the slugs stay as plain and exact as the field shows them. A vivid phrase belongs to this app or it stays out; every part describes the app the person asked for, and each part grows by a few words, never by a feature their words did not imply.`;
|
|
616
|
+
export const MERCURY_CHOPPED_SEED_LINE = `- seed: a small world in words — a lively cast with quoted names and where each one stands (one leading, one close behind, one just joined), what happened just now and last week, and a couple of records in the interesting states the screens would show (one overdue, one sold out, one on a streak). Every seeded record is one the app's own Add could write.`;
|
|
617
|
+
export const MERCURY_CHOPPED_OWN_WORLD_SEED_LINE = `- seed: a small world in words — a lively cast with quoted names and where each one stands in the app's own world, what happened just now and last week, and a couple of records in the interesting states this app's own records hold. Every seeded record is one the app's own Add could write.`;
|
|
618
|
+
export const MERCURY_CHOPPED_OWN_WORLD_BRIEF_RULES = MERCURY_CHOPPED_BRIEF_RULES.replace(MERCURY_CHOPPED_SEED_LINE, MERCURY_CHOPPED_OWN_WORLD_SEED_LINE);
|
|
619
|
+
export const MERCURY_CHOPPED_RECORDS_BRIEF_RULES = MERCURY_CHOPPED_BRIEF_RULES.replace(MERCURY_CHOPPED_SEED_LINE, MERCURY_CHOPPED_RECORDS_SEED_LINE);
|
|
620
|
+
export const MERCURY_ONECALL_CLASSIFY_RECORDS = MERCURY_ONECALL_CLASSIFY.replace(MERCURY_FIRST_PERSON_TRIGGER, `${MERCURY_FIRST_PERSON_TRIGGER}\n${MERCURY_BLOCK_WORLD_TRIGGER}`);
|
|
621
|
+
export const MERCURY_CHOPPED_PREAMBLE = `Make it vivid, like you can already see the app: name the first screen's hero control and the one moment that makes it fun to open, in their nouns. The promise stays the brief's: the core action and that control.`;
|
|
232
622
|
const FIELD_BOUNDS = {
|
|
233
623
|
opening: [15, 200],
|
|
234
624
|
access: [60, 600],
|
|
@@ -236,25 +626,37 @@ const FIELD_BOUNDS = {
|
|
|
236
626
|
look: [15, 150],
|
|
237
627
|
icon: [8, 100],
|
|
238
628
|
};
|
|
239
|
-
|
|
629
|
+
const MERCURY_CLASSIFY_PROPERTIES = {
|
|
630
|
+
archetype: { type: "string", enum: [...MERCURY_ARCHETYPES] },
|
|
631
|
+
coreAction: { type: "string" },
|
|
632
|
+
recordNames: { type: "array", items: { type: "string" } },
|
|
633
|
+
capabilities: { type: "array", items: { type: "string", enum: [...MERCURY_FLAGS] } },
|
|
634
|
+
};
|
|
635
|
+
export function mercuryBriefKeys(fields) {
|
|
636
|
+
return fields.flatMap((f) => (f === "pairs" ? Object.keys(MERCURY_ADDRESS.properties) : [replyKey(f)]));
|
|
637
|
+
}
|
|
638
|
+
export function mercuryBriefSchema(fields, opts = {}) {
|
|
639
|
+
const rooms = opts.rooms === true;
|
|
640
|
+
const font = opts.font === true;
|
|
641
|
+
const fieldProps = Object.fromEntries(fields.flatMap((f) => (f === "pairs" ? Object.entries(MERCURY_ADDRESS.properties) : [[replyKey(f), { type: "string" }]])));
|
|
642
|
+
const screened = opts.screens === true ? withScreensKey(fieldProps, { type: "string" }) : fieldProps;
|
|
643
|
+
const outline = opts.recordsRules === true ? withRecordsKeys(screened, { type: "string" }, { type: "string" }) : screened;
|
|
644
|
+
const props = opts.story === true || opts.preamble === true
|
|
645
|
+
? withWholeKeys(outline, {
|
|
646
|
+
...(opts.story === true ? { story: { type: "string" } } : {}),
|
|
647
|
+
...(opts.preamble === true ? { preamble: { type: "string" } } : {}),
|
|
648
|
+
})
|
|
649
|
+
: outline;
|
|
650
|
+
const lead = withClassifyFirst(props, opts.classify === true, (k) => MERCURY_CLASSIFY_PROPERTIES[k]);
|
|
240
651
|
return {
|
|
241
652
|
type: "object",
|
|
242
653
|
additionalProperties: false,
|
|
243
|
-
required: [...
|
|
244
|
-
properties:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
items: {
|
|
250
|
-
type: "object",
|
|
251
|
-
additionalProperties: false,
|
|
252
|
-
required: ["title", "slug"],
|
|
253
|
-
properties: { title: { type: "string" }, slug: { type: "string" } },
|
|
254
|
-
},
|
|
255
|
-
}
|
|
256
|
-
: { type: "string" },
|
|
257
|
-
])),
|
|
654
|
+
required: [...Object.keys(lead), ...(rooms ? ["rooms"] : []), ...(font ? ["font"] : [])],
|
|
655
|
+
properties: {
|
|
656
|
+
...lead,
|
|
657
|
+
...(rooms ? { rooms: { type: "array", items: { type: "string" } } } : {}),
|
|
658
|
+
...(font ? { font: { type: "string" } } : {}),
|
|
659
|
+
},
|
|
258
660
|
};
|
|
259
661
|
}
|
|
260
662
|
function capabilityDecisions(shape) {
|
|
@@ -269,7 +671,83 @@ function capabilityDecisions(shape) {
|
|
|
269
671
|
];
|
|
270
672
|
}
|
|
271
673
|
export function mercuryBriefMessages(o) {
|
|
272
|
-
|
|
674
|
+
return briefMessages(o, { shape: o.shape });
|
|
675
|
+
}
|
|
676
|
+
export function mercuryOneCallBriefMessages(o) {
|
|
677
|
+
return briefMessages(o, { draft: o.draft });
|
|
678
|
+
}
|
|
679
|
+
function briefMessages(o, by) {
|
|
680
|
+
const s = "shape" in by ? by.shape : undefined;
|
|
681
|
+
const classify = s === undefined;
|
|
682
|
+
const rooms = o.rooms === true;
|
|
683
|
+
const font = o.font === true;
|
|
684
|
+
const screens = o.screens ?? o.depth === "sub-feature";
|
|
685
|
+
const story = o.story ?? o.reply === "whole";
|
|
686
|
+
const preamble = o.preamble ?? o.reply === "whole";
|
|
687
|
+
const recordsRules = o.recordsRules === true;
|
|
688
|
+
const recordsOwed = recordsRules && (o.recordsOwed ?? true);
|
|
689
|
+
const keys = mercuryBriefKeys(o.fields);
|
|
690
|
+
const keyed = Object.fromEntries(keys.map((k) => [k, k]));
|
|
691
|
+
const screenedKeys = screens ? withScreensKey(keyed, "screens") : keyed;
|
|
692
|
+
const outlineKeys = recordsOwed ? withRecordsKeys(screenedKeys, "records", "rules") : screenedKeys;
|
|
693
|
+
const askedKeys = Object.keys(withClassifyFirst(story || preamble
|
|
694
|
+
? withWholeKeys(outlineKeys, { ...(story ? { story: "story" } : {}), ...(preamble ? { preamble: "preamble" } : {}) })
|
|
695
|
+
: outlineKeys, classify, (k) => k));
|
|
696
|
+
const skeleton = [
|
|
697
|
+
"Answer in exactly this shape, each value replaced by the real text:",
|
|
698
|
+
mercuryAnswerSkeleton(o.fields, {
|
|
699
|
+
rooms,
|
|
700
|
+
font,
|
|
701
|
+
...(screens ? { screens } : {}),
|
|
702
|
+
...(o.juice === undefined ? {} : { juice: o.juice }),
|
|
703
|
+
...(story ? { story } : {}),
|
|
704
|
+
...(preamble ? { preamble } : {}),
|
|
705
|
+
...(classify ? { classify } : {}),
|
|
706
|
+
...(recordsOwed ? { recordsRules: true } : {}),
|
|
707
|
+
}),
|
|
708
|
+
...(rooms ? ["", `Rooms: ${poufRoomCatalog()}`] : []),
|
|
709
|
+
...(font ? ["", `Fonts: ${vibeFontCatalog()}`] : []),
|
|
710
|
+
"",
|
|
711
|
+
`Person's words: "${o.personWords}"`,
|
|
712
|
+
"",
|
|
713
|
+
];
|
|
714
|
+
const also = (rooms ? ` ${MERCURY_ROOMS_ASK}` : "") + (font ? ` ${MERCURY_FONT_ASK}` : "");
|
|
715
|
+
const slugWords = o.fields.includes("pairs") && o.words.length > 0 ? [mercurySlugWordsSentence(o.words)] : [];
|
|
716
|
+
const chopped = recordsRules
|
|
717
|
+
? MERCURY_CHOPPED_RECORDS_BRIEF_RULES
|
|
718
|
+
: o.juice === "own-world"
|
|
719
|
+
? MERCURY_CHOPPED_OWN_WORLD_BRIEF_RULES
|
|
720
|
+
: MERCURY_CHOPPED_BRIEF_RULES;
|
|
721
|
+
if (s === undefined) {
|
|
722
|
+
const notes = [
|
|
723
|
+
...(o.fields.includes("pairs") ? [MERCURY_ADDRESS.ask] : []),
|
|
724
|
+
`archetype: one of ${MERCURY_ARCHETYPES.join(", ")}`,
|
|
725
|
+
"recordNames: an array of record names",
|
|
726
|
+
"capabilities: an array from the capability list, empty when none",
|
|
727
|
+
];
|
|
728
|
+
const user = [
|
|
729
|
+
...skeleton,
|
|
730
|
+
`Classify the person's words, then derive this app the way the example with that archetype derives its own. Return JSON with these keys: ${askedKeys.join(", ")} (${notes.join("; ")}).${also}`,
|
|
731
|
+
...slugWords,
|
|
732
|
+
].join("\n");
|
|
733
|
+
const rules = mercuryBriefRules(o).replace(MERCURY_DECIDED_SENTENCE, MERCURY_ONECALL_DECIDED);
|
|
734
|
+
const field = mercuryOneCallField("draft" in by ? by.draft : 0, MERCURY_ADDRESS, recordsRules);
|
|
735
|
+
const classifyBlock = recordsRules ? MERCURY_ONECALL_CLASSIFY_RECORDS : MERCURY_ONECALL_CLASSIFY;
|
|
736
|
+
return [
|
|
737
|
+
{
|
|
738
|
+
role: "system",
|
|
739
|
+
content: [
|
|
740
|
+
{
|
|
741
|
+
type: "text",
|
|
742
|
+
text: o.style === "chopped"
|
|
743
|
+
? `${rules}\n\n${classifyBlock}\n\n${chopped}\n\n${field}`
|
|
744
|
+
: `${rules}\n\n${classifyBlock}\n\n${field}`,
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
},
|
|
748
|
+
{ role: "user", content: [{ type: "text", text: user }] },
|
|
749
|
+
];
|
|
750
|
+
}
|
|
273
751
|
const decisions = [
|
|
274
752
|
`archetype: ${s.archetype}`,
|
|
275
753
|
`core action: ${s.coreAction}`,
|
|
@@ -284,20 +762,28 @@ export function mercuryBriefMessages(o) {
|
|
|
284
762
|
: []),
|
|
285
763
|
];
|
|
286
764
|
const user = [
|
|
287
|
-
|
|
288
|
-
`Today is ${o.today}.`,
|
|
289
|
-
"",
|
|
765
|
+
...skeleton,
|
|
290
766
|
"Decisions:",
|
|
291
767
|
...decisions.map((d) => `- ${d}`),
|
|
292
768
|
"",
|
|
293
|
-
`
|
|
294
|
-
(o.fields.includes("pairs") ?
|
|
295
|
-
"."
|
|
296
|
-
|
|
297
|
-
|
|
769
|
+
`Derive this app the way the example tagged [${s.archetype}] derives its own, from the person's words. Return JSON with these keys: ${askedKeys.join(", ")}` +
|
|
770
|
+
(o.fields.includes("pairs") ? ` (${MERCURY_ADDRESS.ask})` : "") +
|
|
771
|
+
"." +
|
|
772
|
+
also,
|
|
773
|
+
...slugWords,
|
|
298
774
|
].join("\n");
|
|
299
775
|
return [
|
|
300
|
-
{
|
|
776
|
+
{
|
|
777
|
+
role: "system",
|
|
778
|
+
content: [
|
|
779
|
+
{
|
|
780
|
+
type: "text",
|
|
781
|
+
text: o.style === "chopped"
|
|
782
|
+
? `${mercuryBriefRules(o)}\n\n${chopped}\n\n${mercuryExemplarField(s.archetype, MERCURY_ADDRESS, recordsRules)}`
|
|
783
|
+
: `${mercuryBriefRules(o)}\n\n${mercuryExemplarField(s.archetype, MERCURY_ADDRESS, recordsRules)}`,
|
|
784
|
+
},
|
|
785
|
+
],
|
|
786
|
+
},
|
|
301
787
|
{ role: "user", content: [{ type: "text", text: user }] },
|
|
302
788
|
];
|
|
303
789
|
}
|
|
@@ -321,37 +807,64 @@ function mentions(name, text) {
|
|
|
321
807
|
return stem(name).every((w) => lower.includes(w));
|
|
322
808
|
}
|
|
323
809
|
const TAG_LEAK = /\[(personal|group|publish|intake|open|toy)\]|archetype/i;
|
|
324
|
-
const NONCE_LEAK = /variant|#\d{4}/i;
|
|
325
810
|
const MARKUP = /```|^\s*#/m;
|
|
326
811
|
const AI_LEAK = /\bcallai\b|\bAI\b|language model/;
|
|
327
|
-
const pairShape = lazyType(() => type({ title: "string", slug: "string" }).array());
|
|
328
812
|
function kebab(s) {
|
|
329
813
|
return s
|
|
330
814
|
.toLowerCase()
|
|
331
815
|
.replace(/[^a-z0-9]+/g, "-")
|
|
332
816
|
.replace(/^-+|-+$/g, "");
|
|
333
817
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return
|
|
818
|
+
const LEADING_TAG = /^\s*\[(?:personal|group|publish|intake|open|toy)\]\s*/i;
|
|
819
|
+
const DEMO_PREFIX = /^demo\s*:\s*/i;
|
|
820
|
+
export function stripMercuryArchetypeTag(text) {
|
|
821
|
+
return LEADING_TAG.test(text) ? { text: text.replace(LEADING_TAG, ""), repaired: true } : { text, repaired: false };
|
|
822
|
+
}
|
|
823
|
+
export function repairMercuryField(field, value) {
|
|
824
|
+
const repaired = new Set();
|
|
825
|
+
const lead = stripMercuryArchetypeTag(cleanMercuryText(value));
|
|
826
|
+
if (lead.repaired)
|
|
827
|
+
repaired.add("archetype-tag");
|
|
828
|
+
let t = lead.text.trim();
|
|
829
|
+
if (field === "seed" && t.length > 0) {
|
|
830
|
+
const prefix = DEMO_PREFIX.exec(t);
|
|
831
|
+
const rest = stripMercuryArchetypeTag(prefix === null ? t : t.slice(prefix[0].length));
|
|
832
|
+
if (rest.repaired)
|
|
833
|
+
repaired.add("archetype-tag");
|
|
834
|
+
if (prefix === null)
|
|
835
|
+
repaired.add("demo-prefix");
|
|
836
|
+
t = `demo: ${rest.text.trim()}`;
|
|
837
|
+
}
|
|
838
|
+
return { value: t, repaired: [...repaired] };
|
|
839
|
+
}
|
|
840
|
+
export function prepareMercuryField(field, value) {
|
|
841
|
+
const r = repairMercuryField(field, value);
|
|
842
|
+
return { gate: r.value, value: cleanMercuryText(value), misses: r.repaired };
|
|
843
|
+
}
|
|
844
|
+
export function gateMercuryAddress(o) {
|
|
845
|
+
const raw = o.title.trim();
|
|
846
|
+
const cut = stripMercuryArchetypeTag(raw);
|
|
847
|
+
const title = cut.text.trim().replace(/^["*]+|["*]+$/g, "");
|
|
848
|
+
const own = kebab(title);
|
|
849
|
+
if (title.length === 0 || own.length === 0)
|
|
850
|
+
return Result.Err("title-empty");
|
|
851
|
+
const words = title.split(/\s+/).length;
|
|
852
|
+
if (words < 1 || words > 4)
|
|
853
|
+
return Result.Err("title-length");
|
|
854
|
+
const sent = cut.repaired ? raw : title;
|
|
855
|
+
const slugs = o.slugs.map(kebab);
|
|
856
|
+
if (slugs.every((slug) => slug.length === 0))
|
|
857
|
+
return Result.Ok({ pairs: [{ title: sent, slug: own }], fallback: true, tagged: cut.repaired });
|
|
858
|
+
const filled = slugs.map((slug) => (slug.length > 0 ? slug : own));
|
|
859
|
+
if (filled.length !== 3)
|
|
860
|
+
return Result.Err("slugs-count");
|
|
861
|
+
if (new Set(filled).size !== 3)
|
|
862
|
+
return Result.Err("slugs-duplicate");
|
|
863
|
+
return Result.Ok({ pairs: filled.map((slug) => ({ title: sent, slug })), fallback: false, tagged: cut.repaired });
|
|
864
|
+
}
|
|
865
|
+
export function mercurySlugsWithWord(pairs, words) {
|
|
866
|
+
const ws = words.map((w) => kebab(w)).filter((w) => w.length > 0);
|
|
867
|
+
return pairs.filter((p) => ws.some((w) => p.slug.includes(w))).length;
|
|
355
868
|
}
|
|
356
869
|
export function gateMercuryField(o) {
|
|
357
870
|
const t = cleanMercuryText(o.value);
|
|
@@ -366,16 +879,14 @@ export function gateMercuryField(o) {
|
|
|
366
879
|
fails.push("short");
|
|
367
880
|
else if (t.length > hi)
|
|
368
881
|
fails.push("long");
|
|
369
|
-
if (NONCE_LEAK.test(t))
|
|
370
|
-
fails.push("nonce");
|
|
371
882
|
if (MARKUP.test(t))
|
|
372
883
|
fails.push("markup");
|
|
373
884
|
if (TAG_LEAK.test(t))
|
|
374
885
|
fails.push("tag-leak");
|
|
375
886
|
if (o.field !== "look" && o.field !== "icon" && o.shape.flags.ai === false && AI_LEAK.test(t))
|
|
376
887
|
fails.push("ai-leak");
|
|
377
|
-
if (o.field === "seed" &&
|
|
378
|
-
fails.push("
|
|
888
|
+
if (o.field === "seed" && DEMO_PREFIX.test(t) === false)
|
|
889
|
+
fails.push("demo-prefix");
|
|
379
890
|
if (o.field === "seed" && o.shape.recordNames.length > 0 && o.shape.recordNames.some((r) => mentions(r, t)) === false)
|
|
380
891
|
fails.push("seed-no-record");
|
|
381
892
|
if (o.field === "access") {
|
|
@@ -430,14 +941,70 @@ const FLAG_SKILLS = [
|
|
|
430
941
|
["charts", "d3"],
|
|
431
942
|
["boardGame", "board-games"],
|
|
432
943
|
];
|
|
433
|
-
export function mercurySkills(shape) {
|
|
434
|
-
|
|
944
|
+
export function mercurySkills(shape, opts = {}) {
|
|
945
|
+
const skills = ["access", "fireproof", ...FLAG_SKILLS.filter(([f]) => shape.flags[f]).map(([, skill]) => skill)];
|
|
946
|
+
if (opts.recordsRules !== true || !mercuryBlockWorldNamed(opts.personWords ?? ""))
|
|
947
|
+
return skills;
|
|
948
|
+
return [...skills.filter((k) => k !== "three-js"), "voxel", "three-js"];
|
|
949
|
+
}
|
|
950
|
+
const POSTURE_PUBLIC = /\b(anyone with the link|everyone with the link|signed in or not|any visitor|readable by anyone|public(ly)?)\b/i;
|
|
951
|
+
const POSTURE_ALONE = /\b(alone|no one else|nobody else|only (the owner|the creator|you|me) (can )?(read|see)s?)\b/i;
|
|
952
|
+
const POSTURE_MEMBERS = /\b(members?|invit\w*|let in|HandleInput|friends?|households?|travel+ers?|guests?|players?|teammates?)\b/i;
|
|
953
|
+
const POSTURE_WRITE_VERB = /\b(edit|edits|write|writes|add|adds|change|changes|update|updates|post|posts|submit|submits|book|books|log|logs|place|places|remove|removes)\b/i;
|
|
954
|
+
const POSTURE_OTHERS = /\b(members?|everyone|anyone|friends?|guests?|visitors?|others|travel+ers?|households?|players?|teammates?|each (poster|player|member|guest|visitor))\b/i;
|
|
955
|
+
const POSTURE_OWNER_ONLY = /\b(alone|only the (owner|creator)|the (owner|creator) (alone )?(writes|edits))\b/i;
|
|
956
|
+
const POSTURE_OTHERS_READ = /\b(members?|everyone|anyone|friends?|guests?|visitors?|they|others)\b[^.;]*\b(read|reads|see|sees|view|views)\b/i;
|
|
957
|
+
export function mercuryAccessPosture(access) {
|
|
958
|
+
const read = POSTURE_PUBLIC.test(access)
|
|
959
|
+
? "public"
|
|
960
|
+
: POSTURE_ALONE.test(access)
|
|
961
|
+
? "private"
|
|
962
|
+
: POSTURE_MEMBERS.test(access)
|
|
963
|
+
? "members"
|
|
964
|
+
: undefined;
|
|
965
|
+
let othersWrite = false;
|
|
966
|
+
let ownerWrites = false;
|
|
967
|
+
for (const sentence of access.split(/(?<=[.;])\s+/)) {
|
|
968
|
+
if (!POSTURE_WRITE_VERB.test(sentence))
|
|
969
|
+
continue;
|
|
970
|
+
if (POSTURE_OWNER_ONLY.test(sentence) && !POSTURE_OTHERS.test(sentence))
|
|
971
|
+
ownerWrites = true;
|
|
972
|
+
else if (POSTURE_OTHERS.test(sentence))
|
|
973
|
+
othersWrite = true;
|
|
974
|
+
else if (/\b(owner|creator)\b/i.test(sentence))
|
|
975
|
+
ownerWrites = true;
|
|
976
|
+
}
|
|
977
|
+
const write = othersWrite
|
|
978
|
+
? "members"
|
|
979
|
+
: ownerWrites || POSTURE_OTHERS_READ.test(access)
|
|
980
|
+
? "owner"
|
|
981
|
+
: undefined;
|
|
982
|
+
return { ...(read === undefined ? {} : { read }), ...(write === undefined ? {} : { write }) };
|
|
983
|
+
}
|
|
984
|
+
export function mercuryDuoConflict(a, b, personWords) {
|
|
985
|
+
if (a.shape.archetype !== b.shape.archetype)
|
|
986
|
+
return `archetype: ${a.shape.archetype} vs ${b.shape.archetype}`;
|
|
987
|
+
const na = mercuryNature(a.shape, personWords);
|
|
988
|
+
const nb = mercuryNature(b.shape, personWords);
|
|
989
|
+
if (na !== nb)
|
|
990
|
+
return `nature: ${na} vs ${nb}`;
|
|
991
|
+
const pa = mercuryAccessPosture(a.access);
|
|
992
|
+
const pb = mercuryAccessPosture(b.access);
|
|
993
|
+
if (pa.read !== undefined && pb.read !== undefined && pa.read !== pb.read)
|
|
994
|
+
return `access-read: ${pa.read} vs ${pb.read}`;
|
|
995
|
+
if (pa.write !== undefined && pb.write !== undefined && pa.write !== pb.write)
|
|
996
|
+
return `access-write: ${pa.write} vs ${pb.write}`;
|
|
997
|
+
return undefined;
|
|
435
998
|
}
|
|
436
|
-
export function mercuryNature(shape) {
|
|
999
|
+
export function mercuryNature(shape, personWords) {
|
|
1000
|
+
if (shape.keepsToThemselves === true)
|
|
1001
|
+
return "sensitive";
|
|
1002
|
+
if (PRIVATE_WORDS.test(personWords.toLowerCase()))
|
|
1003
|
+
return "sensitive";
|
|
437
1004
|
const a = shape.archetype;
|
|
438
1005
|
if (a === "open" || a === "toy" || a === "publish")
|
|
439
1006
|
return "open";
|
|
440
|
-
return shape.flags.sensitive ? "sensitive" : "collaborative";
|
|
1007
|
+
return shape.flags.sensitive && (a === "personal" || a === "intake") ? "sensitive" : "collaborative";
|
|
441
1008
|
}
|
|
442
1009
|
export function mercuryWorkflowLine(shape) {
|
|
443
1010
|
const caps = CAPABILITY_NAMES.filter(([f]) => shape.flags[f]).map(([, name]) => name);
|
|
@@ -448,30 +1015,134 @@ export function mercuryWorkflowLine(shape) {
|
|
|
448
1015
|
...(caps.length === 0 ? [] : [`Capabilities: ${caps.join(", ")}.`]),
|
|
449
1016
|
].join(" ");
|
|
450
1017
|
}
|
|
451
|
-
|
|
1018
|
+
const MERCURY_ROOMS_MAX = 3;
|
|
1019
|
+
export function gateMercuryRooms(raw) {
|
|
1020
|
+
if (!Array.isArray(raw))
|
|
1021
|
+
return { dropped: raw === undefined ? 0 : 1 };
|
|
1022
|
+
const valid = poufRoomSlugs();
|
|
1023
|
+
const kept = [];
|
|
1024
|
+
for (const r of raw) {
|
|
1025
|
+
if (typeof r !== "string" || !valid.includes(r) || kept.includes(r) || kept.length === MERCURY_ROOMS_MAX)
|
|
1026
|
+
continue;
|
|
1027
|
+
kept.push(r);
|
|
1028
|
+
}
|
|
1029
|
+
const dropped = raw.length - kept.length;
|
|
1030
|
+
return kept.length === 0 ? { dropped } : { rooms: kept, dropped };
|
|
1031
|
+
}
|
|
1032
|
+
export function gateMercuryFont(raw) {
|
|
1033
|
+
if (raw === undefined)
|
|
1034
|
+
return { dropped: 0 };
|
|
1035
|
+
return typeof raw === "string" && vibeFontSlugs().includes(raw) ? { font: raw, dropped: 0 } : { dropped: 1 };
|
|
1036
|
+
}
|
|
1037
|
+
export function gateMercuryScreens(raw) {
|
|
1038
|
+
if (raw === undefined)
|
|
1039
|
+
return { dropped: 0 };
|
|
1040
|
+
return typeof raw === "string" && cleanMercuryText(raw).length > 0 ? { screens: raw, dropped: 0 } : { dropped: 1 };
|
|
1041
|
+
}
|
|
1042
|
+
function mercuryScreensLine(screens) {
|
|
1043
|
+
const t = cleanMercuryText(screens ?? "");
|
|
1044
|
+
if (t.length === 0)
|
|
1045
|
+
return "";
|
|
1046
|
+
return /^screen/i.test(t) ? t : `Screens: ${t}`;
|
|
1047
|
+
}
|
|
1048
|
+
function mercuryPartLine(label, text) {
|
|
1049
|
+
const t = cleanMercuryText(text ?? "").replace(/^(records?|rules?)\s*:\s*/i, "");
|
|
1050
|
+
return t.length === 0 ? "" : `${label}: ${t}`;
|
|
1051
|
+
}
|
|
1052
|
+
const WHOLE_MARKUP = /```|^\s*#/m;
|
|
1053
|
+
export function gateMercuryStory(raw) {
|
|
1054
|
+
if (raw === undefined)
|
|
1055
|
+
return { dropped: 0 };
|
|
1056
|
+
if (typeof raw !== "string")
|
|
1057
|
+
return { dropped: 1 };
|
|
1058
|
+
const t = cleanMercuryText(raw);
|
|
1059
|
+
if (t.length === 0)
|
|
1060
|
+
return { dropped: 1 };
|
|
1061
|
+
return WHOLE_MARKUP.test(t) ? { story: raw, dropped: 0, misses: ["markup"] } : { story: raw, dropped: 0 };
|
|
1062
|
+
}
|
|
1063
|
+
export function gateMercuryWholePreamble(text) {
|
|
1064
|
+
const t = cleanMercuryText(text);
|
|
1065
|
+
const fails = [];
|
|
1066
|
+
if (t.length === 0)
|
|
1067
|
+
fails.push("empty");
|
|
1068
|
+
if (/```|^\s*#|\bbrief\b|\bdemo\s*:/im.test(t))
|
|
1069
|
+
fails.push("markup");
|
|
1070
|
+
return fails;
|
|
1071
|
+
}
|
|
1072
|
+
function mercuryEnrichedPrompt(o) {
|
|
1073
|
+
if (o.reply === "whole")
|
|
1074
|
+
return mercuryWholeEnrichedPrompt(o);
|
|
452
1075
|
const s = o.shape;
|
|
453
1076
|
const keepsNothing = s.archetype === "toy" && s.recordNames.length === 0;
|
|
454
1077
|
const access = cleanMercuryText(o.fields.access);
|
|
455
1078
|
const seed = keepsNothing ? "" : cleanMercuryText(o.fields.seed);
|
|
456
|
-
|
|
1079
|
+
return [
|
|
457
1080
|
cleanMercuryText(o.fields.opening),
|
|
458
1081
|
keepsNothing && access.length === 0 ? "" : mercuryAccessLine(s, access),
|
|
459
|
-
|
|
1082
|
+
s.keepsToThemselves === true ? ONE_TREE_PER_PERSON : "",
|
|
1083
|
+
mercuryWorkflowLine(o.capabilityFlags === undefined ? s : { ...s, flags: o.capabilityFlags }),
|
|
1084
|
+
mercuryScreensLine(o.screens),
|
|
460
1085
|
seed.length === 0 ? "" : `${seed} ${KNOWN_VALUES_DIRECT_CONTROLS}`,
|
|
461
1086
|
`The person asked: "${o.personWords}".`,
|
|
462
|
-
]
|
|
1087
|
+
]
|
|
1088
|
+
.filter((p) => p.length > 0)
|
|
1089
|
+
.join("\n\n");
|
|
1090
|
+
}
|
|
1091
|
+
function mercuryWholeEnrichedPrompt(o) {
|
|
1092
|
+
const own = wholeSection(o.shape, {
|
|
1093
|
+
opening: o.fields.opening,
|
|
1094
|
+
access: o.fields.access,
|
|
1095
|
+
seed: o.fields.seed,
|
|
1096
|
+
look: o.fields.look,
|
|
1097
|
+
...(o.records === undefined ? {} : { records: o.records }),
|
|
1098
|
+
...(o.rules === undefined ? {} : { rules: o.rules }),
|
|
1099
|
+
...(o.screens === undefined ? {} : { screens: o.screens }),
|
|
1100
|
+
...(o.story === undefined ? {} : { story: o.story }),
|
|
1101
|
+
...(o.preamble === undefined ? {} : { preamble: o.preamble }),
|
|
1102
|
+
});
|
|
1103
|
+
const partner = o.partner === undefined ? undefined : o.recordsRules === true ? { ...o.partner, seed: "", story: undefined } : o.partner;
|
|
1104
|
+
const takes = partner === undefined ? own : [...own, "---", ...wholeSection(o.shape, partner)];
|
|
1105
|
+
return [...takes, `The person asked: "${o.personWords}".`].join("\n\n");
|
|
1106
|
+
}
|
|
1107
|
+
function wholeSection(s, t) {
|
|
1108
|
+
const keepsNothing = s.archetype === "toy" && s.recordNames.length === 0;
|
|
1109
|
+
const access = cleanMercuryText(t.access);
|
|
1110
|
+
const seed = keepsNothing ? "" : cleanMercuryText(t.seed);
|
|
1111
|
+
return [
|
|
1112
|
+
cleanMercuryText(t.opening),
|
|
1113
|
+
keepsNothing && access.length === 0 ? "" : mercuryAccessLine(s, access),
|
|
1114
|
+
mercuryPartLine("Records", t.records),
|
|
1115
|
+
mercuryPartLine("Rules", t.rules),
|
|
1116
|
+
mercuryScreensLine(t.screens),
|
|
1117
|
+
seed.length === 0 ? "" : `${seed} ${KNOWN_VALUES_DIRECT_CONTROLS}`,
|
|
1118
|
+
cleanMercuryText(t.story ?? ""),
|
|
1119
|
+
cleanMercuryText(t.preamble ?? ""),
|
|
1120
|
+
cleanMercuryText(t.look),
|
|
1121
|
+
].filter((p) => p.length > 0);
|
|
1122
|
+
}
|
|
1123
|
+
export function mercuryPreambleBrief(o) {
|
|
1124
|
+
return mercuryEnrichedPrompt(o);
|
|
1125
|
+
}
|
|
1126
|
+
export function assembleMercuryCreateVibe(o) {
|
|
1127
|
+
const s = o.shape;
|
|
463
1128
|
const look = cleanMercuryText(o.fields.look);
|
|
464
1129
|
const icon = cleanMercuryText(o.fields.icon);
|
|
465
|
-
const nature = mercuryNature(s);
|
|
1130
|
+
const nature = mercuryNature(s, o.personWords);
|
|
1131
|
+
const rooms = sanitizeRoomShortlist("pouf", o.rooms);
|
|
466
1132
|
return {
|
|
467
1133
|
prompt: o.personWords,
|
|
468
|
-
enrichedPrompt:
|
|
469
|
-
skills: mercurySkills(s
|
|
1134
|
+
enrichedPrompt: mercuryEnrichedPrompt(o),
|
|
1135
|
+
skills: mercurySkills(o.capabilityFlags === undefined ? s : { ...s, flags: o.capabilityFlags }, {
|
|
1136
|
+
personWords: o.personWords,
|
|
1137
|
+
...(o.recordsRules === true ? { recordsRules: true } : {}),
|
|
1138
|
+
}),
|
|
470
1139
|
pairs: o.fields.pairs.map((p) => ({ title: p.title, slug: p.slug })),
|
|
471
1140
|
...(icon.length === 0 ? {} : { iconDescription: icon }),
|
|
472
1141
|
...(look.length === 0 ? {} : { textureNote: look }),
|
|
473
1142
|
nature,
|
|
474
1143
|
...(nature === "sensitive" ? { privacySensitive: true } : {}),
|
|
1144
|
+
...(rooms === undefined ? {} : { rooms: [...rooms] }),
|
|
1145
|
+
...(o.font === undefined ? {} : { font: o.font }),
|
|
475
1146
|
};
|
|
476
1147
|
}
|
|
477
1148
|
export const MERCURY_PREAMBLE_SYSTEM = `You speak to the person who asked for an app, just before it is built. Write two or three plain sentences in their own words: what you are making for them, its core action, and the big control on its first screen that does it. That is the whole promise — the builder works out everything else after you speak. Start with "I'm making" or "I'm building", and write it as plain sentences to the person.`;
|
|
@@ -483,10 +1154,18 @@ export function mercuryPreambleMessages(o) {
|
|
|
483
1154
|
"What is being built:",
|
|
484
1155
|
o.enrichedPrompt,
|
|
485
1156
|
"",
|
|
486
|
-
|
|
1157
|
+
"Write the preamble.",
|
|
487
1158
|
].join("\n");
|
|
488
1159
|
return [
|
|
489
|
-
{
|
|
1160
|
+
{
|
|
1161
|
+
role: "system",
|
|
1162
|
+
content: [
|
|
1163
|
+
{
|
|
1164
|
+
type: "text",
|
|
1165
|
+
text: o.style === "chopped" ? `${MERCURY_PREAMBLE_SYSTEM} ${MERCURY_CHOPPED_PREAMBLE}` : MERCURY_PREAMBLE_SYSTEM,
|
|
1166
|
+
},
|
|
1167
|
+
],
|
|
1168
|
+
},
|
|
490
1169
|
{ role: "user", content: [{ type: "text", text: user }] },
|
|
491
1170
|
];
|
|
492
1171
|
}
|
|
@@ -508,7 +1187,7 @@ export function gateMercuryPreamble(o) {
|
|
|
508
1187
|
fails.push("short");
|
|
509
1188
|
else if (t.length > 480)
|
|
510
1189
|
fails.push("long");
|
|
511
|
-
if (
|
|
1190
|
+
if (/```|^\s*#|\bbrief\b|\bdemo\s*:/im.test(t))
|
|
512
1191
|
fails.push("markup");
|
|
513
1192
|
if ((t.match(/[.!?](\s|$)/g) ?? []).length > 3)
|
|
514
1193
|
fails.push("too-many-sentences");
|
|
@@ -520,14 +1199,24 @@ export function gateMercuryPreamble(o) {
|
|
|
520
1199
|
}
|
|
521
1200
|
const completionBody = lazyType(() => type({
|
|
522
1201
|
"model?": "string",
|
|
523
|
-
choices: type({ message: { content: "string | null" } }).array(),
|
|
524
|
-
"usage?": {
|
|
1202
|
+
choices: type({ message: { content: "string | null" }, "finish_reason?": "string | null" }).array(),
|
|
1203
|
+
"usage?": {
|
|
1204
|
+
prompt_tokens: "number",
|
|
1205
|
+
completion_tokens: "number",
|
|
1206
|
+
"total_tokens?": "number",
|
|
1207
|
+
"cost?": "number",
|
|
1208
|
+
"completion_tokens_details?": type({ "reasoning_tokens?": "number | null" }).or("null"),
|
|
1209
|
+
},
|
|
525
1210
|
}));
|
|
526
|
-
async function withDeadline(call, timeoutMs) {
|
|
1211
|
+
async function withDeadline(call, timeoutMs, cancel) {
|
|
527
1212
|
const abort = new AbortController();
|
|
528
1213
|
const timer = setTimeout(function expire() {
|
|
529
1214
|
abort.abort();
|
|
530
1215
|
}, timeoutMs);
|
|
1216
|
+
function cancelled() {
|
|
1217
|
+
abort.abort();
|
|
1218
|
+
}
|
|
1219
|
+
cancel?.addEventListener("abort", cancelled, { once: true });
|
|
531
1220
|
return Promise.resolve()
|
|
532
1221
|
.then(function invoke() {
|
|
533
1222
|
return call(abort.signal);
|
|
@@ -541,15 +1230,19 @@ async function withDeadline(call, timeoutMs) {
|
|
|
541
1230
|
})
|
|
542
1231
|
.finally(function disarm() {
|
|
543
1232
|
clearTimeout(timer);
|
|
1233
|
+
cancel?.removeEventListener("abort", cancelled);
|
|
544
1234
|
});
|
|
545
1235
|
}
|
|
546
1236
|
async function requestMercury(vctx, o, signal) {
|
|
1237
|
+
const model = o.model ?? plannerModel("mercury");
|
|
547
1238
|
const req = {
|
|
548
|
-
model:
|
|
1239
|
+
model: model.id,
|
|
549
1240
|
stream: false,
|
|
550
1241
|
max_tokens: o.maxTokens,
|
|
551
|
-
reasoning: { ...
|
|
552
|
-
|
|
1242
|
+
...(model.reasoning === undefined ? {} : { reasoning: { ...model.reasoning } }),
|
|
1243
|
+
...(o.schema === undefined
|
|
1244
|
+
? {}
|
|
1245
|
+
: { response_format: { type: "json_schema", json_schema: { name: o.name, strict: true, schema: o.schema } } }),
|
|
553
1246
|
messages: o.messages,
|
|
554
1247
|
};
|
|
555
1248
|
const rRes = await exception2Result(() => vctx.llmRequest({ ...req, headers: vctx.params.llm.headers }, { signal }));
|
|
@@ -560,7 +1253,7 @@ async function requestMercury(vctx, o, signal) {
|
|
|
560
1253
|
const { snippet } = await readFailureBody(res);
|
|
561
1254
|
const message = `mercury ${o.name}: status ${res.status}${snippet.length === 0 ? "" : ` ${snippet.slice(0, 200)}`}`;
|
|
562
1255
|
return MERCURY_TRANSIENT_STATUSES.has(res.status)
|
|
563
|
-
? Result.Err(new MercuryTransientError(message, parseRetryAfterMs(res.headers)))
|
|
1256
|
+
? Result.Err(new MercuryTransientError(message, res.status, parseRetryAfterMs(res.headers)))
|
|
564
1257
|
: Result.Err(message);
|
|
565
1258
|
}
|
|
566
1259
|
const rBody = await exception2Result(() => res.json());
|
|
@@ -570,14 +1263,19 @@ async function requestMercury(vctx, o, signal) {
|
|
|
570
1263
|
if (body instanceof type.errors)
|
|
571
1264
|
return Result.Err(`mercury ${o.name}: reply: ${body.summary}`);
|
|
572
1265
|
const content = body.choices[0]?.message.content;
|
|
1266
|
+
const finish = body.choices[0]?.finish_reason ?? "unknown";
|
|
1267
|
+
const ended = `(finish ${finish}, ${body.usage?.completion_tokens ?? "?"} of ${o.maxTokens} tokens)`;
|
|
573
1268
|
if (content === undefined || content === null || content.trim().length === 0)
|
|
574
|
-
return Result.Err(`mercury ${o.name}: empty reply`);
|
|
575
|
-
const rJson = exception2Result(() => JSON.parse(cleanMercuryText(content)));
|
|
1269
|
+
return Result.Err(`mercury ${o.name}: empty reply ${ended}`);
|
|
1270
|
+
const rJson = o.schema === undefined ? Result.Ok(content) : exception2Result(() => JSON.parse(cleanMercuryText(content)));
|
|
576
1271
|
if (rJson.isErr())
|
|
577
|
-
return Result.Err(`mercury ${o.name}: JSON: ${rJson.Err().message}`);
|
|
1272
|
+
return Result.Err(`mercury ${o.name}: JSON ${ended}: ${rJson.Err().message.slice(0, 80)}`);
|
|
578
1273
|
const u = body.usage;
|
|
1274
|
+
const reasoningTokens = u?.completion_tokens_details?.reasoning_tokens;
|
|
579
1275
|
return Result.Ok({
|
|
580
1276
|
json: rJson.Ok(),
|
|
1277
|
+
text: content,
|
|
1278
|
+
...(typeof reasoningTokens === "number" ? { reasoningTokens } : {}),
|
|
581
1279
|
...(body.model === undefined ? {} : { servedModel: body.model }),
|
|
582
1280
|
...(u === undefined
|
|
583
1281
|
? {}
|
|
@@ -591,85 +1289,232 @@ async function requestMercury(vctx, o, signal) {
|
|
|
591
1289
|
}),
|
|
592
1290
|
});
|
|
593
1291
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1292
|
+
function mercurySendLog() {
|
|
1293
|
+
return { sends: 0, retries: 0, history: [], cause: "budget", lastError: "no attempt", sentAt: Date.now() };
|
|
1294
|
+
}
|
|
1295
|
+
function noteSend(log, prefix = "") {
|
|
1296
|
+
log.history.push(`${prefix}${log.cause} ${Date.now() - log.sentAt}ms: ${log.lastError}`.slice(0, 200));
|
|
1297
|
+
}
|
|
1298
|
+
function isCancelled(cancel) {
|
|
1299
|
+
return cancel?.aborted === true;
|
|
1300
|
+
}
|
|
1301
|
+
function eitherSignal(a, b) {
|
|
1302
|
+
if (a === undefined)
|
|
1303
|
+
return b;
|
|
1304
|
+
if (b === undefined)
|
|
1305
|
+
return a;
|
|
1306
|
+
const both = new AbortController();
|
|
1307
|
+
function fire() {
|
|
1308
|
+
both.abort();
|
|
1309
|
+
}
|
|
1310
|
+
if (a.aborted || b.aborted)
|
|
1311
|
+
both.abort();
|
|
1312
|
+
else {
|
|
1313
|
+
a.addEventListener("abort", fire, { once: true });
|
|
1314
|
+
b.addEventListener("abort", fire, { once: true });
|
|
1315
|
+
}
|
|
1316
|
+
return both.signal;
|
|
1317
|
+
}
|
|
1318
|
+
function waitOrCancel(ms, cancel) {
|
|
1319
|
+
if (cancel === undefined)
|
|
1320
|
+
return waitMs(ms);
|
|
1321
|
+
if (cancel.aborted)
|
|
1322
|
+
return Promise.resolve();
|
|
1323
|
+
return new Promise((resolve) => {
|
|
1324
|
+
const timer = setTimeout(done, ms);
|
|
1325
|
+
function done() {
|
|
1326
|
+
clearTimeout(timer);
|
|
1327
|
+
cancel?.removeEventListener("abort", done);
|
|
1328
|
+
resolve();
|
|
1329
|
+
}
|
|
1330
|
+
cancel.addEventListener("abort", done, { once: true });
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
async function mercuryAttempt(vctx, o, log) {
|
|
1334
|
+
const cancel = eitherSignal(o.cancel, o.budget.cancel);
|
|
1335
|
+
function cancelledAttempt(inFlight) {
|
|
1336
|
+
if (o.budget.cancel?.aborted === true) {
|
|
1337
|
+
log.cause = "cancelled";
|
|
1338
|
+
log.lastError = `mercury ${o.call}: cancelled, the first build went to the agent`;
|
|
1339
|
+
}
|
|
1340
|
+
return { kind: "cancelled", inFlight };
|
|
1341
|
+
}
|
|
1342
|
+
const persistent = o.persistent === true;
|
|
1343
|
+
const maxTransientRetries = persistent ? Number.POSITIVE_INFINITY : (o.transientRetries ?? MERCURY_TRANSIENT_RETRIES);
|
|
599
1344
|
let transientRetries = 0;
|
|
600
|
-
|
|
1345
|
+
async function persistentWait(retryAfterMs) {
|
|
1346
|
+
const wait = mercuryTesterRetryWaitMs(transientRetries + 1, retryAfterMs);
|
|
1347
|
+
if (wait > o.budget.remainingMs() - MERCURY_MIN_ATTEMPT_MS) {
|
|
1348
|
+
log.lastError = `${log.lastError} (next wait ${wait}ms does not fit the stage bound)`;
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
await waitOrCancel(wait, cancel);
|
|
1352
|
+
transientRetries += 1;
|
|
1353
|
+
log.retries += 1;
|
|
1354
|
+
return true;
|
|
1355
|
+
}
|
|
1356
|
+
for (;;) {
|
|
1357
|
+
if (isCancelled(cancel))
|
|
1358
|
+
return cancelledAttempt(false);
|
|
1359
|
+
const remaining = o.budget.remainingMs();
|
|
1360
|
+
if (remaining < MERCURY_MIN_ATTEMPT_MS) {
|
|
1361
|
+
const spent = `mercury ${o.call}: stage budget spent (${o.budget.elapsedMs()}ms of ${o.budget.totalMs}ms)`;
|
|
1362
|
+
log.lastError = log.sends === 0 && log.carried !== true ? spent : `${spent} after ${log.cause}: ${log.lastError}`;
|
|
1363
|
+
log.cause = o.budget.rateLimitCapMs() === undefined ? "budget" : "rate-limited";
|
|
1364
|
+
return { kind: "budget" };
|
|
1365
|
+
}
|
|
1366
|
+
const deadline = Math.min(o.deadlineMs, remaining);
|
|
601
1367
|
const ask = o.request();
|
|
602
|
-
|
|
1368
|
+
log.sends += 1;
|
|
1369
|
+
log.sentAt = Date.now();
|
|
1370
|
+
log.inFlight = true;
|
|
1371
|
+
const r = await withDeadline((signal) => requestMercury(vctx, {
|
|
1372
|
+
name: o.call,
|
|
1373
|
+
...(ask.schema === undefined ? {} : { schema: ask.schema }),
|
|
1374
|
+
messages: ask.messages,
|
|
1375
|
+
maxTokens: o.maxTokens,
|
|
1376
|
+
...(o.model === undefined ? {} : { model: o.model }),
|
|
1377
|
+
}, signal), deadline, cancel);
|
|
1378
|
+
log.inFlight = false;
|
|
1379
|
+
if (r.timedOut && isCancelled(cancel))
|
|
1380
|
+
return cancelledAttempt(true);
|
|
603
1381
|
if (r.timedOut) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
1382
|
+
const clipped = deadline < o.deadlineMs;
|
|
1383
|
+
log.lastError = `mercury ${o.call}: timed out after ${deadline}ms${clipped ? " (stage budget)" : ""}`;
|
|
1384
|
+
log.cause = clipped ? (o.budget.rateLimitCapMs() === undefined ? "budget" : "rate-limited") : "timeout";
|
|
1385
|
+
noteSend(log, o.label);
|
|
1386
|
+
if (persistent && !clipped && (await persistentWait(undefined)))
|
|
1387
|
+
continue;
|
|
1388
|
+
return { kind: "failed" };
|
|
607
1389
|
}
|
|
608
|
-
if (r.reply.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1390
|
+
if (r.reply.isOk())
|
|
1391
|
+
return { kind: "reply", reply: r.reply.Ok() };
|
|
1392
|
+
const err = r.reply.Err();
|
|
1393
|
+
log.lastError = err.message;
|
|
1394
|
+
if (err instanceof MercuryTransientError === false) {
|
|
1395
|
+
log.cause = persistent ? "network" : "error";
|
|
1396
|
+
noteSend(log, o.label);
|
|
1397
|
+
if (persistent && (await persistentWait(undefined)))
|
|
615
1398
|
continue;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
|
|
1399
|
+
return { kind: "failed" };
|
|
1400
|
+
}
|
|
1401
|
+
log.cause = err.status === 429 ? "rate-limited" : "upstream-error";
|
|
1402
|
+
if (err.status === 429 && !persistent)
|
|
1403
|
+
o.budget.noteRateLimited();
|
|
1404
|
+
if (err.retryAfterMs !== undefined)
|
|
1405
|
+
log.retryAfterMs = Math.max(log.retryAfterMs ?? 0, err.retryAfterMs);
|
|
1406
|
+
noteSend(log, o.label);
|
|
1407
|
+
if (persistent) {
|
|
1408
|
+
if (await persistentWait(err.retryAfterMs))
|
|
1409
|
+
continue;
|
|
1410
|
+
return { kind: "stop" };
|
|
1411
|
+
}
|
|
1412
|
+
if (transientRetries >= maxTransientRetries)
|
|
1413
|
+
return { kind: "failed" };
|
|
1414
|
+
const wait = mercuryRetryWaitMs(transientRetries + 1, err.retryAfterMs);
|
|
1415
|
+
if (wait === undefined || wait > o.budget.remainingMs() - MERCURY_MIN_ATTEMPT_MS) {
|
|
1416
|
+
log.lastError = `${err.message} (retry-after ${err.retryAfterMs ?? "none"}ms; falling back inside the stage budget)`;
|
|
1417
|
+
return { kind: "stop" };
|
|
619
1418
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1419
|
+
await waitOrCancel(wait, cancel);
|
|
1420
|
+
transientRetries += 1;
|
|
1421
|
+
log.retries += 1;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function absorbReply(into, reply) {
|
|
1425
|
+
if (reply.usage !== undefined)
|
|
1426
|
+
into.usage.push(reply.usage);
|
|
1427
|
+
if (reply.servedModel !== undefined)
|
|
1428
|
+
into.servedModel = reply.servedModel;
|
|
1429
|
+
if (reply.reasoningTokens !== undefined)
|
|
1430
|
+
into.reasoningTokens = (into.reasoningTokens ?? 0) + reply.reasoningTokens;
|
|
1431
|
+
}
|
|
1432
|
+
async function mercuryCall(vctx, o) {
|
|
1433
|
+
const startedAt = Date.now();
|
|
1434
|
+
const totals = { usage: [] };
|
|
1435
|
+
const log = mercurySendLog();
|
|
1436
|
+
let attempts = 0;
|
|
1437
|
+
while (attempts < MERCURY_CALL_ATTEMPTS) {
|
|
1438
|
+
const a = await mercuryAttempt(vctx, o, log);
|
|
1439
|
+
if (a.kind === "budget" || a.kind === "stop" || a.kind === "cancelled")
|
|
1440
|
+
break;
|
|
1441
|
+
attempts += 1;
|
|
1442
|
+
if (a.kind === "failed")
|
|
1443
|
+
continue;
|
|
1444
|
+
absorbReply(totals, a.reply);
|
|
1445
|
+
const accepted = o.accept(a.reply.json);
|
|
626
1446
|
if (accepted.isOk()) {
|
|
1447
|
+
o.onCounts?.(log.sends, log.retries);
|
|
627
1448
|
await recordMercuryRow(vctx, {
|
|
628
1449
|
...o.row,
|
|
629
|
-
record: {
|
|
630
|
-
call: o.call,
|
|
631
|
-
ok: true,
|
|
632
|
-
attempts: attempt + 1,
|
|
633
|
-
elapsedMs: Date.now() - startedAt,
|
|
634
|
-
...(servedModel === undefined ? {} : { servedModel }),
|
|
635
|
-
usage,
|
|
636
|
-
},
|
|
1450
|
+
record: callRecord({ call: o.call, ok: true, attempts, startedAt, totals, log, extra: { text: a.reply.text } }),
|
|
637
1451
|
});
|
|
638
|
-
return accepted;
|
|
639
|
-
}
|
|
640
|
-
lastError = `mercury ${o.call}: ${accepted.Err().message}`;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1452
|
+
return Result.Ok(accepted.Ok());
|
|
1453
|
+
}
|
|
1454
|
+
log.lastError = `mercury ${o.call}: ${accepted.Err().message}`;
|
|
1455
|
+
log.cause = "gate";
|
|
1456
|
+
noteSend(log);
|
|
1457
|
+
}
|
|
1458
|
+
return failCall(vctx, { call: o.call, row: o.row, attempts, startedAt, totals, log });
|
|
1459
|
+
}
|
|
1460
|
+
function callRecord(o) {
|
|
1461
|
+
const t = o.totals;
|
|
1462
|
+
return {
|
|
1463
|
+
call: o.call,
|
|
1464
|
+
ok: o.ok,
|
|
1465
|
+
attempts: o.attempts,
|
|
1466
|
+
elapsedMs: Date.now() - o.startedAt,
|
|
1467
|
+
...(t.servedModel === undefined ? {} : { servedModel: t.servedModel }),
|
|
1468
|
+
usage: t.usage,
|
|
1469
|
+
...(o.ok ? {} : { error: o.log.lastError, cause: o.log.cause }),
|
|
1470
|
+
sends: o.log.sends,
|
|
1471
|
+
retries: o.log.retries,
|
|
1472
|
+
...(o.log.history.length === 0 && o.ok ? {} : { history: o.log.history }),
|
|
1473
|
+
...(t.reasoningTokens === undefined ? {} : { reasoningTokens: t.reasoningTokens }),
|
|
1474
|
+
...o.extra,
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
async function failCall(vctx, o) {
|
|
1478
|
+
const { log } = o;
|
|
1479
|
+
vctx.logger
|
|
1480
|
+
.Warn()
|
|
1481
|
+
.Any("event", {
|
|
1482
|
+
chatId: o.row.chatId,
|
|
1483
|
+
call: o.call,
|
|
1484
|
+
cause: log.cause,
|
|
1485
|
+
sends: log.sends,
|
|
1486
|
+
retries: log.retries,
|
|
1487
|
+
error: log.lastError,
|
|
1488
|
+
})
|
|
1489
|
+
.Msg("mercury-first-build-call-failed");
|
|
1490
|
+
await recordMercuryRow(vctx, { ...o.row, record: callRecord({ ...o, ok: false }) });
|
|
1491
|
+
return Result.Err(new MercuryStageError(log.lastError, log.cause, {
|
|
1492
|
+
call: o.call,
|
|
1493
|
+
sends: log.sends,
|
|
1494
|
+
retries: log.retries,
|
|
1495
|
+
elapsedMs: Date.now() - o.startedAt,
|
|
1496
|
+
}));
|
|
657
1497
|
}
|
|
658
1498
|
async function recordMercuryRow(vctx, o) {
|
|
1499
|
+
const asked = plannerAskedModel(o.record.planner);
|
|
1500
|
+
const model = o.record.servedModel ?? asked;
|
|
1501
|
+
const rounds = o.record.usage.map((u) => ({ ...u, model, role: "mercury" }));
|
|
1502
|
+
if (rounds.length > 0)
|
|
1503
|
+
o.onUsage?.(rounds);
|
|
659
1504
|
const zero = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
660
|
-
const
|
|
1505
|
+
const promptId = vctx.sthis.nextId(12).str;
|
|
661
1506
|
const rInsert = await exception2Result(() => vctx.sql.db.insert(vctx.sql.tables.promptContexts).values({
|
|
662
1507
|
userId: o.userId,
|
|
663
1508
|
chatId: o.chatId,
|
|
664
|
-
promptId
|
|
1509
|
+
promptId,
|
|
665
1510
|
nethash: vctx.netHash(),
|
|
666
1511
|
promptTokens: 0,
|
|
667
1512
|
completionTokens: 0,
|
|
668
1513
|
totalTokens: 0,
|
|
669
1514
|
ref: {
|
|
670
1515
|
type: "prompt.usage.sql",
|
|
671
|
-
usage: { given:
|
|
672
|
-
model:
|
|
1516
|
+
usage: { given: [], calculated: zero },
|
|
1517
|
+
model: asked,
|
|
673
1518
|
step: "first-mercury",
|
|
674
1519
|
mercury: {
|
|
675
1520
|
call: o.record.call,
|
|
@@ -678,102 +1523,1922 @@ async function recordMercuryRow(vctx, o) {
|
|
|
678
1523
|
elapsedMs: o.record.elapsedMs,
|
|
679
1524
|
...(o.record.servedModel === undefined ? {} : { servedModel: o.record.servedModel }),
|
|
680
1525
|
...(o.record.error === undefined ? {} : { error: o.record.error.slice(0, 300) }),
|
|
1526
|
+
...(o.record.cause === undefined ? {} : { cause: o.record.cause }),
|
|
1527
|
+
sends: o.record.sends,
|
|
1528
|
+
retries: o.record.retries,
|
|
1529
|
+
...(o.record.history === undefined || o.record.history.length === 0 ? {} : { history: [...o.record.history] }),
|
|
1530
|
+
...(o.record.reasoningTokens === undefined ? {} : { reasoningTokens: o.record.reasoningTokens }),
|
|
1531
|
+
...(o.record.passedThrough === undefined || o.record.passedThrough.length === 0
|
|
1532
|
+
? {}
|
|
1533
|
+
: { passedThrough: [...o.record.passedThrough] }),
|
|
1534
|
+
...(o.record.addressRepair === undefined || o.record.addressRepair.length === 0
|
|
1535
|
+
? {}
|
|
1536
|
+
: { addressRepair: [...o.record.addressRepair] }),
|
|
1537
|
+
...(o.record.slugsWithWord === undefined ? {} : { slugsWithWord: o.record.slugsWithWord }),
|
|
1538
|
+
...(o.record.roomsDropped === undefined ? {} : { roomsDropped: o.record.roomsDropped }),
|
|
1539
|
+
...(o.record.fontDropped === undefined ? {} : { fontDropped: o.record.fontDropped }),
|
|
1540
|
+
...(o.record.fanout === undefined ? {} : { fanout: o.record.fanout }),
|
|
1541
|
+
...(o.record.juice === undefined ? {} : { juice: o.record.juice }),
|
|
1542
|
+
...(o.record.depth === undefined ? {} : { depth: o.record.depth }),
|
|
1543
|
+
...(o.record.screensDropped === undefined ? {} : { screensDropped: o.record.screensDropped }),
|
|
1544
|
+
...(o.record.reply === undefined ? {} : { reply: o.record.reply }),
|
|
1545
|
+
...(o.record.preambleFrom === undefined ? {} : { preambleFrom: o.record.preambleFrom }),
|
|
1546
|
+
...(o.record.storyChars === undefined ? {} : { storyChars: o.record.storyChars }),
|
|
1547
|
+
...(o.record.storyDropped === undefined ? {} : { storyDropped: o.record.storyDropped }),
|
|
1548
|
+
...(o.record.preambleChars === undefined ? {} : { preambleChars: o.record.preambleChars }),
|
|
1549
|
+
...(o.record.softMisses === undefined ? {} : { softMisses: o.record.softMisses }),
|
|
1550
|
+
...(o.record.coerced === undefined ? {} : { coerced: [...o.record.coerced] }),
|
|
1551
|
+
...(o.record.fieldsDropped === undefined ? {} : { fieldsDropped: [...o.record.fieldsDropped] }),
|
|
1552
|
+
...(o.record.preambleFallback === undefined ? {} : { preambleFallback: o.record.preambleFallback }),
|
|
1553
|
+
...(o.record.duo === undefined ? {} : { duo: o.record.duo }),
|
|
1554
|
+
...(o.record.duoDrafts === undefined ? {} : { duoDrafts: [...o.record.duoDrafts] }),
|
|
1555
|
+
...(o.record.duoBreadth === undefined ? {} : { duoBreadth: [...o.record.duoBreadth] }),
|
|
1556
|
+
...(o.record.duoPool === undefined ? {} : { duoPool: o.record.duoPool }),
|
|
1557
|
+
...(o.record.duoArchetypes === undefined ? {} : { duoArchetypes: [...o.record.duoArchetypes] }),
|
|
1558
|
+
...(o.record.duoConflict === undefined
|
|
1559
|
+
? {}
|
|
1560
|
+
: { duoConflict: o.record.duoConflict, duoConflictReason: o.record.duoConflictReason }),
|
|
1561
|
+
...(o.record.recordsRules === undefined ? {} : { recordsRules: o.record.recordsRules }),
|
|
1562
|
+
...(o.record.recordsDropped === undefined ? {} : { recordsDropped: o.record.recordsDropped }),
|
|
1563
|
+
...(o.record.rulesDropped === undefined ? {} : { rulesDropped: o.record.rulesDropped }),
|
|
1564
|
+
...(o.record.oneCall === undefined ? {} : { oneCall: o.record.oneCall }),
|
|
1565
|
+
...(o.record.shapeCall === undefined ? {} : { shapeCall: o.record.shapeCall }),
|
|
1566
|
+
...(o.record.draftArchetypes === undefined ? {} : { draftArchetypes: [...o.record.draftArchetypes] }),
|
|
1567
|
+
...(o.record.archetypeSplit === undefined ? {} : { archetypeSplit: o.record.archetypeSplit }),
|
|
1568
|
+
...(o.record.flagsWinner === undefined ? {} : { flagsWinner: [...o.record.flagsWinner] }),
|
|
1569
|
+
...(o.record.flagsUnion === undefined ? {} : { flagsUnion: [...o.record.flagsUnion] }),
|
|
1570
|
+
...(o.record.backendFilterCleared === undefined ? {} : { backendFilterCleared: [...o.record.backendFilterCleared] }),
|
|
1571
|
+
...(o.record.backendFilterKept === undefined ? {} : { backendFilterKept: o.record.backendFilterKept }),
|
|
1572
|
+
...(o.record.planner === undefined ? {} : { planner: o.record.planner }),
|
|
1573
|
+
...(o.record.format === undefined ? {} : { format: o.record.format }),
|
|
1574
|
+
...(o.record.thinkAloud === undefined ? {} : { thinkAloud: { ...o.record.thinkAloud } }),
|
|
1575
|
+
...(o.record.cards === undefined ? {} : { cards: { ...o.record.cards } }),
|
|
1576
|
+
...(o.keepText !== true || o.record.text === undefined ? {} : { text: o.record.text }),
|
|
1577
|
+
...(o.keepText !== true || o.record.partialText === undefined ? {} : { partialText: o.record.partialText }),
|
|
1578
|
+
...(o.keepText !== true || o.record.duoTexts === undefined ? {} : { duoTexts: [...o.record.duoTexts] }),
|
|
1579
|
+
...(rounds.length === 0 ? {} : { usage: rounds.map((r) => ({ ...r })) }),
|
|
681
1580
|
},
|
|
682
1581
|
},
|
|
683
1582
|
created: new Date().toISOString(),
|
|
684
1583
|
}));
|
|
685
1584
|
if (rInsert.isErr())
|
|
686
1585
|
vctx.logger.Warn().Err(rInsert).Str("chatId", o.chatId).Msg("mercury-first-build-row-failed");
|
|
1586
|
+
else
|
|
1587
|
+
o.onWritten?.(promptId);
|
|
687
1588
|
}
|
|
688
|
-
function
|
|
689
|
-
|
|
1589
|
+
export async function recordMercuryDispatched(vctx, o) {
|
|
1590
|
+
const t = vctx.sql.tables.promptContexts;
|
|
1591
|
+
const where = and(eq(t.chatId, o.chatId), eq(t.promptId, o.promptId));
|
|
1592
|
+
const r = await exception2Result(async () => {
|
|
1593
|
+
const [row] = await vctx.sql.db.select({ ref: t.ref }).from(t).where(where).limit(1);
|
|
1594
|
+
if (row === undefined)
|
|
1595
|
+
return;
|
|
1596
|
+
const ref = row.ref;
|
|
1597
|
+
if (ref.mercury === undefined)
|
|
1598
|
+
return;
|
|
1599
|
+
await vctx.sql.db
|
|
1600
|
+
.update(t)
|
|
1601
|
+
.set({ ref: { ...ref, mercury: { ...ref.mercury, dispatched: { enrichedPrompt: o.enrichedPrompt } } } })
|
|
1602
|
+
.where(where);
|
|
1603
|
+
});
|
|
1604
|
+
if (r.isErr())
|
|
1605
|
+
vctx.logger.Warn().Err(r).Str("chatId", o.chatId).Msg("mercury-first-build-dispatched-row-failed");
|
|
690
1606
|
}
|
|
691
|
-
|
|
692
|
-
|
|
1607
|
+
export async function recordMercuryRace(vctx, o) {
|
|
1608
|
+
const t = vctx.sql.tables.promptContexts;
|
|
1609
|
+
const where = and(eq(t.chatId, o.chatId), eq(t.promptId, o.promptId));
|
|
1610
|
+
const r = await exception2Result(async () => {
|
|
1611
|
+
const [row] = await vctx.sql.db.select({ ref: t.ref }).from(t).where(where).limit(1);
|
|
1612
|
+
if (row === undefined)
|
|
1613
|
+
return;
|
|
1614
|
+
const ref = row.ref;
|
|
1615
|
+
if (ref.mercury === undefined)
|
|
1616
|
+
return;
|
|
1617
|
+
await vctx.sql.db
|
|
1618
|
+
.update(t)
|
|
1619
|
+
.set({ ref: { ...ref, mercury: { ...ref.mercury, race: { ...o.race } } } })
|
|
1620
|
+
.where(where);
|
|
1621
|
+
});
|
|
1622
|
+
if (r.isErr())
|
|
1623
|
+
vctx.logger.Warn().Err(r).Str("chatId", o.chatId).Msg("mercury-first-build-race-row-failed");
|
|
1624
|
+
}
|
|
1625
|
+
export async function recordMercuryFim(vctx, o) {
|
|
1626
|
+
const t = vctx.sql.tables.promptContexts;
|
|
1627
|
+
const where = and(eq(t.chatId, o.chatId), eq(t.promptId, o.promptId));
|
|
1628
|
+
const r = await exception2Result(async () => {
|
|
1629
|
+
const [row] = await vctx.sql.db.select({ ref: t.ref }).from(t).where(where).limit(1);
|
|
1630
|
+
if (row === undefined)
|
|
1631
|
+
return;
|
|
1632
|
+
const ref = row.ref;
|
|
1633
|
+
if (ref.mercury === undefined)
|
|
1634
|
+
return;
|
|
1635
|
+
await vctx.sql.db
|
|
1636
|
+
.update(t)
|
|
1637
|
+
.set({
|
|
1638
|
+
ref: {
|
|
1639
|
+
...ref,
|
|
1640
|
+
mercury: { ...ref.mercury, fim: { ...o.fim }, ...(o.filledText === undefined ? {} : { filledText: o.filledText }) },
|
|
1641
|
+
},
|
|
1642
|
+
})
|
|
1643
|
+
.where(where);
|
|
1644
|
+
});
|
|
1645
|
+
if (r.isErr())
|
|
1646
|
+
vctx.logger.Warn().Err(r).Str("chatId", o.chatId).Msg("mercury-first-build-fim-row-failed");
|
|
1647
|
+
}
|
|
1648
|
+
export const MERCURY_PROSE_FORMAT_EXAMPLES = [
|
|
1649
|
+
{
|
|
1650
|
+
ask: "an appointments book for my dog grooming business, with a page of notes on each client's dog",
|
|
1651
|
+
brief: 'An appointments book for a dog grooming business. The owner\'s business object (default, named for the app, renameable in settings) holds two record kinds: dogs (client dogs) and appointments.\n\nMain screen: appointments book — a day-by-day agenda grouped by date (today first, upcoming days below, past collapsible), with a week strip to jump between days. Each appointment shows time, dog name, owner name, service (e.g. full groom, bath & brush, nail trim), duration, and status (booked / done / no-show) as a one-tap toggle. "New appointment" button opens a plain form: pick an existing dog (or add a new dog inline), date (relative words like "tomorrow"/"Friday" resolve against today and show the resolved date), time, service, duration, price, short note. Edit and delete on each appointment.\n\nDogs page: list of client dogs (searchable), each opening its own dog page: name, breed, owner name, owner phone, and a free-form notes page (temperament, coat, allergies, preferred cut, behaviour tips) that saves as you type, plus that dog\'s appointment history and next appointment, and a "Book this dog" button. Tapping a dog name on an appointment opens its page.\n\nRecords: dog {type:\'dog\', name, breed, ownerName, ownerPhone, notes} and appointment {type:\'appointment\', dogId, date (YYYY-MM-DD), time, service, durationMin, price, status, note}, all keyed to the business object id so staff can be invited later. Access rules: creator of the business object can read/write everything in it; members could be admitted later. No invite screen on screen yet.\n\nseed.json demo: the owner\'s business object, 6 dogs with realistic notes (e.g. a nervous poodle, a doodle with matting, a husky that hates dryers), and ~10 appointments dated around 2026-10-06 (a few past done, several today and this week).',
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
ask: "a snake game with a high score",
|
|
1655
|
+
brief: "A classic snake game. Main screen: a square grid board (drawn in SVG or canvas using theme tokens) where the snake moves steadily and eats food to grow; hitting a wall or itself ends the round. Controls: arrow keys / WASD on desktop, swipe plus an on-screen D-pad on phones. Start/Pause and Play again buttons. Current score shown above the board, and the person's high score beside it. When a round ends with a new best, save a score record (player handle, score, date) and celebrate \"New high score!\". Show a small high-score list (top 10 scores with ViewerTag for who set them) below the board. Speed rises gently as the snake grows. Records: each signed-in person's scores keyed on their own handle; access rules let the creator write their own score records and everyone with the link read the leaderboard. seed.json: a handful of demo scores (e.g. 8–6 entries from a small cast of players, scores 12–87, dated in the days around 2026-10-06), including one for the owner.",
|
|
1656
|
+
},
|
|
1657
|
+
{
|
|
1658
|
+
ask: "Make a family reunion bingo app. Let me paste our inside jokes, memories, or a message thread and turn them into a batch of squares I can edit. Deal a different card to each person I invite. We can mark squares and call bingo on our phones, with cards and progress saved.",
|
|
1659
|
+
brief: 'Family reunion bingo. The owner has a default game (named for the app, renamable in settings) keyed on their handle. Front door: one composer where the owner pastes inside jokes, memories or a whole message thread. Add sends the text to callAI, which extracts short bingo square phrases, saves each as a square record in the game immediately, and lists the saved squares with Edit (inline text edit) and Delete actions. Each saved square has Change by sentence: selecting it switches Add to Apply correction, which rewrites that same square id. Ask action answers a question about the squares/game read-only beside the composer (e.g. "which squares mention Grandma?"). One Undo restores the latest batch add or the previous version of an edited square. If extraction fails, keep the pasted text in the box. A plain "Add square" text input stays beside the composer.\nInvite: HandleInput in the game writes a membership for each invited family member. Deal: each member (owner included) gets their own card record — a 5x5 grid with a FREE center, drawn by a seeded shuffle from the square pool, so each person\'s card is different; owner can press Deal cards / Redeal. Players open the game on their phones, tap squares to mark them (marks saved on their own card record, only the card\'s player can mark), and a Bingo! button lights up when a row, column or diagonal is complete; calling bingo writes a bingo record that everyone in the game sees in a winners feed with ViewerTag. A progress view shows each player\'s marked count. Games list reads games by type, showing shared ones with who made them.\nAccess: game created by owner; members can read the game, squares, all cards and bingo calls; only owner edits squares/deals; each player writes only their own card\'s marks and their own bingo call.\nSeed.json demo: a "Miller Family Reunion 2025" game owned by the owner with ~30 squares of believable inside jokes ("Uncle Ray tells the fishing story", "Someone burns the burgers", "Grandma asks who\'s dating whom", "Kids find the creek", "Aunt Linda\'s potato salad debate"), owner\'s membership plus two cousins, dealt cards with a few marks, and one bingo call.',
|
|
1660
|
+
},
|
|
1661
|
+
];
|
|
1662
|
+
export const MERCURY_PROSE_SYSTEM = `You write the build brief for a small web app from a person's one-message request. The brief is read by the builder that writes the app's code. Return only JSON.
|
|
1663
|
+
|
|
1664
|
+
Your reply first classifies the request, then names the app, then writes the brief.
|
|
1665
|
+
|
|
1666
|
+
archetype, exactly one:
|
|
1667
|
+
${MERCURY_SHAPE_ARCHETYPE_TEXT}
|
|
1668
|
+
|
|
1669
|
+
coreAction: the one thing the person does most in this app, as a short verb phrase in their own words ("tap once per glass", "paste the group chat", "lasso cows").
|
|
1670
|
+
recordNames: 1-5 short singular record type names the app stores (toys may have just "score" or none).
|
|
1671
|
+
capabilities: an array naming each capability the request itself needs, from this list; empty when it needs none:
|
|
1672
|
+
${MERCURY_CAPABILITY_TRIGGERS}
|
|
1673
|
+
|
|
1674
|
+
${MERCURY_ADDRESS.rule}
|
|
1675
|
+
|
|
1676
|
+
brief: one free-prose brief to the builder, in exactly the shape of the example briefs below — the same sections in the same order, the same voice addressed to the builder, the same plain paragraphs. It opens with one line saying what the app is; names the object the records belong to and who owns it; walks the screens with the controls each one has and what each does; names each record with its fields, as the examples write them; states the access rules — who reads and who writes; and ends with the seed.json demo, specific: names, items and states. Everything in it follows the classification and the person's own words.`;
|
|
1677
|
+
export function mercuryProseBriefSchema(opts = {}) {
|
|
1678
|
+
const props = {
|
|
1679
|
+
...MERCURY_CLASSIFY_PROPERTIES,
|
|
1680
|
+
...MERCURY_ADDRESS.properties,
|
|
1681
|
+
...(opts.rooms === true ? { rooms: { type: "array", items: { type: "string" } } } : {}),
|
|
1682
|
+
...(opts.font === true ? { font: { type: "string" } } : {}),
|
|
1683
|
+
brief: { type: "string" },
|
|
1684
|
+
};
|
|
1685
|
+
return { type: "object", additionalProperties: false, required: Object.keys(props), properties: props };
|
|
1686
|
+
}
|
|
1687
|
+
export function mercuryProseBriefMessages(o) {
|
|
1688
|
+
const rooms = o.rooms === true;
|
|
1689
|
+
const font = o.font === true;
|
|
1690
|
+
const examples = MERCURY_PROSE_FORMAT_EXAMPLES.map((e, i) => `Example ${i + 1}. The person asked: "${e.ask}"\nbrief:\n${e.brief}`).join("\n\n");
|
|
1691
|
+
const skeleton = {
|
|
1692
|
+
...Object.fromEntries(Object.entries(MERCURY_CLASSIFY_HINTS).map(([k, h]) => [k, `<${h}>`])),
|
|
1693
|
+
...Object.fromEntries(Object.entries(MERCURY_ADDRESS.hints).map(([k, h]) => [k, `<${h}>`])),
|
|
1694
|
+
...(rooms ? { rooms: `<${MERCURY_ROOMS_HINT}>` } : {}),
|
|
1695
|
+
...(font ? { font: `<${MERCURY_FONT_HINT}>` } : {}),
|
|
1696
|
+
brief: "<the brief to the builder, in the examples' shape>",
|
|
1697
|
+
};
|
|
1698
|
+
const user = [
|
|
1699
|
+
"Answer in exactly this shape, each value replaced by the real text:",
|
|
1700
|
+
JSON.stringify(skeleton, null, 1),
|
|
1701
|
+
...(rooms ? ["", `Rooms: ${poufRoomCatalog()}`] : []),
|
|
1702
|
+
...(font ? ["", `Fonts: ${vibeFontCatalog()}`] : []),
|
|
1703
|
+
"",
|
|
1704
|
+
`Person's words: "${o.personWords}"`,
|
|
1705
|
+
"",
|
|
1706
|
+
`Classify the person's words, name the app, then write its brief the way the examples write theirs. Return JSON with these keys: ${Object.keys(skeleton).join(", ")} (${MERCURY_ADDRESS.ask}).` +
|
|
1707
|
+
(rooms ? ` ${MERCURY_ROOMS_ASK}` : "") +
|
|
1708
|
+
(font ? ` ${MERCURY_FONT_ASK}` : ""),
|
|
1709
|
+
...(o.words.length > 0 ? [mercurySlugWordsSentence(o.words)] : []),
|
|
1710
|
+
].join("\n");
|
|
1711
|
+
return [
|
|
1712
|
+
{ role: "system", content: [{ type: "text", text: `${MERCURY_PROSE_SYSTEM}\n\n${examples}` }] },
|
|
1713
|
+
{ role: "user", content: [{ type: "text", text: user }] },
|
|
1714
|
+
];
|
|
1715
|
+
}
|
|
1716
|
+
export function gateMercuryProseDraft(json, o) {
|
|
693
1717
|
const parsed = replyObject(json);
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1718
|
+
if (Array.isArray(json) || parsed instanceof type.errors)
|
|
1719
|
+
return Result.Err("reply:not-an-object");
|
|
1720
|
+
const rShape = parseMercuryDraftShape(parsed, o.personWords);
|
|
1721
|
+
if (rShape.isErr())
|
|
1722
|
+
return Result.Err(`classification:${rShape.Err().message}`);
|
|
1723
|
+
const brief = typeof parsed.brief === "string" ? cleanMercuryText(parsed.brief) : "";
|
|
1724
|
+
if (brief.length === 0)
|
|
1725
|
+
return Result.Err("brief:empty");
|
|
1726
|
+
const decoded = MERCURY_ADDRESS.decode(parsed);
|
|
1727
|
+
if (decoded.isErr())
|
|
1728
|
+
return Result.Err(`pairs:${decoded.Err().message}`);
|
|
1729
|
+
const d = decoded.Ok();
|
|
1730
|
+
const strict = d.title === undefined ? undefined : gateMercuryAddress({ title: d.title, slugs: d.slugs });
|
|
1731
|
+
const address = strict?.isOk() === true ? strict.Ok() : looseMercuryAddress(d.title, d.slugs);
|
|
1732
|
+
if (address === undefined)
|
|
1733
|
+
return Result.Err("pairs:empty");
|
|
1734
|
+
return Result.Ok({
|
|
1735
|
+
shape: rShape.Ok(),
|
|
1736
|
+
brief,
|
|
1737
|
+
address,
|
|
1738
|
+
addressRepair: [...d.repairs, ...(address.fallback ? ["slug-fallback"] : [])],
|
|
1739
|
+
...(o.askRooms && "rooms" in parsed ? { rooms: gateMercuryRooms(parsed.rooms) } : {}),
|
|
1740
|
+
...(o.askRooms && "font" in parsed ? { font: gateMercuryFont(parsed.font) } : {}),
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
export function assembleMercuryProseCreateVibe(o) {
|
|
1744
|
+
const s = o.draft.shape;
|
|
1745
|
+
const nature = mercuryNature(s, o.personWords);
|
|
1746
|
+
const rooms = sanitizeRoomShortlist("pouf", o.draft.rooms?.rooms);
|
|
1747
|
+
return {
|
|
1748
|
+
prompt: o.personWords,
|
|
1749
|
+
enrichedPrompt: `${o.draft.brief}\n\nThe person asked: "${o.personWords}".`,
|
|
1750
|
+
skills: mercurySkills(s, { personWords: o.personWords }),
|
|
1751
|
+
pairs: o.draft.address.pairs.map((p) => ({ title: p.title, slug: p.slug })),
|
|
1752
|
+
nature,
|
|
1753
|
+
...(nature === "sensitive" ? { privacySensitive: true } : {}),
|
|
1754
|
+
...(rooms === undefined ? {} : { rooms: [...rooms] }),
|
|
1755
|
+
...(o.draft.font?.font === undefined ? {} : { font: o.draft.font.font }),
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
export function mercuryProsePreamble(draft) {
|
|
1759
|
+
return `I'm making ${draft.address.pairs[0]?.title ?? "your app"} for you: ${draft.shape.coreAction}.`;
|
|
1760
|
+
}
|
|
1761
|
+
export const MERCURY_THINKALOUD_MARKER = "--- SPEC ---";
|
|
1762
|
+
export const MERCURY_THINKALOUD_SPEC_HINTS = {
|
|
1763
|
+
archetype: "one of personal, group, publish, intake, open, toy: the ownership shape that fits the person's words",
|
|
1764
|
+
coreAction: "the one thing the person does most in this app, a short verb phrase in their own words",
|
|
1765
|
+
recordNames: "1 to 5 short singular record type names the app stores, comma-separated",
|
|
1766
|
+
capabilities: "the capabilities this request needs, from the capability list, comma-separated; none when it needs none",
|
|
1767
|
+
opening: "one sentence to the builder: what the app is, for whom, and what it does, in the person's words",
|
|
1768
|
+
access: "who owns the object, who else is let in and what they write, what a visitor sees",
|
|
1769
|
+
records: 'each record type with its fields: "name (field, field); name (field, field)", plus the id rule where one matters',
|
|
1770
|
+
screens: "1. the first screen: what it shows from the records, what the person does there, and what leads while it is empty; 2. ...; 3. ...",
|
|
1771
|
+
seed: "seed.json: the demo records in words: who is in it, which state each record is in, how they relate; every state the mechanism and the clauses mention; the person's typed items verbatim; the builder picks amounts, counts and dates",
|
|
1772
|
+
mechanism: "the one computation or invariant the app hinges on, with the unit of every number it computes",
|
|
1773
|
+
clauses: 'one line per clause of the person\'s words: "their clause" → record field → control → twice: what the second identical write does',
|
|
1774
|
+
look: "one scene: surface, palette, card shape, one gesture of mood",
|
|
1775
|
+
title: "the app's name, two or three words in its own vocabulary",
|
|
1776
|
+
slugs: "three short kebab-case addresses for the app, comma-separated",
|
|
1777
|
+
icon: "one object from the app's world",
|
|
1778
|
+
};
|
|
1779
|
+
const THINKALOUD_BRIEF_KEYS = ["opening", "access", "records", "screens", "seed", "mechanism", "clauses", "look", "icon"];
|
|
1780
|
+
export function mercuryThinkAloudSystem(o = {}) {
|
|
1781
|
+
const keys = {
|
|
1782
|
+
...MERCURY_THINKALOUD_SPEC_HINTS,
|
|
1783
|
+
...(o.rooms === true ? { rooms: `${MERCURY_ROOMS_HINT}, comma-separated` } : {}),
|
|
1784
|
+
...(o.font === true ? { font: MERCURY_FONT_HINT } : {}),
|
|
1785
|
+
};
|
|
1786
|
+
const specLines = Object.entries(keys)
|
|
1787
|
+
.map(([k, h]) => `${k}: <${h}>`)
|
|
1788
|
+
.join("\n");
|
|
1789
|
+
return `You are a usability tester. Someone has just shown you this request for a small web app, and you are seeing it for the first time. Think out loud, in the first person, the way you would in a recorded session. These are hunches, not answers: label them as guesses ("I'd guess", "probably", "not sure", "wait").
|
|
1790
|
+
|
|
1791
|
+
1. What you picture when you read it, and what you would tap first.
|
|
1792
|
+
2. Then keep asking "and then what happens to…": if I do this, what changes somewhere else? What does that number mean, and what is it made of? What did the person not say that the app has to decide anyway?
|
|
1793
|
+
3. What feels off or ambiguous in the person's exact words, quoting them.
|
|
1794
|
+
|
|
1795
|
+
Then write one line that reads exactly "${MERCURY_THINKALOUD_MARKER}" and switch roles: write the settled parts of a brief for a builder, using only what you now believe after the think-aloud. One key per line, in this order, as "key: value"; the clauses and screens may take one line each under their key. Every line that rests on one of your guesses rather than on the person's words starts its value with "guess:"; a line the person's words settle carries no label.
|
|
1796
|
+
|
|
1797
|
+
${specLines}
|
|
1798
|
+
|
|
1799
|
+
archetype, exactly one:
|
|
1800
|
+
${MERCURY_LEAN_ARCHETYPES}
|
|
1801
|
+
|
|
1802
|
+
capabilities: each capability the request itself needs, from this list; none when it needs none:
|
|
1803
|
+
${MERCURY_LEAN_CAPABILITIES}
|
|
1804
|
+
|
|
1805
|
+
${MERCURY_LEAN_ADDRESS}`;
|
|
1806
|
+
}
|
|
1807
|
+
export function mercuryThinkAloudMessages(o) {
|
|
1808
|
+
const rooms = o.rooms === true;
|
|
1809
|
+
const font = o.font === true;
|
|
1810
|
+
const user = [
|
|
1811
|
+
`The app creator's initial prompt: "${o.personWords}"`,
|
|
1812
|
+
...(rooms ? ["", `Rooms: ${poufRoomCatalog()}`] : []),
|
|
1813
|
+
...(font ? ["", `Fonts: ${vibeFontCatalog()}`] : []),
|
|
1814
|
+
...(o.words.length > 0 ? ["", mercurySlugWordsSentence(o.words)] : []),
|
|
1815
|
+
].join("\n");
|
|
1816
|
+
return [
|
|
1817
|
+
{ role: "system", content: [{ type: "text", text: mercuryThinkAloudSystem({ rooms, font }) }] },
|
|
1818
|
+
{ role: "user", content: [{ type: "text", text: user }] },
|
|
1819
|
+
];
|
|
1820
|
+
}
|
|
1821
|
+
const THINKALOUD_KEYS = new Set([...Object.keys(MERCURY_THINKALOUD_SPEC_HINTS), "rooms", "font"]);
|
|
1822
|
+
const THINKALOUD_MARKER_LINE = /^[ \t]*[*_#>-]*[ \t]*-{2,}[ \t]*SPEC[ \t]*-{2,}[ \t]*[*_]*[ \t]*$/im;
|
|
1823
|
+
const THINKALOUD_KEY_LINE = /^\s*(?:[-*]\s+)?(?:\*\*|__)?([A-Za-z]+)(?:\*\*|__)?\s*:(?:\*\*|__)?\s*(.*)$/;
|
|
1824
|
+
const GUESS_LABEL = /\bguess\s*:/i;
|
|
1825
|
+
export function parseMercuryThinkAloud(text) {
|
|
1826
|
+
const m = THINKALOUD_MARKER_LINE.exec(text);
|
|
1827
|
+
if (m === null)
|
|
1828
|
+
return undefined;
|
|
1829
|
+
const narration = text.slice(0, m.index).trim();
|
|
1830
|
+
const spec = text
|
|
1831
|
+
.slice(m.index + m[0].length)
|
|
1832
|
+
.replace(/^\s*```[a-zA-Z]*\n?|\n?```\s*$/g, "")
|
|
1833
|
+
.trim();
|
|
1834
|
+
const keys = {};
|
|
1835
|
+
let current;
|
|
1836
|
+
for (const line of spec.split("\n")) {
|
|
1837
|
+
const k = THINKALOUD_KEY_LINE.exec(line);
|
|
1838
|
+
const name = k === null ? undefined : [...THINKALOUD_KEYS].find((x) => x.toLowerCase() === k[1].toLowerCase());
|
|
1839
|
+
if (name !== undefined && k !== null && keys[name] === undefined) {
|
|
1840
|
+
current = name;
|
|
1841
|
+
keys[name] = k[2].trim();
|
|
704
1842
|
continue;
|
|
705
1843
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
o.into.texts.set(f, v);
|
|
709
|
-
else
|
|
710
|
-
failed.push(`${f}:${fails.join("+")}`);
|
|
1844
|
+
if (current !== undefined && line.trim().length > 0)
|
|
1845
|
+
keys[current] = `${keys[current]}\n${line.trim()}`.trim();
|
|
711
1846
|
}
|
|
712
|
-
|
|
1847
|
+
const guessLines = spec.split("\n").filter((l) => GUESS_LABEL.test(l)).length;
|
|
1848
|
+
return { narration, spec, keys, guessLines };
|
|
713
1849
|
}
|
|
714
|
-
|
|
715
|
-
return
|
|
1850
|
+
function thinkAloudScalar(v) {
|
|
1851
|
+
return (v ?? "")
|
|
1852
|
+
.replace(/^\s*\(?\s*guess\s*:\s*/i, "")
|
|
1853
|
+
.replace(/\(\s*guess[^)]*\)/gi, "")
|
|
1854
|
+
.replace(/\bguess\s*:\s*/gi, "")
|
|
1855
|
+
.trim()
|
|
1856
|
+
.replace(/^["'`]+|["'`]+$/g, "")
|
|
1857
|
+
.trim();
|
|
716
1858
|
}
|
|
717
|
-
|
|
1859
|
+
function thinkAloudList(v) {
|
|
1860
|
+
const t = thinkAloudScalar(v).replace(/^\[|\]$/g, "");
|
|
1861
|
+
return t
|
|
1862
|
+
.split(/\s*[,;\n]\s*/)
|
|
1863
|
+
.map((x) => x
|
|
1864
|
+
.trim()
|
|
1865
|
+
.replace(/^["'`]+|["'`]+$/g, "")
|
|
1866
|
+
.trim())
|
|
1867
|
+
.filter((x) => x.length > 0 && /^(none|n\/a|nothing|-)$/i.test(x) === false);
|
|
1868
|
+
}
|
|
1869
|
+
export const MERCURY_THINKALOUD_DEFAULT_ARCHETYPE = MERCURY_ARCHETYPES[0];
|
|
1870
|
+
export function gateMercuryThinkAloud(raw, o) {
|
|
1871
|
+
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
1872
|
+
return Result.Err("parse:empty");
|
|
1873
|
+
const soft = {};
|
|
1874
|
+
const miss = (part, what) => {
|
|
1875
|
+
soft[part] = [...(soft[part] ?? []), what];
|
|
1876
|
+
};
|
|
1877
|
+
const parsed = parseMercuryThinkAloud(raw);
|
|
1878
|
+
if (parsed === undefined)
|
|
1879
|
+
miss("marker", "missing");
|
|
1880
|
+
else if (parsed.spec.length === 0)
|
|
1881
|
+
miss("spec", "empty");
|
|
1882
|
+
const p = parsed ?? {
|
|
1883
|
+
narration: raw.trim(),
|
|
1884
|
+
spec: "",
|
|
1885
|
+
keys: {},
|
|
1886
|
+
guessLines: raw.split("\n").filter((l) => GUESS_LABEL.test(l)).length,
|
|
1887
|
+
};
|
|
1888
|
+
const caps = [];
|
|
1889
|
+
for (const c of thinkAloudList(p.keys.capabilities).map((x) => x.toLowerCase())) {
|
|
1890
|
+
const flag = MERCURY_FLAGS.find((f) => f.toLowerCase() === c);
|
|
1891
|
+
if (flag === undefined)
|
|
1892
|
+
miss("capabilities", "unknown");
|
|
1893
|
+
else if (!caps.includes(flag))
|
|
1894
|
+
caps.push(flag);
|
|
1895
|
+
}
|
|
1896
|
+
const named = thinkAloudScalar(p.keys.archetype)
|
|
1897
|
+
.toLowerCase()
|
|
1898
|
+
.match(/[a-z]+/)?.[0] ?? "";
|
|
1899
|
+
const known = MERCURY_ARCHETYPES.find((a) => a === named);
|
|
1900
|
+
if (known === undefined)
|
|
1901
|
+
miss("archetype", "default");
|
|
1902
|
+
const said = thinkAloudScalar(p.keys.coreAction);
|
|
1903
|
+
if (said.length === 0)
|
|
1904
|
+
miss("coreAction", "default");
|
|
1905
|
+
const rShape = parseMercuryDraftShape({
|
|
1906
|
+
archetype: known ?? MERCURY_THINKALOUD_DEFAULT_ARCHETYPE,
|
|
1907
|
+
coreAction: said.length > 0 ? said : o.personWords.trim(),
|
|
1908
|
+
recordNames: thinkAloudList(p.keys.recordNames),
|
|
1909
|
+
capabilities: caps,
|
|
1910
|
+
}, o.personWords);
|
|
1911
|
+
if (rShape.isErr())
|
|
1912
|
+
return Result.Err(`classification:${rShape.Err().message}`);
|
|
1913
|
+
const decoded = MERCURY_ADDRESS.decode({
|
|
1914
|
+
...(p.keys.title === undefined ? {} : { title: thinkAloudScalar(p.keys.title) }),
|
|
1915
|
+
slugs: thinkAloudList(p.keys.slugs),
|
|
1916
|
+
});
|
|
1917
|
+
if (decoded.isErr())
|
|
1918
|
+
miss("pairs", decoded.Err().message);
|
|
1919
|
+
const d = decoded.isOk() ? decoded.Ok() : { slugs: [], repairs: [] };
|
|
1920
|
+
const strict = d.title === undefined ? undefined : gateMercuryAddress({ title: d.title, slugs: d.slugs });
|
|
1921
|
+
const loose = strict?.isOk() === true ? strict.Ok() : looseMercuryAddress(d.title, d.slugs);
|
|
1922
|
+
if (strict?.isErr() === true && loose !== undefined)
|
|
1923
|
+
miss("pairs", strict.Err().message);
|
|
1924
|
+
if (loose === undefined)
|
|
1925
|
+
miss("pairs", "default");
|
|
1926
|
+
const address = loose ?? { pairs: [], fallback: true, tagged: false };
|
|
1927
|
+
if (p.narration.length === 0)
|
|
1928
|
+
miss("narration", "empty");
|
|
1929
|
+
for (const k of THINKALOUD_BRIEF_KEYS)
|
|
1930
|
+
if (p.keys[k] === undefined || p.keys[k].trim().length === 0)
|
|
1931
|
+
miss(k, "missing");
|
|
1932
|
+
if (o.askRooms && p.keys.rooms === undefined)
|
|
1933
|
+
miss("rooms", "default");
|
|
1934
|
+
if (o.askRooms && p.keys.font === undefined)
|
|
1935
|
+
miss("font", "default");
|
|
1936
|
+
return Result.Ok({
|
|
1937
|
+
shape: rShape.Ok(),
|
|
1938
|
+
reply: raw,
|
|
1939
|
+
narration: p.narration,
|
|
1940
|
+
spec: p.spec,
|
|
1941
|
+
address,
|
|
1942
|
+
addressRepair: [...d.repairs, ...(address.fallback && address.pairs.length > 0 ? ["slug-fallback"] : [])],
|
|
1943
|
+
...(o.askRooms && p.keys.rooms !== undefined ? { rooms: gateMercuryRooms(thinkAloudList(p.keys.rooms)) } : {}),
|
|
1944
|
+
...(o.askRooms && p.keys.font !== undefined ? { font: gateMercuryFont(thinkAloudScalar(p.keys.font)) } : {}),
|
|
1945
|
+
softMisses: soft,
|
|
1946
|
+
guessLines: p.guessLines,
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
export const MERCURY_TESTER_REPLY_INTRO = "A fast tester's first read:";
|
|
1950
|
+
export const MERCURY_TESTER_REPLY_RULE = "Guesses only; the creator's words win.";
|
|
1951
|
+
export function mercuryTesterReplyEnrichedPrompt(reply, personWords) {
|
|
1952
|
+
return [
|
|
1953
|
+
MERCURY_TESTER_REPLY_INTRO,
|
|
1954
|
+
`<tester-reply>\n${reply}\n</tester-reply>`,
|
|
1955
|
+
MERCURY_TESTER_REPLY_RULE,
|
|
1956
|
+
`The app creator's initial prompt: "${personWords}"`,
|
|
1957
|
+
].join("\n\n");
|
|
1958
|
+
}
|
|
1959
|
+
export function assembleMercuryThinkAloudCreateVibe(o) {
|
|
1960
|
+
const s = o.draft.shape;
|
|
1961
|
+
const nature = mercuryNature(s, o.personWords);
|
|
1962
|
+
const rooms = sanitizeRoomShortlist("pouf", o.draft.rooms?.rooms);
|
|
1963
|
+
return {
|
|
1964
|
+
prompt: o.personWords,
|
|
1965
|
+
enrichedPrompt: mercuryTesterReplyEnrichedPrompt(o.draft.reply, o.personWords),
|
|
1966
|
+
skills: mercurySkills(s, { personWords: o.personWords }),
|
|
1967
|
+
...(o.draft.address.pairs.length === 0 ? {} : { pairs: o.draft.address.pairs.map((p) => ({ title: p.title, slug: p.slug })) }),
|
|
1968
|
+
nature,
|
|
1969
|
+
...(nature === "sensitive" ? { privacySensitive: true } : {}),
|
|
1970
|
+
...(rooms === undefined ? {} : { rooms: [...rooms] }),
|
|
1971
|
+
...(o.draft.font?.font === undefined ? {} : { font: o.draft.font.font }),
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
export function mercuryThinkAloudPreamble(draft) {
|
|
1975
|
+
return `I'm making ${draft.address.pairs[0]?.title ?? "your app"} for you: ${draft.shape.coreAction}.`;
|
|
1976
|
+
}
|
|
1977
|
+
export const MERCURY_CARDS_MARKER = "--- CARDS ---";
|
|
1978
|
+
export const MERCURY_CARDS_APP_MARKER = "--- APP ---";
|
|
1979
|
+
export const MERCURY_CARDS_TAIL_HINTS = {
|
|
1980
|
+
title: "the app's name, two or three words in its own vocabulary",
|
|
1981
|
+
slugs: "three short kebab-case addresses for the app, comma-separated",
|
|
1982
|
+
archetype: "one of personal, group, publish, intake, open, toy: the ownership shape that fits the person's words",
|
|
1983
|
+
capabilities: "the capabilities this request needs, from the capability list, comma-separated; none when it needs none",
|
|
1984
|
+
};
|
|
1985
|
+
export const MERCURY_CARDS_PREAMBLE_RULE = `Start with a short paragraph to the app creator, in plain warm words: what you're making for them and its most important workflow, walked through the way they'll do it. Say "probably" where you're guessing.`;
|
|
1986
|
+
export const MERCURY_CARDS_SHAPE = `As a <role>, I <action on a named record>, so that <outcome>.
|
|
1987
|
+
Accept: <one testable line, in terms of records and fields>
|
|
1988
|
+
Accept: <another>
|
|
1989
|
+
Data: <Record>{<field>, <field>, ...}
|
|
1990
|
+
Rule: <a value this card depends on and how it comes about, said loosely in one plain English sentence a friend would understand>`;
|
|
1991
|
+
export const MERCURY_CARDS_RULES = `Rules:
|
|
1992
|
+
- Use the person's own nouns for records, roles and actions.
|
|
1993
|
+
- Give what the person asked for plus what those parts need in order to work.
|
|
1994
|
+
- A line that rests on your assumption and not on the person's words ends with (guess). A line the person's words settle carries no label.`;
|
|
1995
|
+
export function mercuryCardsSystem(o = {}) {
|
|
1996
|
+
const keys = {
|
|
1997
|
+
...MERCURY_CARDS_TAIL_HINTS,
|
|
1998
|
+
...(o.rooms === true ? { rooms: `${MERCURY_ROOMS_HINT}, comma-separated` } : {}),
|
|
1999
|
+
...(o.font === true ? { font: MERCURY_FONT_HINT } : {}),
|
|
2000
|
+
};
|
|
2001
|
+
const tailLines = Object.entries(keys)
|
|
2002
|
+
.map(([k, h]) => `${k}: <${h}>`)
|
|
2003
|
+
.join("\n");
|
|
2004
|
+
return `You read a person's request for a small web app and write its first plan as plain text.
|
|
2005
|
+
|
|
2006
|
+
${MERCURY_CARDS_PREAMBLE_RULE} Then a line reading exactly "${MERCURY_CARDS_MARKER}", then the requirements as user-story cards, one card per thing a role can do or see:
|
|
2007
|
+
${MERCURY_CARDS_SHAPE}
|
|
2008
|
+
Cover every role the person's words name, every action, every derived value, and who may do what without signing in.
|
|
2009
|
+
|
|
2010
|
+
${MERCURY_CARDS_RULES}
|
|
2011
|
+
|
|
2012
|
+
Then a line reading exactly "${MERCURY_CARDS_APP_MARKER}", then one line per key, as "key: value":
|
|
2013
|
+
${tailLines}
|
|
2014
|
+
|
|
2015
|
+
archetype, exactly one:
|
|
2016
|
+
${MERCURY_LEAN_ARCHETYPES}
|
|
2017
|
+
|
|
2018
|
+
capabilities: each capability the request itself needs, from this list; none when it needs none:
|
|
2019
|
+
${MERCURY_LEAN_CAPABILITIES}
|
|
2020
|
+
|
|
2021
|
+
${MERCURY_LEAN_ADDRESS}`;
|
|
2022
|
+
}
|
|
2023
|
+
export function mercuryCardsMessages(o) {
|
|
2024
|
+
const rooms = o.rooms === true;
|
|
2025
|
+
const font = o.font === true;
|
|
2026
|
+
const user = [
|
|
2027
|
+
`The app creator's initial prompt: "${o.personWords}"`,
|
|
2028
|
+
...(rooms ? ["", `Rooms: ${poufRoomCatalog()}`] : []),
|
|
2029
|
+
...(font ? ["", `Fonts: ${vibeFontCatalog()}`] : []),
|
|
2030
|
+
...(o.words.length > 0 ? ["", mercurySlugWordsSentence(o.words)] : []),
|
|
2031
|
+
].join("\n");
|
|
2032
|
+
return [
|
|
2033
|
+
{ role: "system", content: [{ type: "text", text: mercuryCardsSystem({ rooms, font }) }] },
|
|
2034
|
+
{ role: "user", content: [{ type: "text", text: user }] },
|
|
2035
|
+
];
|
|
2036
|
+
}
|
|
2037
|
+
const CARDS_TAIL_KEYS = new Set([...Object.keys(MERCURY_CARDS_TAIL_HINTS), "rooms", "font"]);
|
|
2038
|
+
const CARDS_MARKER_LINE = /^[ \t]*[*_#>-]*[ \t]*-{2,}[ \t]*CARDS[ \t]*-{2,}[ \t]*[*_]*[ \t]*$/i;
|
|
2039
|
+
const APP_MARKER_LINE = /^[ \t]*[*_#>-]*[ \t]*-{2,}[ \t]*APP[ \t]*-{2,}[ \t]*[*_]*[ \t]*$/i;
|
|
2040
|
+
const CARD_LINE = /^\s*(?:[-*•]|\d+[.)])?\s*(?:\*\*)?As an? \S/i;
|
|
2041
|
+
const DATA_LINE = /^\s*(?:[-*]\s*)?(?:\*\*)?Data(?:\*\*)?\s*:\s*(?:\*\*)?([A-Za-z][A-Za-z0-9 _-]*?)\s*\{/;
|
|
2042
|
+
const ACCEPT_LINE = /^\s*(?:[-*]\s*)?(?:\*\*)?Accept(?:\*\*)?\s*:/i;
|
|
2043
|
+
const RULE_LINE = /^\s*(?:[-*]\s*)?(?:\*\*)?Rule(?:\*\*)?\s*:/i;
|
|
2044
|
+
const CARD_GUESS = /\(\s*guess\s*\)|\bguess\s*:/i;
|
|
2045
|
+
export function parseMercuryCards(text) {
|
|
2046
|
+
const lines = text.split("\n");
|
|
2047
|
+
const cardsMarkerAt = lines.findIndex((l) => CARDS_MARKER_LINE.test(l));
|
|
2048
|
+
const appMarkerAt = lines.findIndex((l, i) => APP_MARKER_LINE.test(l) && (cardsMarkerAt < 0 || i > cardsMarkerAt));
|
|
2049
|
+
const bodyStart = cardsMarkerAt < 0 ? 0 : cardsMarkerAt + 1;
|
|
2050
|
+
const bodyEnd = appMarkerAt < 0 ? lines.length : appMarkerAt;
|
|
2051
|
+
const preamble = lines
|
|
2052
|
+
.slice(0, cardsMarkerAt < 0 ? 0 : cardsMarkerAt)
|
|
2053
|
+
.join("\n")
|
|
2054
|
+
.trim();
|
|
2055
|
+
const cardLines = [];
|
|
2056
|
+
const dataRecords = [];
|
|
2057
|
+
let acceptLines = 0;
|
|
2058
|
+
let ruleLines = 0;
|
|
2059
|
+
let guessLines = 0;
|
|
2060
|
+
for (let i = bodyStart; i < bodyEnd; i += 1) {
|
|
2061
|
+
const l = lines[i];
|
|
2062
|
+
if (CARD_LINE.test(l))
|
|
2063
|
+
cardLines.push(i);
|
|
2064
|
+
const d = DATA_LINE.exec(l);
|
|
2065
|
+
if (d !== null && dataRecords.includes(d[1].trim()) === false)
|
|
2066
|
+
dataRecords.push(d[1].trim());
|
|
2067
|
+
if (ACCEPT_LINE.test(l))
|
|
2068
|
+
acceptLines += 1;
|
|
2069
|
+
if (RULE_LINE.test(l))
|
|
2070
|
+
ruleLines += 1;
|
|
2071
|
+
if (CARD_GUESS.test(l))
|
|
2072
|
+
guessLines += 1;
|
|
2073
|
+
}
|
|
2074
|
+
const keys = {};
|
|
2075
|
+
if (appMarkerAt >= 0) {
|
|
2076
|
+
for (const line of lines.slice(appMarkerAt + 1)) {
|
|
2077
|
+
const k = THINKALOUD_KEY_LINE.exec(line);
|
|
2078
|
+
const name = k === null ? undefined : [...CARDS_TAIL_KEYS].find((x) => x.toLowerCase() === k[1].toLowerCase());
|
|
2079
|
+
if (name !== undefined && k !== null && keys[name] === undefined)
|
|
2080
|
+
keys[name] = k[2].trim();
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return {
|
|
2084
|
+
lines,
|
|
2085
|
+
preamble,
|
|
2086
|
+
...(cardsMarkerAt < 0 ? {} : { cardsMarkerAt }),
|
|
2087
|
+
...(appMarkerAt < 0 ? {} : { appMarkerAt }),
|
|
2088
|
+
cardLines,
|
|
2089
|
+
keys,
|
|
2090
|
+
dataRecords,
|
|
2091
|
+
acceptLines,
|
|
2092
|
+
ruleLines,
|
|
2093
|
+
guessLines,
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
export function gateMercuryCards(raw, o) {
|
|
2097
|
+
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
2098
|
+
return Result.Err("parse:empty");
|
|
2099
|
+
const soft = {};
|
|
2100
|
+
const miss = (part, what) => {
|
|
2101
|
+
soft[part] = [...(soft[part] ?? []), what];
|
|
2102
|
+
};
|
|
2103
|
+
const p = parseMercuryCards(raw);
|
|
2104
|
+
if (p.cardsMarkerAt === undefined)
|
|
2105
|
+
miss("cardsMarker", "missing");
|
|
2106
|
+
if (p.appMarkerAt === undefined)
|
|
2107
|
+
miss("appMarker", "missing");
|
|
2108
|
+
if (p.preamble.length === 0)
|
|
2109
|
+
miss("preamble", "empty");
|
|
2110
|
+
if (p.cardLines.length === 0)
|
|
2111
|
+
miss("cards", "none");
|
|
2112
|
+
const caps = [];
|
|
2113
|
+
for (const c of thinkAloudList(p.keys.capabilities).map((x) => x.toLowerCase())) {
|
|
2114
|
+
const flag = MERCURY_FLAGS.find((f) => f.toLowerCase() === c);
|
|
2115
|
+
if (flag === undefined)
|
|
2116
|
+
miss("capabilities", "unknown");
|
|
2117
|
+
else if (!caps.includes(flag))
|
|
2118
|
+
caps.push(flag);
|
|
2119
|
+
}
|
|
2120
|
+
const named = thinkAloudScalar(p.keys.archetype)
|
|
2121
|
+
.toLowerCase()
|
|
2122
|
+
.match(/[a-z]+/)?.[0] ?? "";
|
|
2123
|
+
const known = MERCURY_ARCHETYPES.find((a) => a === named);
|
|
2124
|
+
if (known === undefined)
|
|
2125
|
+
miss("archetype", "default");
|
|
2126
|
+
const rShape = parseMercuryDraftShape({
|
|
2127
|
+
archetype: known ?? MERCURY_THINKALOUD_DEFAULT_ARCHETYPE,
|
|
2128
|
+
coreAction: o.personWords.trim(),
|
|
2129
|
+
recordNames: p.dataRecords.slice(0, 5),
|
|
2130
|
+
capabilities: caps,
|
|
2131
|
+
}, o.personWords);
|
|
2132
|
+
if (rShape.isErr())
|
|
2133
|
+
return Result.Err(`classification:${rShape.Err().message}`);
|
|
2134
|
+
const decoded = MERCURY_ADDRESS.decode({
|
|
2135
|
+
...(p.keys.title === undefined ? {} : { title: thinkAloudScalar(p.keys.title) }),
|
|
2136
|
+
slugs: thinkAloudList(p.keys.slugs),
|
|
2137
|
+
});
|
|
2138
|
+
if (decoded.isErr())
|
|
2139
|
+
miss("pairs", decoded.Err().message);
|
|
2140
|
+
const d = decoded.isOk() ? decoded.Ok() : { slugs: [], repairs: [] };
|
|
2141
|
+
const strict = d.title === undefined ? undefined : gateMercuryAddress({ title: d.title, slugs: d.slugs });
|
|
2142
|
+
const loose = strict?.isOk() === true ? strict.Ok() : looseMercuryAddress(d.title, d.slugs);
|
|
2143
|
+
if (strict?.isErr() === true && loose !== undefined)
|
|
2144
|
+
miss("pairs", strict.Err().message);
|
|
2145
|
+
if (loose === undefined)
|
|
2146
|
+
miss("pairs", "default");
|
|
2147
|
+
const address = loose ?? { pairs: [], fallback: true, tagged: false };
|
|
2148
|
+
if (o.askRooms && p.keys.rooms === undefined)
|
|
2149
|
+
miss("rooms", "default");
|
|
2150
|
+
if (o.askRooms && p.keys.font === undefined)
|
|
2151
|
+
miss("font", "default");
|
|
2152
|
+
return Result.Ok({
|
|
2153
|
+
shape: rShape.Ok(),
|
|
2154
|
+
reply: raw,
|
|
2155
|
+
parsed: p,
|
|
2156
|
+
address,
|
|
2157
|
+
addressRepair: [...d.repairs, ...(address.fallback && address.pairs.length > 0 ? ["slug-fallback"] : [])],
|
|
2158
|
+
...(o.askRooms && p.keys.rooms !== undefined ? { rooms: gateMercuryRooms(thinkAloudList(p.keys.rooms)) } : {}),
|
|
2159
|
+
...(o.askRooms && p.keys.font !== undefined ? { font: gateMercuryFont(thinkAloudScalar(p.keys.font)) } : {}),
|
|
2160
|
+
softMisses: soft,
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
export const MERCURY_FIM_MODEL = "mercury-coder";
|
|
2164
|
+
export const MERCURY_FIM_URL = "https://api.inceptionlabs.ai/v1/fim/completions";
|
|
2165
|
+
export const MERCURY_FIM_MAX_TOKENS = 2000;
|
|
2166
|
+
export const MERCURY_FIM_STOP = ['"', "\n"];
|
|
2167
|
+
export const MERCURY_FIM_CALL_DEADLINE_MS = 20_000;
|
|
2168
|
+
export const MERCURY_FIM_BUDGET_MS = 40_000;
|
|
2169
|
+
export const MERCURY_FIM_CONCURRENCY = 8;
|
|
2170
|
+
export const MERCURY_FIM_PRICE = { inputUsdPerMTok: 0.25, outputUsdPerMTok: 1 };
|
|
2171
|
+
export function mercuryFimUsageRound(fim) {
|
|
2172
|
+
if (fim.promptTokens === 0 && fim.completionTokens === 0)
|
|
2173
|
+
return undefined;
|
|
2174
|
+
return {
|
|
2175
|
+
prompt_tokens: fim.promptTokens,
|
|
2176
|
+
completion_tokens: fim.completionTokens,
|
|
2177
|
+
total_tokens: fim.promptTokens + fim.completionTokens,
|
|
2178
|
+
cost: (fim.promptTokens * MERCURY_FIM_PRICE.inputUsdPerMTok + fim.completionTokens * MERCURY_FIM_PRICE.outputUsdPerMTok) /
|
|
2179
|
+
1_000_000,
|
|
2180
|
+
model: MERCURY_FIM_MODEL,
|
|
2181
|
+
role: "mercury",
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
export const MERCURY_FIM_WAVES = ["see", "expect"];
|
|
2185
|
+
export const MERCURY_FIM_SEEDS = {
|
|
2186
|
+
see: 'See: "I open this and I see',
|
|
2187
|
+
expect: 'Expect: "When I do this, I expect',
|
|
2188
|
+
};
|
|
2189
|
+
export function mercuryFimHeader(personWords) {
|
|
2190
|
+
return `# Requirements as user-story cards. Under each card, a usability tester narrates in the first person what she sees when she opens it and what she expects to happen when she does it.\n# The app creator's initial prompt: "${personWords}"\n\n`;
|
|
2191
|
+
}
|
|
2192
|
+
export function renderMercuryCardsFills(o) {
|
|
2193
|
+
const out = [];
|
|
2194
|
+
for (let i = 0; i < o.bodyEnd; i += 1) {
|
|
2195
|
+
out.push(o.lines[i]);
|
|
2196
|
+
if (o.cardLines.includes(i) === false)
|
|
2197
|
+
continue;
|
|
2198
|
+
for (const kind of MERCURY_FIM_WAVES) {
|
|
2199
|
+
const k = `${i}:${kind}`;
|
|
2200
|
+
if (o.gap === k)
|
|
2201
|
+
out.push(` ${MERCURY_FIM_SEEDS[kind]}\u0000`);
|
|
2202
|
+
else if (o.fills.has(k))
|
|
2203
|
+
out.push(` ${MERCURY_FIM_SEEDS[kind]}${o.fills.get(k)}"`);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return out.join("\n");
|
|
2207
|
+
}
|
|
2208
|
+
export function mercuryFimRequest(o) {
|
|
2209
|
+
const t = renderMercuryCardsFills(o);
|
|
2210
|
+
const at = t.indexOf("\u0000");
|
|
2211
|
+
return {
|
|
2212
|
+
model: MERCURY_FIM_MODEL,
|
|
2213
|
+
prompt: mercuryFimHeader(o.personWords) + t.slice(0, at),
|
|
2214
|
+
suffix: `"${t.slice(at + 1)}`,
|
|
2215
|
+
max_tokens: MERCURY_FIM_MAX_TOKENS,
|
|
2216
|
+
stop: [...MERCURY_FIM_STOP],
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
const fimBody = lazyType(() => type({
|
|
2220
|
+
choices: type({ "text?": "string | null", "message?": { content: "string | null" } }).array(),
|
|
2221
|
+
"usage?": { "prompt_tokens?": "number", "completion_tokens?": "number" },
|
|
2222
|
+
}));
|
|
2223
|
+
async function mercuryFimCall(fetchImpl, o) {
|
|
2224
|
+
const abort = new AbortController();
|
|
2225
|
+
const timer = setTimeout(function expire() {
|
|
2226
|
+
abort.abort();
|
|
2227
|
+
}, o.deadlineMs);
|
|
2228
|
+
function cancelled() {
|
|
2229
|
+
abort.abort();
|
|
2230
|
+
}
|
|
2231
|
+
if (o.cancel?.aborted === true)
|
|
2232
|
+
abort.abort();
|
|
2233
|
+
o.cancel?.addEventListener("abort", cancelled, { once: true });
|
|
2234
|
+
const startedAt = Date.now();
|
|
2235
|
+
let last = "";
|
|
2236
|
+
try {
|
|
2237
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2238
|
+
if (abort.signal.aborted)
|
|
2239
|
+
break;
|
|
2240
|
+
const rRes = await exception2Result(() => fetchImpl(o.url ?? MERCURY_FIM_URL, {
|
|
2241
|
+
method: "POST",
|
|
2242
|
+
headers: { authorization: `Bearer ${o.apiKey}`, "content-type": "application/json" },
|
|
2243
|
+
body: JSON.stringify(o.body),
|
|
2244
|
+
signal: abort.signal,
|
|
2245
|
+
}));
|
|
2246
|
+
if (rRes.isErr()) {
|
|
2247
|
+
last = abort.signal.aborted ? `timeout after ${Date.now() - startedAt} ms` : `network: ${rRes.Err().message.slice(0, 120)}`;
|
|
2248
|
+
if (abort.signal.aborted)
|
|
2249
|
+
break;
|
|
2250
|
+
}
|
|
2251
|
+
else {
|
|
2252
|
+
const res = rRes.Ok();
|
|
2253
|
+
if (res.ok) {
|
|
2254
|
+
const rJson = await exception2Result(() => res.json());
|
|
2255
|
+
if (rJson.isErr())
|
|
2256
|
+
return { kind: "failed", error: `body: ${rJson.Err().message.slice(0, 120)}` };
|
|
2257
|
+
const body = fimBody(rJson.Ok());
|
|
2258
|
+
if (body instanceof type.errors)
|
|
2259
|
+
return { kind: "failed", error: `reply: ${body.summary.slice(0, 120)}` };
|
|
2260
|
+
const c = body.choices[0];
|
|
2261
|
+
const text = c?.text ?? c?.message?.content ?? null;
|
|
2262
|
+
if (text === null || text === undefined)
|
|
2263
|
+
return { kind: "failed", error: "reply: no text" };
|
|
2264
|
+
return {
|
|
2265
|
+
kind: "fill",
|
|
2266
|
+
text,
|
|
2267
|
+
promptTokens: body.usage?.prompt_tokens ?? 0,
|
|
2268
|
+
completionTokens: body.usage?.completion_tokens ?? 0,
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
const { snippet } = await readFailureBody(res);
|
|
2272
|
+
last = `status ${res.status}${snippet.length === 0 ? "" : ` ${snippet.slice(0, 120)}`}`;
|
|
2273
|
+
if (MERCURY_TRANSIENT_STATUSES.has(res.status) === false)
|
|
2274
|
+
break;
|
|
2275
|
+
}
|
|
2276
|
+
const wait = 1000 * 2 ** attempt;
|
|
2277
|
+
if (Date.now() - startedAt + wait >= o.deadlineMs)
|
|
2278
|
+
break;
|
|
2279
|
+
await waitMs(wait);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
finally {
|
|
2283
|
+
clearTimeout(timer);
|
|
2284
|
+
o.cancel?.removeEventListener("abort", cancelled);
|
|
2285
|
+
}
|
|
2286
|
+
return { kind: "failed", error: last.length === 0 ? "aborted" : last };
|
|
2287
|
+
}
|
|
2288
|
+
export async function mercuryCardsFim(vctx, o) {
|
|
2289
|
+
const p = o.draft.parsed;
|
|
2290
|
+
const startedAt = Date.now();
|
|
2291
|
+
const bodyEnd = p.appMarkerAt ?? p.lines.length;
|
|
2292
|
+
const base = {
|
|
2293
|
+
model: MERCURY_FIM_MODEL,
|
|
2294
|
+
cards: p.cardLines.length,
|
|
2295
|
+
slots: p.cardLines.length * MERCURY_FIM_WAVES.length,
|
|
2296
|
+
filled: 0,
|
|
2297
|
+
empty: 0,
|
|
2298
|
+
failed: 0,
|
|
2299
|
+
promptTokens: 0,
|
|
2300
|
+
completionTokens: 0,
|
|
2301
|
+
};
|
|
2302
|
+
if (o.apiKey === undefined || o.apiKey.length === 0)
|
|
2303
|
+
return { reply: o.draft.reply, receipt: { status: "no-key", ...base, ms: Date.now() - startedAt } };
|
|
2304
|
+
if (p.cardLines.length === 0)
|
|
2305
|
+
return { reply: o.draft.reply, receipt: { status: "no-cards", ...base, ms: Date.now() - startedAt } };
|
|
2306
|
+
const apiKey = o.apiKey;
|
|
2307
|
+
const budgetMs = o.budgetMs ?? MERCURY_FIM_BUDGET_MS;
|
|
2308
|
+
const callMs = o.deadlineMs ?? MERCURY_FIM_CALL_DEADLINE_MS;
|
|
2309
|
+
const fills = new Map();
|
|
2310
|
+
const errors = [];
|
|
2311
|
+
let promptTokens = 0;
|
|
2312
|
+
let completionTokens = 0;
|
|
2313
|
+
let empty = 0;
|
|
2314
|
+
for (const kind of MERCURY_FIM_WAVES) {
|
|
2315
|
+
const slots = p.cardLines.map((i) => `${i}:${kind}`);
|
|
2316
|
+
const got = new Map();
|
|
2317
|
+
let next = 0;
|
|
2318
|
+
const worker = async () => {
|
|
2319
|
+
while (next < slots.length) {
|
|
2320
|
+
const gap = slots[next];
|
|
2321
|
+
next += 1;
|
|
2322
|
+
const remaining = budgetMs - (Date.now() - startedAt);
|
|
2323
|
+
if (remaining <= 0 || o.cancel?.aborted === true) {
|
|
2324
|
+
errors.push(`${gap}: ${o.cancel?.aborted === true ? "cancelled" : "budget spent"}`);
|
|
2325
|
+
continue;
|
|
2326
|
+
}
|
|
2327
|
+
const body = mercuryFimRequest({ personWords: o.personWords, lines: p.lines, bodyEnd, cardLines: p.cardLines, fills, gap });
|
|
2328
|
+
const r = await mercuryFimCall(vctx.inferenceFetch, {
|
|
2329
|
+
apiKey,
|
|
2330
|
+
body,
|
|
2331
|
+
deadlineMs: Math.min(callMs, remaining),
|
|
2332
|
+
...(o.url === undefined ? {} : { url: o.url }),
|
|
2333
|
+
...(o.cancel === undefined ? {} : { cancel: o.cancel }),
|
|
2334
|
+
});
|
|
2335
|
+
if (r.kind === "failed") {
|
|
2336
|
+
errors.push(`${gap}: ${r.error}`);
|
|
2337
|
+
continue;
|
|
2338
|
+
}
|
|
2339
|
+
got.set(gap, r.text);
|
|
2340
|
+
promptTokens += r.promptTokens;
|
|
2341
|
+
completionTokens += r.completionTokens;
|
|
2342
|
+
if (r.text.length === 0)
|
|
2343
|
+
empty += 1;
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
await Promise.all(Array.from({ length: Math.min(MERCURY_FIM_CONCURRENCY, slots.length) }, worker));
|
|
2347
|
+
for (const [k, v] of got)
|
|
2348
|
+
fills.set(k, v);
|
|
2349
|
+
}
|
|
2350
|
+
const body = renderMercuryCardsFills({ lines: p.lines, bodyEnd, cardLines: p.cardLines, fills });
|
|
2351
|
+
const tail = p.lines.slice(bodyEnd);
|
|
2352
|
+
const reply = tail.length === 0 ? body : `${body}\n${tail.join("\n")}`;
|
|
2353
|
+
const filled = fills.size;
|
|
2354
|
+
return {
|
|
2355
|
+
reply,
|
|
2356
|
+
receipt: {
|
|
2357
|
+
status: filled > 0 ? "ok" : "failed",
|
|
2358
|
+
...base,
|
|
2359
|
+
filled,
|
|
2360
|
+
empty,
|
|
2361
|
+
failed: base.slots - filled,
|
|
2362
|
+
ms: Date.now() - startedAt,
|
|
2363
|
+
promptTokens,
|
|
2364
|
+
completionTokens,
|
|
2365
|
+
...(errors.length === 0 ? {} : { errors: errors.slice(0, 20) }),
|
|
2366
|
+
},
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
export function assembleMercuryCardsCreateVibe(o) {
|
|
2370
|
+
const s = o.draft.shape;
|
|
2371
|
+
const nature = mercuryNature(s, o.personWords);
|
|
2372
|
+
const rooms = sanitizeRoomShortlist("pouf", o.draft.rooms?.rooms);
|
|
2373
|
+
return {
|
|
2374
|
+
prompt: o.personWords,
|
|
2375
|
+
enrichedPrompt: mercuryTesterReplyEnrichedPrompt(o.reply, o.personWords),
|
|
2376
|
+
skills: mercurySkills(s, { personWords: o.personWords }),
|
|
2377
|
+
...(o.draft.address.pairs.length === 0 ? {} : { pairs: o.draft.address.pairs.map((p) => ({ title: p.title, slug: p.slug })) }),
|
|
2378
|
+
nature,
|
|
2379
|
+
...(nature === "sensitive" ? { privacySensitive: true } : {}),
|
|
2380
|
+
...(rooms === undefined ? {} : { rooms: [...rooms] }),
|
|
2381
|
+
...(o.draft.font?.font === undefined ? {} : { font: o.draft.font.font }),
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
export function mercuryCardsPreamble(draft, personWords) {
|
|
2385
|
+
if (draft.parsed.preamble.length > 0)
|
|
2386
|
+
return draft.parsed.preamble;
|
|
2387
|
+
return `I'm making ${draft.address.pairs[0]?.title ?? "your app"} for you: ${personWords}.`;
|
|
2388
|
+
}
|
|
2389
|
+
function takeOf(g) {
|
|
2390
|
+
return {
|
|
2391
|
+
opening: g.texts.get("opening") ?? "",
|
|
2392
|
+
access: g.texts.get("access") ?? "",
|
|
2393
|
+
seed: g.texts.get("seed") ?? "",
|
|
2394
|
+
look: g.texts.get("look") ?? "",
|
|
2395
|
+
...(g.records?.screens === undefined ? {} : { records: g.records.screens }),
|
|
2396
|
+
...(g.rules?.screens === undefined ? {} : { rules: g.rules.screens }),
|
|
2397
|
+
...(g.screens?.screens === undefined ? {} : { screens: g.screens.screens }),
|
|
2398
|
+
...(g.story?.story === undefined ? {} : { story: g.story.story }),
|
|
2399
|
+
...(g.preamble === undefined ? {} : { preamble: g.preamble }),
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
function owedBriefFields(gated) {
|
|
2403
|
+
return MERCURY_BRIEF_FIELDS.filter((f) => (f === "pairs" ? gated.pairs === undefined : gated.texts.has(f) === false));
|
|
2404
|
+
}
|
|
2405
|
+
const replyObject = lazyType(() => type("Record<string, unknown>"));
|
|
2406
|
+
export function coerceMercuryText(v) {
|
|
2407
|
+
if (typeof v === "string")
|
|
2408
|
+
return { text: v, coerced: false };
|
|
2409
|
+
const leaves = (x) => {
|
|
2410
|
+
if (typeof x === "string")
|
|
2411
|
+
return x.trim().length === 0 ? undefined : x;
|
|
2412
|
+
if (Array.isArray(x)) {
|
|
2413
|
+
const parts = x.map(leaves).filter((t) => t !== undefined);
|
|
2414
|
+
return parts.length === 0 ? undefined : parts.join("; ");
|
|
2415
|
+
}
|
|
2416
|
+
if (x !== null && typeof x === "object") {
|
|
2417
|
+
const parts = Object.entries(x).flatMap(([k, val]) => {
|
|
2418
|
+
const t = leaves(val);
|
|
2419
|
+
return t === undefined ? [] : [`${k}: ${t}`];
|
|
2420
|
+
});
|
|
2421
|
+
return parts.length === 0 ? undefined : parts.join("; ");
|
|
2422
|
+
}
|
|
2423
|
+
return undefined;
|
|
2424
|
+
};
|
|
2425
|
+
if (v === null || typeof v !== "object")
|
|
2426
|
+
return { coerced: false };
|
|
2427
|
+
const text = leaves(v);
|
|
2428
|
+
return text === undefined ? { coerced: false } : { text, coerced: true };
|
|
2429
|
+
}
|
|
2430
|
+
export const MERCURY_DROP_OVER_BOUND = 3;
|
|
2431
|
+
function titleFromSlug(slug) {
|
|
2432
|
+
return slug
|
|
2433
|
+
.split("-")
|
|
2434
|
+
.filter((w) => w.length > 0)
|
|
2435
|
+
.slice(0, 4)
|
|
2436
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
2437
|
+
.join(" ");
|
|
2438
|
+
}
|
|
2439
|
+
function looseMercuryAddress(title, slugs) {
|
|
2440
|
+
const cut = stripMercuryArchetypeTag((title ?? "").trim());
|
|
2441
|
+
const own = cut.text.trim().replace(/^["*]+|["*]+$/g, "");
|
|
2442
|
+
const kept = [...new Set(slugs.map(kebab).filter((x) => x.length > 0))].slice(0, 3);
|
|
2443
|
+
const name = own.length > 0 && kebab(own).length > 0 ? own : kept.length > 0 ? titleFromSlug(kept[0]) : "";
|
|
2444
|
+
if (name.length === 0)
|
|
2445
|
+
return undefined;
|
|
2446
|
+
const sent = own.length > 0 && cut.repaired ? (title ?? "").trim() : name;
|
|
2447
|
+
const addresses = kept.length > 0 ? kept : [kebab(name)];
|
|
2448
|
+
return { pairs: addresses.map((slug) => ({ title: sent, slug })), fallback: kept.length === 0, tagged: cut.repaired };
|
|
2449
|
+
}
|
|
2450
|
+
function acceptBriefFields(json, o) {
|
|
2451
|
+
const parsed = replyObject(json);
|
|
2452
|
+
const notAnObject = Array.isArray(json) || parsed instanceof type.errors;
|
|
2453
|
+
const salvaged = notAnObject ? MERCURY_ADDRESS.salvage(json) : undefined;
|
|
2454
|
+
const body = notAnObject ? (salvaged ?? {}) : parsed;
|
|
2455
|
+
const failed = [];
|
|
2456
|
+
const keepsNothing = o.shape.archetype === "toy" && o.shape.recordNames.length === 0;
|
|
2457
|
+
const soft = (field, misses) => {
|
|
2458
|
+
if (misses.length === 0)
|
|
2459
|
+
return;
|
|
2460
|
+
o.into.softMisses.set(field, [...(o.into.softMisses.get(field) ?? []), ...misses]);
|
|
2461
|
+
};
|
|
2462
|
+
const text = (key) => {
|
|
2463
|
+
const c = coerceMercuryText(body[key]);
|
|
2464
|
+
if (c.coerced)
|
|
2465
|
+
o.into.coerced.add(key);
|
|
2466
|
+
return c.text;
|
|
2467
|
+
};
|
|
2468
|
+
if (o.roomsOwed === true && "rooms" in body)
|
|
2469
|
+
o.into.rooms = gateMercuryRooms(body.rooms);
|
|
2470
|
+
if (o.fontOwed === true && "font" in body)
|
|
2471
|
+
o.into.font = gateMercuryFont(body.font);
|
|
2472
|
+
if (o.screensOwed === true && "screens" in body) {
|
|
2473
|
+
const t = text("screens");
|
|
2474
|
+
o.into.screens = gateMercuryScreens(t ?? body.screens);
|
|
2475
|
+
}
|
|
2476
|
+
if (o.recordsOwed === true) {
|
|
2477
|
+
for (const key of ["records", "rules"]) {
|
|
2478
|
+
if (o.into[key] !== undefined)
|
|
2479
|
+
continue;
|
|
2480
|
+
if (!(key in body)) {
|
|
2481
|
+
soft(key, ["not-a-string"]);
|
|
2482
|
+
continue;
|
|
2483
|
+
}
|
|
2484
|
+
const t = text(key);
|
|
2485
|
+
o.into[key] = gateMercuryScreens(t ?? body[key]);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
if (o.storyOwed === true && "story" in body) {
|
|
2489
|
+
const t = text("story");
|
|
2490
|
+
const g = gateMercuryStory(t ?? body.story);
|
|
2491
|
+
o.into.story = g;
|
|
2492
|
+
soft("story", g.misses ?? []);
|
|
2493
|
+
}
|
|
2494
|
+
if (o.preambleOwed === true && "preamble" in body) {
|
|
2495
|
+
const t = text("preamble");
|
|
2496
|
+
if (t === undefined || cleanMercuryText(t).length === 0)
|
|
2497
|
+
soft("preamble", ["empty"]);
|
|
2498
|
+
else {
|
|
2499
|
+
soft("preamble", gateMercuryWholePreamble(t));
|
|
2500
|
+
o.into.preamble = cleanMercuryText(t);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
for (const f of o.owed) {
|
|
2504
|
+
if (f === "pairs") {
|
|
2505
|
+
const decoded = MERCURY_ADDRESS.decode(body);
|
|
2506
|
+
if (decoded.isErr()) {
|
|
2507
|
+
failed.push(`pairs:${decoded.Err().message}`);
|
|
2508
|
+
continue;
|
|
2509
|
+
}
|
|
2510
|
+
const d = decoded.Ok();
|
|
2511
|
+
if (salvaged !== undefined)
|
|
2512
|
+
o.into.addressRepair.add("salvage");
|
|
2513
|
+
for (const r of d.repairs)
|
|
2514
|
+
o.into.addressRepair.add(r);
|
|
2515
|
+
const slugs = d.slugs.length > 0 ? d.slugs : (o.into.heldSlugs ?? []);
|
|
2516
|
+
if (d.title === undefined && d.slugs.length > 0)
|
|
2517
|
+
o.into.heldSlugs = d.slugs;
|
|
2518
|
+
if (d.title === undefined && failed.length > 0) {
|
|
2519
|
+
failed.push("pairs:title-missing");
|
|
2520
|
+
continue;
|
|
2521
|
+
}
|
|
2522
|
+
const strict = d.title === undefined ? Result.Err("title-missing") : gateMercuryAddress({ title: d.title, slugs });
|
|
2523
|
+
const address = strict.isOk() ? strict.Ok() : looseMercuryAddress(d.title, slugs);
|
|
2524
|
+
if (address === undefined) {
|
|
2525
|
+
failed.push(`pairs:${strict.isErr() ? strict.Err().message : "empty"}`);
|
|
2526
|
+
continue;
|
|
2527
|
+
}
|
|
2528
|
+
if (strict.isErr()) {
|
|
2529
|
+
soft("pairs", [strict.Err().message]);
|
|
2530
|
+
if (d.title === undefined)
|
|
2531
|
+
o.into.addressRepair.add("title-from-slug");
|
|
2532
|
+
}
|
|
2533
|
+
if (address.tagged)
|
|
2534
|
+
o.into.misses.add("archetype-tag");
|
|
2535
|
+
if (address.fallback)
|
|
2536
|
+
o.into.addressRepair.add("slug-fallback");
|
|
2537
|
+
o.into.pairs = address.pairs;
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
const v = text(replyKey(f));
|
|
2541
|
+
if (v === undefined) {
|
|
2542
|
+
if (f === "opening")
|
|
2543
|
+
failed.push("opening:not-a-string");
|
|
2544
|
+
else
|
|
2545
|
+
soft(f, ["not-a-string"]);
|
|
2546
|
+
continue;
|
|
2547
|
+
}
|
|
2548
|
+
const prepared = prepareMercuryField(f, v);
|
|
2549
|
+
const fails = gateMercuryField({ field: f, value: prepared.gate, shape: o.shape });
|
|
2550
|
+
if (f === "opening" && cleanMercuryText(prepared.value).length === 0) {
|
|
2551
|
+
failed.push("opening:empty");
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
const [, hi] = FIELD_BOUNDS[f];
|
|
2555
|
+
if (cleanMercuryText(prepared.value).length > hi * MERCURY_DROP_OVER_BOUND) {
|
|
2556
|
+
if (f === "opening") {
|
|
2557
|
+
failed.push("opening:long");
|
|
2558
|
+
continue;
|
|
2559
|
+
}
|
|
2560
|
+
o.into.fieldsDropped.add(f);
|
|
2561
|
+
o.into.texts.set(f, "");
|
|
2562
|
+
continue;
|
|
2563
|
+
}
|
|
2564
|
+
soft(f, fails);
|
|
2565
|
+
o.into.texts.set(f, prepared.value);
|
|
2566
|
+
if (f !== "seed" || keepsNothing === false)
|
|
2567
|
+
for (const m of prepared.misses)
|
|
2568
|
+
o.into.misses.add(m);
|
|
2569
|
+
}
|
|
2570
|
+
return failed;
|
|
2571
|
+
}
|
|
2572
|
+
function softMissCount(g) {
|
|
2573
|
+
return [...g.softMisses.values()].reduce((n, m) => n + m.length, 0);
|
|
2574
|
+
}
|
|
2575
|
+
export const MERCURY_BRIEF_FANOUT = 4;
|
|
2576
|
+
export const MERCURY_SHAPE_MAX_TOKENS = 800;
|
|
2577
|
+
export const MERCURY_PICK_WAIT_MS = 1500;
|
|
2578
|
+
export const MERCURY_PICK_COUNT = 2;
|
|
2579
|
+
export const MERCURY_DUO_PICK_COUNT = 3;
|
|
2580
|
+
export const MERCURY_ONECALL_EXTRA_TOKENS = 400;
|
|
2581
|
+
const BREADTH_CTRL = /\b(button|tap|taps|toggle|picker|stepper|chip|chips|slider|drag|swipe|stars|ribbon|stamp|badge|bars?|search|filter|tabs?|headline|confetti|animation|counter|dial|segmented|checkbox|strip|gauge|sparkline|progress|swatch|card|cards|input|field|dropdown|menu|list)\b/gi;
|
|
2582
|
+
const BREADTH_STATE = /\b(overdue|sold out|resolved|streak|late|red|amber|green|dims|highlight\w*|pinned|large|big|biggest|newest|empty|disabled|colou?r|glow\w*|pulse\w*|flash\w*|turns|changes|pending|confirmed|done|active|fermenting|winning|leading|second|behind|waiting|missing|ahead|nearly|just|still|recently)\b/gi;
|
|
2583
|
+
const BREADTH_DEEP = /\b(undo|long[- ]press|press and hold|drag(s|ged|ging)? to reorder|reorder|inline edit|edit(s|ed)? inline|in place|filter chips?|swipe)\b/gi;
|
|
2584
|
+
function breadthNorm(w) {
|
|
2585
|
+
const l = w.toLowerCase();
|
|
2586
|
+
return l.length > 4 ? l.replace(/(es|s)$/, "") : l;
|
|
2587
|
+
}
|
|
2588
|
+
function distinctTerms(re, text) {
|
|
2589
|
+
return new Set([...text.matchAll(re)].map((m) => breadthNorm(m[0]))).size;
|
|
2590
|
+
}
|
|
2591
|
+
export function mercuryBreadth(o) {
|
|
2592
|
+
const t = o.screens ?? [o.opening, o.seed ?? "", o.access ?? ""].filter((p) => p.length > 0).join(" ");
|
|
2593
|
+
return distinctTerms(BREADTH_CTRL, t) + distinctTerms(BREADTH_STATE, t) + 2 * distinctTerms(BREADTH_DEEP, t);
|
|
2594
|
+
}
|
|
2595
|
+
function freshGated() {
|
|
2596
|
+
return {
|
|
2597
|
+
texts: new Map(),
|
|
2598
|
+
misses: new Set(),
|
|
2599
|
+
addressRepair: new Set(),
|
|
2600
|
+
softMisses: new Map(),
|
|
2601
|
+
coerced: new Set(),
|
|
2602
|
+
fieldsDropped: new Set(),
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
function gatedCount(g) {
|
|
2606
|
+
return MERCURY_BRIEF_FIELDS.length - owedBriefFields(g).length;
|
|
2607
|
+
}
|
|
2608
|
+
function billedTotals(usage, aborted) {
|
|
2609
|
+
const sum = (f) => usage.reduce((n, u) => n + f(u), 0);
|
|
2610
|
+
const scale = usage.length === 0 ? 0 : (usage.length + aborted) / usage.length;
|
|
2611
|
+
const costs = usage.filter((u) => u.cost !== undefined);
|
|
2612
|
+
return {
|
|
2613
|
+
prompt_tokens: Math.round(sum((u) => u.prompt_tokens) * scale),
|
|
2614
|
+
completion_tokens: Math.round(sum((u) => u.completion_tokens) * scale),
|
|
2615
|
+
total_tokens: Math.round(sum((u) => u.total_tokens) * scale),
|
|
2616
|
+
...(costs.length === 0 ? {} : { cost: sum((u) => u.cost ?? 0) * scale }),
|
|
2617
|
+
estimated: usage.length === 0 ? 0 : aborted,
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
async function mercuryBriefCall(vctx, o) {
|
|
2621
|
+
const startedAt = Date.now();
|
|
2622
|
+
const cancel = new AbortController();
|
|
2623
|
+
const drafts = Array.from({ length: o.fanout }, (_, index) => ({
|
|
2624
|
+
index,
|
|
2625
|
+
words: index === 0 || o.words.length === 0 ? o.words : o.wordBag(o.words.length),
|
|
2626
|
+
gated: freshGated(),
|
|
2627
|
+
log: mercurySendLog(),
|
|
2628
|
+
totals: { usage: [] },
|
|
2629
|
+
valid: false,
|
|
2630
|
+
aborted: false,
|
|
2631
|
+
}));
|
|
2632
|
+
const roomsOwed = (g) => o.askRooms && g.rooms === undefined;
|
|
2633
|
+
const fontOwed = (g) => o.askRooms && g.font === undefined;
|
|
2634
|
+
const screensOwed = (g) => o.depth === "sub-feature" && g.screens === undefined;
|
|
2635
|
+
const storyOwed = (g) => o.reply === "whole" && g.story === undefined;
|
|
2636
|
+
const preambleOwed = (g) => o.reply === "whole" && g.preamble === undefined;
|
|
2637
|
+
const recordsOwed = (g) => o.recordsRules && (g.records === undefined || g.rules === undefined);
|
|
2638
|
+
const request = (g, words, draft) => () => {
|
|
2639
|
+
const owed = owedBriefFields(g);
|
|
2640
|
+
const rooms = roomsOwed(g);
|
|
2641
|
+
const font = fontOwed(g);
|
|
2642
|
+
const screens = screensOwed(g);
|
|
2643
|
+
const story = storyOwed(g);
|
|
2644
|
+
const preamble = preambleOwed(g);
|
|
2645
|
+
const records = recordsOwed(g);
|
|
2646
|
+
const shape = g.shape ?? o.shape;
|
|
2647
|
+
const opts = {
|
|
2648
|
+
personWords: o.personWords,
|
|
2649
|
+
fields: owed,
|
|
2650
|
+
words,
|
|
2651
|
+
style: o.style,
|
|
2652
|
+
rooms,
|
|
2653
|
+
font,
|
|
2654
|
+
...(o.juice === "examples" ? {} : { juice: o.juice }),
|
|
2655
|
+
...(o.depth === "none" ? {} : { depth: o.depth }),
|
|
2656
|
+
screens,
|
|
2657
|
+
...(o.reply === "split" ? {} : { reply: o.reply, story, preamble }),
|
|
2658
|
+
...(o.recordsRules ? { recordsRules: true, recordsOwed: records } : {}),
|
|
2659
|
+
};
|
|
2660
|
+
return {
|
|
2661
|
+
schema: mercuryBriefSchema(owed, {
|
|
2662
|
+
rooms,
|
|
2663
|
+
font,
|
|
2664
|
+
...(screens ? { screens } : {}),
|
|
2665
|
+
...(records ? { recordsRules: true } : {}),
|
|
2666
|
+
...(story ? { story } : {}),
|
|
2667
|
+
...(preamble ? { preamble } : {}),
|
|
2668
|
+
...(shape === undefined ? { classify: true } : {}),
|
|
2669
|
+
}),
|
|
2670
|
+
messages: shape === undefined ? mercuryOneCallBriefMessages({ ...opts, draft }) : mercuryBriefMessages({ ...opts, shape }),
|
|
2671
|
+
};
|
|
2672
|
+
};
|
|
2673
|
+
const gate = (json, g) => {
|
|
2674
|
+
if (g.shape === undefined && o.shape === undefined) {
|
|
2675
|
+
const parsed = parseMercuryDraftShape(json, o.personWords);
|
|
2676
|
+
if (parsed.isErr())
|
|
2677
|
+
return [`classification:${parsed.Err().message}`];
|
|
2678
|
+
g.shape = parsed.Ok();
|
|
2679
|
+
}
|
|
2680
|
+
const shape = g.shape ?? o.shape;
|
|
2681
|
+
if (shape === undefined)
|
|
2682
|
+
return ["classification:missing"];
|
|
2683
|
+
return acceptBriefFields(json, {
|
|
2684
|
+
owed: owedBriefFields(g),
|
|
2685
|
+
shape,
|
|
2686
|
+
into: g,
|
|
2687
|
+
roomsOwed: roomsOwed(g),
|
|
2688
|
+
fontOwed: fontOwed(g),
|
|
2689
|
+
screensOwed: screensOwed(g),
|
|
2690
|
+
storyOwed: storyOwed(g),
|
|
2691
|
+
preambleOwed: preambleOwed(g),
|
|
2692
|
+
recordsOwed: recordsOwed(g),
|
|
2693
|
+
});
|
|
2694
|
+
};
|
|
2695
|
+
const maxTokens = o.planner === "opus"
|
|
2696
|
+
? MERCURY_OPUS_PLANNER_MAX_TOKENS
|
|
2697
|
+
: (o.reply === "whole" ? 2400 : 1600) + (o.shape === undefined ? MERCURY_ONECALL_EXTRA_TOKENS : 0);
|
|
2698
|
+
const attempt = {
|
|
2699
|
+
call: "brief",
|
|
2700
|
+
deadlineMs: o.deadlineMs,
|
|
2701
|
+
budget: o.budget,
|
|
2702
|
+
maxTokens,
|
|
2703
|
+
...(o.planner === "opus" ? { model: plannerModel("opus") } : {}),
|
|
2704
|
+
};
|
|
2705
|
+
let winner;
|
|
2706
|
+
let winnerMs;
|
|
2707
|
+
const candidates = [];
|
|
2708
|
+
let pickTimer;
|
|
2709
|
+
let pickWaitMs;
|
|
2710
|
+
let partner;
|
|
2711
|
+
let duoConflict;
|
|
2712
|
+
const pickCount = o.duo ? o.duoPool : MERCURY_PICK_COUNT;
|
|
2713
|
+
const pickReserveMs = o.reply === "whole" ? 0 : MERCURY_MIN_ATTEMPT_MS;
|
|
2714
|
+
let won = () => undefined;
|
|
2715
|
+
const firstValid = new Promise((resolve) => {
|
|
2716
|
+
won = resolve;
|
|
2717
|
+
});
|
|
2718
|
+
const pick = () => {
|
|
2719
|
+
if (winner !== undefined || candidates.length === 0)
|
|
2720
|
+
return;
|
|
2721
|
+
if (pickTimer !== undefined)
|
|
2722
|
+
clearTimeout(pickTimer);
|
|
2723
|
+
const ranked = [...candidates].sort((a, b) => a.softMisses - b.softMisses || b.breadth - a.breadth);
|
|
2724
|
+
winner = ranked[0].draft;
|
|
2725
|
+
if (o.duo && ranked.length > 1) {
|
|
2726
|
+
const second = ranked[1].draft;
|
|
2727
|
+
const shapeA = winner.gated.shape ?? o.shape;
|
|
2728
|
+
const shapeB = second.gated.shape ?? o.shape;
|
|
2729
|
+
duoConflict =
|
|
2730
|
+
o.recordsRules && shapeA !== undefined && shapeB !== undefined
|
|
2731
|
+
? mercuryDuoConflict({ shape: shapeA, access: winner.gated.texts.get("access") ?? "" }, { shape: shapeB, access: second.gated.texts.get("access") ?? "" }, o.personWords)
|
|
2732
|
+
: undefined;
|
|
2733
|
+
if (duoConflict === undefined)
|
|
2734
|
+
partner = { draft: second, breadth: ranked[1].breadth };
|
|
2735
|
+
}
|
|
2736
|
+
winnerMs = Date.now() - startedAt;
|
|
2737
|
+
for (const s of drafts)
|
|
2738
|
+
if (s.ended === undefined && s.log.inFlight === true)
|
|
2739
|
+
s.aborted = true;
|
|
2740
|
+
cancel.abort();
|
|
2741
|
+
won();
|
|
2742
|
+
};
|
|
2743
|
+
const everyDraft = Promise.allSettled(drafts.map(async function runDraft(d) {
|
|
2744
|
+
const label = `draft ${d.index}: `;
|
|
2745
|
+
const a = await mercuryAttempt(vctx, { ...attempt, request: request(d.gated, d.words, d.index), cancel: cancel.signal, label, transientRetries: 0 }, d.log);
|
|
2746
|
+
if (winner !== undefined)
|
|
2747
|
+
return;
|
|
2748
|
+
d.ended = a.kind;
|
|
2749
|
+
if (a.kind === "cancelled" && a.inFlight)
|
|
2750
|
+
d.aborted = true;
|
|
2751
|
+
if (a.kind !== "reply")
|
|
2752
|
+
return;
|
|
2753
|
+
absorbReply(d.totals, a.reply);
|
|
2754
|
+
d.text = a.reply.text;
|
|
2755
|
+
const failed = gate(a.reply.json, d.gated);
|
|
2756
|
+
if (failed.length === 0) {
|
|
2757
|
+
d.valid = true;
|
|
2758
|
+
const breadth = mercuryBreadth({
|
|
2759
|
+
...(d.gated.screens?.screens === undefined ? {} : { screens: d.gated.screens.screens }),
|
|
2760
|
+
opening: d.gated.texts.get("opening") ?? "",
|
|
2761
|
+
seed: d.gated.texts.get("seed") ?? "",
|
|
2762
|
+
access: d.gated.texts.get("access") ?? "",
|
|
2763
|
+
});
|
|
2764
|
+
candidates.push({ draft: d, breadth, softMisses: softMissCount(d.gated), ms: Date.now() - startedAt });
|
|
2765
|
+
if (candidates.length >= pickCount)
|
|
2766
|
+
pick();
|
|
2767
|
+
else if (candidates.length === 1) {
|
|
2768
|
+
const blind = !o.duo && candidates[0].breadth === 0 && candidates[0].softMisses === 0;
|
|
2769
|
+
pickWaitMs = blind ? 0 : Math.max(0, Math.min(MERCURY_PICK_WAIT_MS, o.budget.remainingMs() - pickReserveMs));
|
|
2770
|
+
if (pickWaitMs === 0)
|
|
2771
|
+
pick();
|
|
2772
|
+
else
|
|
2773
|
+
pickTimer = setTimeout(pick, pickWaitMs);
|
|
2774
|
+
}
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
d.log.lastError = `mercury brief: gate ${failed.join(", ")}`;
|
|
2778
|
+
d.log.cause = "gate";
|
|
2779
|
+
noteSend(d.log, label);
|
|
2780
|
+
}));
|
|
2781
|
+
await Promise.race([firstValid, everyDraft]);
|
|
2782
|
+
pick();
|
|
2783
|
+
const totals = { usage: drafts.flatMap((d) => d.totals.usage) };
|
|
2784
|
+
for (const d of drafts) {
|
|
2785
|
+
if (d.totals.servedModel !== undefined)
|
|
2786
|
+
totals.servedModel = d.totals.servedModel;
|
|
2787
|
+
if (d.totals.reasoningTokens !== undefined)
|
|
2788
|
+
totals.reasoningTokens = (totals.reasoningTokens ?? 0) + d.totals.reasoningTokens;
|
|
2789
|
+
}
|
|
2790
|
+
const log = {
|
|
2791
|
+
...mercurySendLog(),
|
|
2792
|
+
sends: drafts.reduce((n, d) => n + d.log.sends, 0),
|
|
2793
|
+
retries: drafts.reduce((n, d) => n + d.log.retries, 0),
|
|
2794
|
+
history: drafts.flatMap((d) => d.log.history),
|
|
2795
|
+
};
|
|
2796
|
+
const chosen = winner ?? drafts.reduce((best, d) => (gatedCount(d.gated) > gatedCount(best.gated) ? d : best), drafts[0]);
|
|
2797
|
+
log.cause = chosen.log.cause;
|
|
2798
|
+
log.lastError = chosen.log.lastError;
|
|
2799
|
+
const draftUsage = totals.usage.length;
|
|
2800
|
+
let attempts = drafts.some((d) => d.ended === "reply" || d.ended === "failed") ? 1 : 0;
|
|
2801
|
+
let ok = winner !== undefined;
|
|
2802
|
+
let text = winner?.text;
|
|
2803
|
+
let partialText;
|
|
2804
|
+
const throttled = drafts.filter((d) => d.ended === "failed" && (d.log.cause === "rate-limited" || d.log.cause === "upstream-error")).length;
|
|
2805
|
+
let throttleStop = false;
|
|
2806
|
+
if (ok === false && throttled === o.fanout) {
|
|
2807
|
+
const retryAfters = drafts.flatMap((d) => (d.log.retryAfterMs === undefined ? [] : [d.log.retryAfterMs]));
|
|
2808
|
+
const wait = retryAfters.length > 0 ? Math.max(...retryAfters) : (mercuryRetryWaitMs(1, undefined) ?? 0);
|
|
2809
|
+
if (wait > o.budget.remainingMs() - MERCURY_MIN_ATTEMPT_MS) {
|
|
2810
|
+
throttleStop = true;
|
|
2811
|
+
log.lastError = `${chosen.log.lastError} (retry-after ${wait}ms; falling back inside the stage budget)`;
|
|
2812
|
+
}
|
|
2813
|
+
else {
|
|
2814
|
+
await waitMs(wait);
|
|
2815
|
+
log.retries += 1;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
if (ok === false && attempts === 1 && throttleStop === false) {
|
|
2819
|
+
const reask = mercurySendLog();
|
|
2820
|
+
reask.cause = chosen.log.cause;
|
|
2821
|
+
reask.lastError = chosen.log.lastError;
|
|
2822
|
+
reask.carried = true;
|
|
2823
|
+
const a = await mercuryAttempt(vctx, { ...attempt, request: request(chosen.gated, chosen.words, chosen.index) }, reask);
|
|
2824
|
+
log.sends += reask.sends;
|
|
2825
|
+
log.retries += reask.retries;
|
|
2826
|
+
log.history.push(...reask.history);
|
|
2827
|
+
log.cause = reask.cause;
|
|
2828
|
+
log.lastError = reask.lastError;
|
|
2829
|
+
if (a.kind === "reply" || a.kind === "failed")
|
|
2830
|
+
attempts += 1;
|
|
2831
|
+
if (a.kind === "reply") {
|
|
2832
|
+
absorbReply(totals, a.reply);
|
|
2833
|
+
const failed = gate(a.reply.json, chosen.gated);
|
|
2834
|
+
if (failed.length === 0) {
|
|
2835
|
+
ok = true;
|
|
2836
|
+
text = a.reply.text;
|
|
2837
|
+
partialText = chosen.text;
|
|
2838
|
+
}
|
|
2839
|
+
else {
|
|
2840
|
+
log.lastError = `mercury brief: gate ${failed.join(", ")}`;
|
|
2841
|
+
log.cause = "gate";
|
|
2842
|
+
noteSend(log);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
const aborted = drafts.filter((d) => d.aborted).length;
|
|
2847
|
+
const pickCandidates = candidates.map((c) => ({
|
|
2848
|
+
draft: c.draft.index,
|
|
2849
|
+
breadth: c.breadth,
|
|
2850
|
+
softMisses: c.softMisses,
|
|
2851
|
+
ms: c.ms,
|
|
2852
|
+
...(c.draft.gated.shape === undefined ? {} : { archetype: c.draft.gated.shape.archetype }),
|
|
2853
|
+
}));
|
|
2854
|
+
const billedDrafts = billedTotals(totals.usage.slice(0, draftUsage), aborted);
|
|
2855
|
+
const reaskUsage = totals.usage.slice(draftUsage);
|
|
2856
|
+
const fanout = {
|
|
2857
|
+
drafts: drafts.filter((d) => d.log.sends > 0).length,
|
|
2858
|
+
valid: drafts.filter((d) => d.valid).length,
|
|
2859
|
+
...(winner === undefined ? {} : { winner: winner.index }),
|
|
2860
|
+
...(winnerMs === undefined ? {} : { winnerMs }),
|
|
2861
|
+
...(winner === undefined
|
|
2862
|
+
? {}
|
|
2863
|
+
: {
|
|
2864
|
+
picked: winner.index,
|
|
2865
|
+
candidates: pickCandidates,
|
|
2866
|
+
...(pickWaitMs === undefined ? {} : { pickWaitMs }),
|
|
2867
|
+
}),
|
|
2868
|
+
aborted,
|
|
2869
|
+
...(throttled === 0 ? {} : { throttled }),
|
|
2870
|
+
billed: reaskUsage.length === 0
|
|
2871
|
+
? billedDrafts
|
|
2872
|
+
: {
|
|
2873
|
+
...billedDrafts,
|
|
2874
|
+
prompt_tokens: billedDrafts.prompt_tokens + reaskUsage.reduce((n, u) => n + u.prompt_tokens, 0),
|
|
2875
|
+
completion_tokens: billedDrafts.completion_tokens + reaskUsage.reduce((n, u) => n + u.completion_tokens, 0),
|
|
2876
|
+
total_tokens: billedDrafts.total_tokens + reaskUsage.reduce((n, u) => n + u.total_tokens, 0),
|
|
2877
|
+
...(billedDrafts.cost === undefined && reaskUsage.every((u) => u.cost === undefined)
|
|
2878
|
+
? {}
|
|
2879
|
+
: { cost: (billedDrafts.cost ?? 0) + reaskUsage.reduce((n, u) => n + (u.cost ?? 0), 0) }),
|
|
2880
|
+
},
|
|
2881
|
+
};
|
|
2882
|
+
const g = chosen.gated;
|
|
2883
|
+
const pickedBreadth = candidates.find((c) => c.draft === winner)?.breadth;
|
|
2884
|
+
const pair = winner === undefined ? [] : [winner, ...(partner === undefined ? [] : [partner.draft])];
|
|
2885
|
+
const pairArchetypes = pair.flatMap((d) => (d.gated.shape === undefined ? [] : [d.gated.shape.archetype]));
|
|
2886
|
+
const duo = o.duo && winner !== undefined && pickedBreadth !== undefined
|
|
2887
|
+
? {
|
|
2888
|
+
drafts: pair.map((d) => d.index),
|
|
2889
|
+
breadth: [pickedBreadth, ...(partner === undefined ? [] : [partner.breadth])],
|
|
2890
|
+
...(o.shape === undefined
|
|
2891
|
+
? {
|
|
2892
|
+
pool: o.duoPool,
|
|
2893
|
+
candidates: pickCandidates,
|
|
2894
|
+
archetypes: pairArchetypes,
|
|
2895
|
+
archetypeSplit: new Set(pairArchetypes).size > 1,
|
|
2896
|
+
}
|
|
2897
|
+
: {}),
|
|
2898
|
+
...(duoConflict === undefined ? {} : { conflict: duoConflict }),
|
|
2899
|
+
}
|
|
2900
|
+
: undefined;
|
|
2901
|
+
const backendWorded = mercuryBackendWordsNamed(o.personWords);
|
|
2902
|
+
const backendCleared = backendWorded
|
|
2903
|
+
? []
|
|
2904
|
+
: drafts.filter((d) => d !== chosen && d.gated.shape?.flags.backend === true).map((d) => d.index);
|
|
2905
|
+
const oneCall = o.shape === undefined
|
|
2906
|
+
? {
|
|
2907
|
+
draftArchetypes: drafts.map((d) => d.gated.shape?.archetype ?? null),
|
|
2908
|
+
archetypeSplit: new Set(drafts.flatMap((d) => (d.gated.shape === undefined ? [] : [d.gated.shape.archetype]))).size > 1,
|
|
2909
|
+
flagsWinner: g.shape === undefined ? [] : setFlags(g.shape.flags),
|
|
2910
|
+
flagsUnion: MERCURY_FLAGS.filter((f) => drafts.some((d) => d.gated.shape?.flags[f] === true && (f !== "backend" || backendWorded || d === chosen))),
|
|
2911
|
+
...(backendCleared.length === 0 ? {} : { backendFilterCleared: backendCleared }),
|
|
2912
|
+
...(backendWorded || g.shape?.flags.backend !== true ? {} : { backendFilterKept: chosen.index }),
|
|
2913
|
+
}
|
|
2914
|
+
: undefined;
|
|
2915
|
+
const extra = {
|
|
2916
|
+
passedThrough: [...g.misses],
|
|
2917
|
+
addressRepair: [...g.addressRepair],
|
|
2918
|
+
...(g.pairs === undefined ? {} : { slugsWithWord: mercurySlugsWithWord(g.pairs, chosen.words) }),
|
|
2919
|
+
...(g.rooms === undefined ? {} : { roomsDropped: g.rooms.dropped }),
|
|
2920
|
+
...(g.font === undefined ? {} : { fontDropped: g.font.dropped }),
|
|
2921
|
+
...(g.screens === undefined ? {} : { screensDropped: g.screens.dropped }),
|
|
2922
|
+
fanout,
|
|
2923
|
+
juice: o.juice,
|
|
2924
|
+
depth: o.depth,
|
|
2925
|
+
...(o.reply === "split"
|
|
2926
|
+
? {}
|
|
2927
|
+
: {
|
|
2928
|
+
reply: o.reply,
|
|
2929
|
+
preambleFrom: "brief",
|
|
2930
|
+
storyChars: g.story?.story === undefined ? 0 : cleanMercuryText(g.story.story).length,
|
|
2931
|
+
storyDropped: g.story?.dropped ?? 0,
|
|
2932
|
+
...(g.preamble === undefined ? {} : { preambleChars: g.preamble.length }),
|
|
2933
|
+
}),
|
|
2934
|
+
...(duo === undefined ? {} : { duo: true, duoDrafts: duo.drafts, duoBreadth: duo.breadth }),
|
|
2935
|
+
...(duo?.pool === undefined ? {} : { duoPool: duo.pool }),
|
|
2936
|
+
...(duo?.archetypes === undefined ? {} : { duoArchetypes: duo.archetypes }),
|
|
2937
|
+
...(duo?.conflict === undefined ? {} : { duoConflict: true, duoConflictReason: duo.conflict }),
|
|
2938
|
+
...(o.recordsRules
|
|
2939
|
+
? {
|
|
2940
|
+
recordsRules: true,
|
|
2941
|
+
...(g.records === undefined ? {} : { recordsDropped: g.records.dropped }),
|
|
2942
|
+
...(g.rules === undefined ? {} : { rulesDropped: g.rules.dropped }),
|
|
2943
|
+
}
|
|
2944
|
+
: {}),
|
|
2945
|
+
...(oneCall === undefined ? {} : { oneCall: true, shapeCall: false, ...oneCall }),
|
|
2946
|
+
...(g.softMisses.size === 0 ? {} : { softMisses: Object.fromEntries(g.softMisses) }),
|
|
2947
|
+
...(g.coerced.size === 0 ? {} : { coerced: [...g.coerced] }),
|
|
2948
|
+
...(g.fieldsDropped.size === 0 ? {} : { fieldsDropped: [...g.fieldsDropped] }),
|
|
2949
|
+
...(o.reply === "whole" && g.preamble === undefined ? { preambleFallback: 1 } : {}),
|
|
2950
|
+
...(o.planner === "opus" ? { planner: "opus" } : {}),
|
|
2951
|
+
...(text === undefined ? {} : { text }),
|
|
2952
|
+
...(partialText === undefined ? {} : { partialText }),
|
|
2953
|
+
...(duo === undefined ? {} : { duoTexts: pair.flatMap((d) => (d.text === undefined ? [] : [d.text])) }),
|
|
2954
|
+
};
|
|
2955
|
+
const call = { call: "brief", row: o.row, attempts, startedAt, totals, log, extra };
|
|
2956
|
+
if (ok === false)
|
|
2957
|
+
return failCall(vctx, call);
|
|
2958
|
+
o.onCounts(log.sends, log.retries);
|
|
2959
|
+
await recordMercuryRow(vctx, { ...o.row, record: callRecord({ ...call, ok: true }) });
|
|
2960
|
+
return Result.Ok({
|
|
2961
|
+
gated: g,
|
|
2962
|
+
words: chosen.words,
|
|
2963
|
+
...(partner === undefined || winner === undefined ? {} : { partner: partner.draft.gated }),
|
|
2964
|
+
...(duo === undefined ? {} : { duo }),
|
|
2965
|
+
...(oneCall === undefined ? {} : { oneCall }),
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
export const MERCURY_PROSE_MAX_TOKENS = 3200;
|
|
2969
|
+
async function mercuryProseBriefCall(vctx, o) {
|
|
2970
|
+
return mercuryFirstValidCall(vctx, {
|
|
2971
|
+
...o,
|
|
2972
|
+
maxTokens: MERCURY_PROSE_MAX_TOKENS,
|
|
2973
|
+
request: (words) => ({
|
|
2974
|
+
schema: mercuryProseBriefSchema({ rooms: o.askRooms, font: o.askRooms }),
|
|
2975
|
+
messages: mercuryProseBriefMessages({ personWords: o.personWords, words, rooms: o.askRooms, font: o.askRooms }),
|
|
2976
|
+
}),
|
|
2977
|
+
gate: (json) => gateMercuryProseDraft(json, { personWords: o.personWords, askRooms: o.askRooms }),
|
|
2978
|
+
extra: () => ({ format: "prose" }),
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
export const MERCURY_THINKALOUD_MAX_TOKENS = 16_000;
|
|
2982
|
+
export const MERCURY_THINKALOUD_DEADLINE_MS = 20_000;
|
|
2983
|
+
export const MERCURY_THINKALOUD_BUDGET_MS = 25_000;
|
|
2984
|
+
async function mercuryThinkAloudBriefCall(vctx, o) {
|
|
2985
|
+
return mercuryFirstValidCall(vctx, {
|
|
2986
|
+
...o,
|
|
2987
|
+
persistent: true,
|
|
2988
|
+
maxTokens: MERCURY_THINKALOUD_MAX_TOKENS,
|
|
2989
|
+
request: (words) => ({
|
|
2990
|
+
messages: mercuryThinkAloudMessages({ personWords: o.personWords, words, rooms: o.askRooms, font: o.askRooms }),
|
|
2991
|
+
}),
|
|
2992
|
+
gate: (raw) => gateMercuryThinkAloud(raw, { personWords: o.personWords, askRooms: o.askRooms }),
|
|
2993
|
+
extra: (d) => ({
|
|
2994
|
+
format: "thinkaloud",
|
|
2995
|
+
...(d === undefined
|
|
2996
|
+
? {}
|
|
2997
|
+
: {
|
|
2998
|
+
...(Object.keys(d.softMisses).length === 0 ? {} : { softMisses: d.softMisses }),
|
|
2999
|
+
thinkAloud: { narrationChars: d.narration.length, specChars: d.spec.length, guessLines: d.guessLines },
|
|
3000
|
+
}),
|
|
3001
|
+
}),
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
async function mercuryCardsBriefCall(vctx, o) {
|
|
3005
|
+
return mercuryFirstValidCall(vctx, {
|
|
3006
|
+
...o,
|
|
3007
|
+
persistent: true,
|
|
3008
|
+
maxTokens: MERCURY_THINKALOUD_MAX_TOKENS,
|
|
3009
|
+
request: (words) => ({
|
|
3010
|
+
messages: mercuryCardsMessages({ personWords: o.personWords, words, rooms: o.askRooms, font: o.askRooms }),
|
|
3011
|
+
}),
|
|
3012
|
+
gate: (raw) => gateMercuryCards(raw, { personWords: o.personWords, askRooms: o.askRooms }),
|
|
3013
|
+
extra: (d) => ({
|
|
3014
|
+
format: o.format,
|
|
3015
|
+
...(o.planner === "mercury" ? {} : { planner: o.planner }),
|
|
3016
|
+
...(d === undefined
|
|
3017
|
+
? {}
|
|
3018
|
+
: {
|
|
3019
|
+
...(Object.keys(d.softMisses).length === 0 ? {} : { softMisses: d.softMisses }),
|
|
3020
|
+
cards: {
|
|
3021
|
+
preambleChars: d.parsed.preamble.length,
|
|
3022
|
+
cards: d.parsed.cardLines.length,
|
|
3023
|
+
acceptLines: d.parsed.acceptLines,
|
|
3024
|
+
ruleLines: d.parsed.ruleLines,
|
|
3025
|
+
guessLines: d.parsed.guessLines,
|
|
3026
|
+
dataRecords: d.parsed.dataRecords.length,
|
|
3027
|
+
},
|
|
3028
|
+
}),
|
|
3029
|
+
}),
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
3032
|
+
async function mercuryFirstValidCall(vctx, o) {
|
|
3033
|
+
const startedAt = Date.now();
|
|
3034
|
+
const cancel = new AbortController();
|
|
3035
|
+
const drafts = Array.from({ length: o.persistent === true ? MERCURY_TESTER_FANOUT : MERCURY_BRIEF_FANOUT }, (_, index) => ({
|
|
3036
|
+
index,
|
|
3037
|
+
words: index === 0 || o.words.length === 0 ? o.words : o.wordBag(o.words.length),
|
|
3038
|
+
log: mercurySendLog(),
|
|
3039
|
+
totals: { usage: [] },
|
|
3040
|
+
aborted: false,
|
|
3041
|
+
}));
|
|
3042
|
+
const request = (words) => () => o.request(words);
|
|
3043
|
+
const gate = o.gate;
|
|
3044
|
+
const attempt = {
|
|
3045
|
+
call: "brief",
|
|
3046
|
+
deadlineMs: o.deadlineMs,
|
|
3047
|
+
budget: o.budget,
|
|
3048
|
+
maxTokens: o.maxTokens,
|
|
3049
|
+
...(o.planner === undefined || o.planner === "mercury" ? {} : { model: plannerModel(o.planner) }),
|
|
3050
|
+
...(o.persistent === true ? { persistent: true } : {}),
|
|
3051
|
+
};
|
|
3052
|
+
let winner;
|
|
3053
|
+
let valid = 0;
|
|
3054
|
+
let won = () => undefined;
|
|
3055
|
+
const firstValid = new Promise((resolve) => {
|
|
3056
|
+
won = resolve;
|
|
3057
|
+
});
|
|
3058
|
+
const everyDraft = Promise.allSettled(drafts.map(async function runDraft(d) {
|
|
3059
|
+
const label = `draft ${d.index}: `;
|
|
3060
|
+
const a = await mercuryAttempt(vctx, {
|
|
3061
|
+
...attempt,
|
|
3062
|
+
request: request(d.words),
|
|
3063
|
+
cancel: cancel.signal,
|
|
3064
|
+
label,
|
|
3065
|
+
...(o.persistent === true ? {} : { transientRetries: 0 }),
|
|
3066
|
+
}, d.log);
|
|
3067
|
+
if (winner !== undefined)
|
|
3068
|
+
return;
|
|
3069
|
+
d.ended = a.kind;
|
|
3070
|
+
if (a.kind === "cancelled" && a.inFlight)
|
|
3071
|
+
d.aborted = true;
|
|
3072
|
+
if (a.kind !== "reply")
|
|
3073
|
+
return;
|
|
3074
|
+
absorbReply(d.totals, a.reply);
|
|
3075
|
+
const g = gate(a.reply.json);
|
|
3076
|
+
if (g.isOk()) {
|
|
3077
|
+
valid += 1;
|
|
3078
|
+
winner = { from: d, draft: g.Ok(), ms: Date.now() - startedAt, text: a.reply.text };
|
|
3079
|
+
for (const s of drafts)
|
|
3080
|
+
if (s.ended === undefined && s.log.inFlight === true)
|
|
3081
|
+
s.aborted = true;
|
|
3082
|
+
cancel.abort();
|
|
3083
|
+
won();
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
d.log.lastError = `mercury brief: gate ${g.Err().message}`;
|
|
3087
|
+
d.log.cause = "gate";
|
|
3088
|
+
noteSend(d.log, label);
|
|
3089
|
+
}));
|
|
3090
|
+
await Promise.race([firstValid, everyDraft]);
|
|
3091
|
+
const totals = { usage: drafts.flatMap((d) => d.totals.usage) };
|
|
3092
|
+
for (const d of drafts) {
|
|
3093
|
+
if (d.totals.servedModel !== undefined)
|
|
3094
|
+
totals.servedModel = d.totals.servedModel;
|
|
3095
|
+
if (d.totals.reasoningTokens !== undefined)
|
|
3096
|
+
totals.reasoningTokens = (totals.reasoningTokens ?? 0) + d.totals.reasoningTokens;
|
|
3097
|
+
}
|
|
3098
|
+
const last = drafts.find((d) => d.ended === "reply") ?? drafts[0];
|
|
3099
|
+
const log = {
|
|
3100
|
+
...mercurySendLog(),
|
|
3101
|
+
sends: drafts.reduce((n, d) => n + d.log.sends, 0),
|
|
3102
|
+
retries: drafts.reduce((n, d) => n + d.log.retries, 0),
|
|
3103
|
+
history: drafts.flatMap((d) => d.log.history),
|
|
3104
|
+
cause: last.log.cause,
|
|
3105
|
+
lastError: last.log.lastError,
|
|
3106
|
+
};
|
|
3107
|
+
const draftUsage = totals.usage.length;
|
|
3108
|
+
let attempts = drafts.some((d) => d.ended === "reply" || d.ended === "failed") ? 1 : 0;
|
|
3109
|
+
let chosen = winner;
|
|
3110
|
+
if (chosen === undefined && attempts === 1) {
|
|
3111
|
+
const reask = mercurySendLog();
|
|
3112
|
+
reask.cause = log.cause;
|
|
3113
|
+
reask.lastError = log.lastError;
|
|
3114
|
+
reask.carried = true;
|
|
3115
|
+
const a = await mercuryAttempt(vctx, { ...attempt, request: request(drafts[0].words) }, reask);
|
|
3116
|
+
log.sends += reask.sends;
|
|
3117
|
+
log.retries += reask.retries;
|
|
3118
|
+
log.history.push(...reask.history);
|
|
3119
|
+
log.cause = reask.cause;
|
|
3120
|
+
log.lastError = reask.lastError;
|
|
3121
|
+
if (a.kind === "reply" || a.kind === "failed")
|
|
3122
|
+
attempts += 1;
|
|
3123
|
+
if (a.kind === "reply") {
|
|
3124
|
+
absorbReply(totals, a.reply);
|
|
3125
|
+
const g = gate(a.reply.json);
|
|
3126
|
+
if (g.isOk()) {
|
|
3127
|
+
valid += 1;
|
|
3128
|
+
chosen = { from: drafts[0], draft: g.Ok(), ms: Date.now() - startedAt, text: a.reply.text };
|
|
3129
|
+
}
|
|
3130
|
+
else {
|
|
3131
|
+
log.lastError = `mercury brief: gate ${g.Err().message}`;
|
|
3132
|
+
log.cause = "gate";
|
|
3133
|
+
noteSend(log);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
const aborted = drafts.filter((d) => d.aborted).length;
|
|
3138
|
+
const billedDrafts = billedTotals(totals.usage.slice(0, draftUsage), aborted);
|
|
3139
|
+
const reaskUsage = totals.usage.slice(draftUsage);
|
|
3140
|
+
const fanout = {
|
|
3141
|
+
drafts: drafts.filter((d) => d.log.sends > 0).length,
|
|
3142
|
+
valid,
|
|
3143
|
+
...(winner === undefined ? {} : { winner: winner.from.index, winnerMs: winner.ms, picked: winner.from.index }),
|
|
3144
|
+
aborted,
|
|
3145
|
+
billed: reaskUsage.length === 0
|
|
3146
|
+
? billedDrafts
|
|
3147
|
+
: {
|
|
3148
|
+
...billedDrafts,
|
|
3149
|
+
prompt_tokens: billedDrafts.prompt_tokens + reaskUsage.reduce((n, u) => n + u.prompt_tokens, 0),
|
|
3150
|
+
completion_tokens: billedDrafts.completion_tokens + reaskUsage.reduce((n, u) => n + u.completion_tokens, 0),
|
|
3151
|
+
total_tokens: billedDrafts.total_tokens + reaskUsage.reduce((n, u) => n + u.total_tokens, 0),
|
|
3152
|
+
...(billedDrafts.cost === undefined && reaskUsage.every((u) => u.cost === undefined)
|
|
3153
|
+
? {}
|
|
3154
|
+
: { cost: (billedDrafts.cost ?? 0) + reaskUsage.reduce((n, u) => n + (u.cost ?? 0), 0) }),
|
|
3155
|
+
},
|
|
3156
|
+
};
|
|
3157
|
+
const c = chosen?.draft;
|
|
3158
|
+
const extra = {
|
|
3159
|
+
...o.extra(c),
|
|
3160
|
+
fanout,
|
|
3161
|
+
reply: "whole",
|
|
3162
|
+
preambleFrom: "classification",
|
|
3163
|
+
oneCall: true,
|
|
3164
|
+
shapeCall: false,
|
|
3165
|
+
...(c === undefined
|
|
3166
|
+
? {}
|
|
3167
|
+
: {
|
|
3168
|
+
addressRepair: [...c.addressRepair],
|
|
3169
|
+
slugsWithWord: mercurySlugsWithWord(c.address.pairs, chosen?.from.words ?? []),
|
|
3170
|
+
...(c.rooms === undefined ? {} : { roomsDropped: c.rooms.dropped }),
|
|
3171
|
+
...(c.font === undefined ? {} : { fontDropped: c.font.dropped }),
|
|
3172
|
+
flagsWinner: setFlags(c.shape.flags),
|
|
3173
|
+
}),
|
|
3174
|
+
...(chosen === undefined ? {} : { text: chosen.text }),
|
|
3175
|
+
};
|
|
3176
|
+
const call = { call: "brief", row: o.row, attempts, startedAt, totals, log, extra };
|
|
3177
|
+
if (chosen === undefined)
|
|
3178
|
+
return failCall(vctx, call);
|
|
3179
|
+
o.onCounts(log.sends, log.retries);
|
|
3180
|
+
await recordMercuryRow(vctx, { ...o.row, record: callRecord({ ...call, ok: true }) });
|
|
3181
|
+
return Result.Ok({ draft: chosen.draft, words: chosen.from.words });
|
|
3182
|
+
}
|
|
3183
|
+
export async function runMercuryFirstBuild(vctx, args) {
|
|
3184
|
+
const startedAt = Date.now();
|
|
3185
|
+
const breaker = args.breaker ??
|
|
3186
|
+
(args.planner === "opus"
|
|
3187
|
+
? MERCURY_OPUS_PLANNER_BREAKER
|
|
3188
|
+
: args.planner === "haiku"
|
|
3189
|
+
? MERCURY_HAIKU_PLANNER_BREAKER
|
|
3190
|
+
: MERCURY_BREAKER);
|
|
3191
|
+
const stage = { sends: 0, retries: 0 };
|
|
718
3192
|
if (args.personWords.trim().length === 0)
|
|
719
|
-
return
|
|
3193
|
+
return stageErr("mercury: no words to brief from", "error", stage, startedAt);
|
|
720
3194
|
if (vctx.billing.enforceEnabled) {
|
|
721
3195
|
const rCheck = await vctx.billing.checkCredits({ userId: args.userId });
|
|
722
3196
|
if (rCheck.isOk() && rCheck.Ok().allowed === false)
|
|
723
|
-
return
|
|
3197
|
+
return stageErr("mercury: no credits remaining", "no-credits", stage, startedAt);
|
|
3198
|
+
}
|
|
3199
|
+
const admission = breaker.admit();
|
|
3200
|
+
if (admission === "open") {
|
|
3201
|
+
vctx.logger.Warn().Any("event", { chatId: args.chatId }).Msg("mercury-first-build-circuit-open");
|
|
3202
|
+
return stageErr("mercury: circuit open after repeated provider failures; skipping to the brief round", "circuit-open", stage, startedAt);
|
|
724
3203
|
}
|
|
3204
|
+
const r = await exception2Result(() => pipeline(vctx, args, stage));
|
|
3205
|
+
const failed = r.isErr() && r.Err() instanceof MercuryStageError === false
|
|
3206
|
+
? stageErr(`mercury: ${r.Err().message}`, "error", stage, startedAt)
|
|
3207
|
+
: r;
|
|
3208
|
+
const out = failed.isErr() && args.signal?.aborted === true
|
|
3209
|
+
? Result.Err(new MercuryStageError("mercury: cancelled, the first build went to the agent", "cancelled", failed.Err().detail))
|
|
3210
|
+
: failed;
|
|
3211
|
+
if (out.isOk())
|
|
3212
|
+
breaker.record(admission, { ok: true });
|
|
3213
|
+
else {
|
|
3214
|
+
const e = out.Err();
|
|
3215
|
+
breaker.record(admission, { ok: false, cause: e instanceof MercuryStageError ? e.fallbackCause : "error" });
|
|
3216
|
+
}
|
|
3217
|
+
return out;
|
|
3218
|
+
}
|
|
3219
|
+
function stageErr(message, cause, stage, startedAt, call) {
|
|
3220
|
+
return Result.Err(new MercuryStageError(message, cause, {
|
|
3221
|
+
...(call === undefined ? {} : { call }),
|
|
3222
|
+
sends: stage.sends,
|
|
3223
|
+
retries: stage.retries,
|
|
3224
|
+
elapsedMs: Date.now() - startedAt,
|
|
3225
|
+
}));
|
|
3226
|
+
}
|
|
3227
|
+
async function pipeline(vctx, args, stage) {
|
|
725
3228
|
const startedAt = Date.now();
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
3229
|
+
let lastRow;
|
|
3230
|
+
const keepText = args.keepText === true;
|
|
3231
|
+
const row = {
|
|
3232
|
+
chatId: args.chatId,
|
|
3233
|
+
userId: args.userId,
|
|
3234
|
+
...(args.onUsage === undefined ? {} : { onUsage: args.onUsage }),
|
|
3235
|
+
...(keepText ? { keepText: true } : {}),
|
|
3236
|
+
onWritten: (promptId) => {
|
|
3237
|
+
lastRow = promptId;
|
|
3238
|
+
args.onRowWritten?.(promptId);
|
|
3239
|
+
},
|
|
3240
|
+
};
|
|
3241
|
+
const named = () => (keepText && lastRow !== undefined ? { rowPromptId: lastRow } : {});
|
|
3242
|
+
const opus = args.planner === "opus";
|
|
3243
|
+
const haiku = args.planner === "haiku";
|
|
3244
|
+
const thinkAloud = args.format === "thinkaloud";
|
|
3245
|
+
const cards = args.format === "cards" || args.format === "cards-fim";
|
|
3246
|
+
const freeText = thinkAloud || cards;
|
|
3247
|
+
const deadlineMs = args.deadlineMs ??
|
|
3248
|
+
(opus ? MERCURY_OPUS_PLANNER_DEADLINE_MS : haiku || freeText ? MERCURY_TESTER_REQUEST_DEADLINE_MS : MERCURY_CALL_DEADLINE_MS);
|
|
3249
|
+
const budget = mercuryBudget(args.budgetMs ??
|
|
3250
|
+
(opus ? MERCURY_OPUS_PLANNER_BUDGET_MS : haiku || freeText ? MERCURY_TESTER_STAGE_BUDGET_MS : MERCURY_STAGE_BUDGET_MS), Date.now, args.rateLimitedBudgetMs, args.signal);
|
|
3251
|
+
const wordBag = args.wordBag ?? slugWordBag;
|
|
3252
|
+
const style = args.style ?? "base";
|
|
3253
|
+
const askRooms = args.rooms === true;
|
|
3254
|
+
const reply = args.reply ?? "split";
|
|
3255
|
+
const recordsRules = reply === "whole" && args.recordsRules === true;
|
|
3256
|
+
function tally(r) {
|
|
3257
|
+
if (r.isOk())
|
|
3258
|
+
return r;
|
|
3259
|
+
const e = r.Err();
|
|
3260
|
+
stage.sends += e.detail.sends;
|
|
3261
|
+
stage.retries += e.detail.retries;
|
|
3262
|
+
const rateLimitCapMs = budget.rateLimitCapMs();
|
|
3263
|
+
return Result.Err(new MercuryStageError(e.message, e.fallbackCause, {
|
|
3264
|
+
...(e.detail.call === undefined ? {} : { call: e.detail.call }),
|
|
3265
|
+
sends: stage.sends,
|
|
3266
|
+
retries: stage.retries,
|
|
3267
|
+
elapsedMs: Date.now() - startedAt,
|
|
3268
|
+
...(rateLimitCapMs === undefined ? {} : { rateLimitCapMs }),
|
|
3269
|
+
}));
|
|
3270
|
+
}
|
|
3271
|
+
const counted = (sends, retries) => {
|
|
3272
|
+
stage.sends += sends;
|
|
3273
|
+
stage.retries += retries;
|
|
3274
|
+
};
|
|
3275
|
+
if (args.format === "prose") {
|
|
3276
|
+
const rProse = tally(await mercuryProseBriefCall(vctx, {
|
|
3277
|
+
row,
|
|
3278
|
+
deadlineMs,
|
|
3279
|
+
budget,
|
|
3280
|
+
personWords: args.personWords,
|
|
3281
|
+
askRooms,
|
|
3282
|
+
words: args.words,
|
|
3283
|
+
wordBag,
|
|
3284
|
+
onCounts: counted,
|
|
3285
|
+
}));
|
|
3286
|
+
if (rProse.isErr())
|
|
3287
|
+
return Result.Err(rProse.Err());
|
|
3288
|
+
const { draft } = rProse.Ok();
|
|
3289
|
+
return Result.Ok({
|
|
3290
|
+
input: assembleMercuryProseCreateVibe({ personWords: args.personWords, draft }),
|
|
3291
|
+
preamble: mercuryProsePreamble(draft),
|
|
3292
|
+
preambleFrom: "classification",
|
|
3293
|
+
format: "prose",
|
|
3294
|
+
shape: draft.shape,
|
|
3295
|
+
elapsedMs: Date.now() - startedAt,
|
|
3296
|
+
sends: stage.sends,
|
|
3297
|
+
retries: stage.retries,
|
|
3298
|
+
...named(),
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
if (thinkAloud) {
|
|
3302
|
+
const rThink = tally(await mercuryThinkAloudBriefCall(vctx, {
|
|
3303
|
+
row,
|
|
3304
|
+
deadlineMs,
|
|
3305
|
+
budget,
|
|
3306
|
+
personWords: args.personWords,
|
|
3307
|
+
askRooms,
|
|
3308
|
+
words: args.words,
|
|
3309
|
+
wordBag,
|
|
3310
|
+
onCounts: counted,
|
|
3311
|
+
}));
|
|
3312
|
+
if (rThink.isErr())
|
|
3313
|
+
return Result.Err(rThink.Err());
|
|
3314
|
+
const { draft } = rThink.Ok();
|
|
3315
|
+
return Result.Ok({
|
|
3316
|
+
input: assembleMercuryThinkAloudCreateVibe({ personWords: args.personWords, draft }),
|
|
3317
|
+
preamble: mercuryThinkAloudPreamble(draft),
|
|
3318
|
+
preambleFrom: "classification",
|
|
3319
|
+
format: "thinkaloud",
|
|
3320
|
+
shape: draft.shape,
|
|
3321
|
+
elapsedMs: Date.now() - startedAt,
|
|
3322
|
+
sends: stage.sends,
|
|
3323
|
+
retries: stage.retries,
|
|
3324
|
+
...named(),
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
if (cards && (args.format === "cards" || args.format === "cards-fim")) {
|
|
3328
|
+
const format = args.format;
|
|
3329
|
+
const rCards = tally(await mercuryCardsBriefCall(vctx, {
|
|
3330
|
+
row,
|
|
3331
|
+
deadlineMs,
|
|
3332
|
+
budget,
|
|
3333
|
+
personWords: args.personWords,
|
|
3334
|
+
askRooms,
|
|
3335
|
+
words: args.words,
|
|
3336
|
+
wordBag,
|
|
3337
|
+
onCounts: counted,
|
|
3338
|
+
format,
|
|
3339
|
+
planner: args.planner ?? "mercury",
|
|
3340
|
+
}));
|
|
3341
|
+
if (rCards.isErr())
|
|
3342
|
+
return Result.Err(rCards.Err());
|
|
3343
|
+
const { draft } = rCards.Ok();
|
|
3344
|
+
let reply = draft.reply;
|
|
3345
|
+
let fim;
|
|
3346
|
+
if (format === "cards-fim") {
|
|
3347
|
+
const filled = await mercuryCardsFim(vctx, {
|
|
3348
|
+
draft,
|
|
3349
|
+
personWords: args.personWords,
|
|
3350
|
+
...(args.fimApiKey !== undefined
|
|
3351
|
+
? { apiKey: args.fimApiKey }
|
|
3352
|
+
: vctx.inceptionApiKey === undefined
|
|
3353
|
+
? {}
|
|
3354
|
+
: { apiKey: vctx.inceptionApiKey }),
|
|
3355
|
+
...(args.fimUrl === undefined ? {} : { url: args.fimUrl }),
|
|
3356
|
+
...(args.signal === undefined ? {} : { cancel: args.signal }),
|
|
3357
|
+
});
|
|
3358
|
+
reply = filled.reply;
|
|
3359
|
+
fim = filled.receipt;
|
|
3360
|
+
const fimRound = mercuryFimUsageRound(fim);
|
|
3361
|
+
if (fimRound !== undefined)
|
|
3362
|
+
args.onUsage?.([fimRound]);
|
|
3363
|
+
if (lastRow !== undefined)
|
|
3364
|
+
await recordMercuryFim(vctx, {
|
|
3365
|
+
chatId: args.chatId,
|
|
3366
|
+
promptId: lastRow,
|
|
3367
|
+
fim,
|
|
3368
|
+
...(keepText && fim.filled > 0 ? { filledText: reply } : {}),
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
return Result.Ok({
|
|
3372
|
+
input: assembleMercuryCardsCreateVibe({ personWords: args.personWords, draft, reply }),
|
|
3373
|
+
preamble: mercuryCardsPreamble(draft, args.personWords),
|
|
3374
|
+
preambleFrom: draft.parsed.preamble.length > 0 ? "brief" : "classification",
|
|
3375
|
+
format,
|
|
3376
|
+
...(args.planner === undefined || args.planner === "mercury" ? {} : { planner: args.planner }),
|
|
3377
|
+
...(fim === undefined ? {} : { fim }),
|
|
3378
|
+
shape: draft.shape,
|
|
3379
|
+
elapsedMs: Date.now() - startedAt,
|
|
3380
|
+
sends: stage.sends,
|
|
3381
|
+
retries: stage.retries,
|
|
3382
|
+
...named(),
|
|
3383
|
+
});
|
|
3384
|
+
}
|
|
3385
|
+
const oneCall = args.oneCall === true;
|
|
3386
|
+
const callShape = async () => tally(await mercuryCall(vctx, {
|
|
731
3387
|
call: "shape",
|
|
732
3388
|
row,
|
|
733
3389
|
deadlineMs,
|
|
734
|
-
|
|
3390
|
+
budget,
|
|
3391
|
+
maxTokens: MERCURY_SHAPE_MAX_TOKENS,
|
|
3392
|
+
onCounts: counted,
|
|
735
3393
|
request: () => ({
|
|
736
3394
|
schema: mercuryShapeSchema(),
|
|
737
|
-
messages: mercuryShapeMessages({ personWords: args.personWords
|
|
3395
|
+
messages: mercuryShapeMessages({ personWords: args.personWords }),
|
|
738
3396
|
}),
|
|
739
3397
|
accept: (json) => {
|
|
740
3398
|
const parsed = parseMercuryShape(json);
|
|
741
3399
|
return parsed.isErr() ? parsed : Result.Ok(fixMercuryShape(parsed.Ok(), args.personWords));
|
|
742
3400
|
},
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
3401
|
+
}));
|
|
3402
|
+
let called;
|
|
3403
|
+
if (!oneCall) {
|
|
3404
|
+
const rShape = await callShape();
|
|
3405
|
+
if (rShape.isErr())
|
|
3406
|
+
return Result.Err(rShape.Err());
|
|
3407
|
+
called = rShape.Ok();
|
|
3408
|
+
}
|
|
3409
|
+
const rBrief = tally(await mercuryBriefCall(vctx, {
|
|
750
3410
|
row,
|
|
751
3411
|
deadlineMs,
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
return failed.length === 0 ? Result.Ok(true) : Result.Err(`gate ${failed.join(", ")}`);
|
|
770
|
-
},
|
|
771
|
-
});
|
|
3412
|
+
budget,
|
|
3413
|
+
personWords: args.personWords,
|
|
3414
|
+
...(called === undefined ? {} : { shape: called }),
|
|
3415
|
+
style,
|
|
3416
|
+
juice: args.juice ?? "examples",
|
|
3417
|
+
depth: args.depth ?? "none",
|
|
3418
|
+
reply,
|
|
3419
|
+
duo: reply === "whole" && args.duo === true,
|
|
3420
|
+
duoPool: args.duoPool ?? MERCURY_DUO_PICK_COUNT,
|
|
3421
|
+
recordsRules,
|
|
3422
|
+
askRooms,
|
|
3423
|
+
words: args.words,
|
|
3424
|
+
wordBag,
|
|
3425
|
+
onCounts: counted,
|
|
3426
|
+
fanout: opus ? MERCURY_OPUS_PLANNER_FANOUT : MERCURY_BRIEF_FANOUT,
|
|
3427
|
+
planner: opus ? "opus" : "mercury",
|
|
3428
|
+
}));
|
|
772
3429
|
if (rBrief.isErr())
|
|
773
|
-
return Result.Err(rBrief);
|
|
774
|
-
const
|
|
3430
|
+
return Result.Err(rBrief.Err());
|
|
3431
|
+
const { gated, partner, duo, oneCall: oneCallReceipt } = rBrief.Ok();
|
|
3432
|
+
const shape = called ?? gated.shape;
|
|
3433
|
+
if (shape === undefined)
|
|
3434
|
+
return stageErr("mercury: no classification parsed", "gate", stage, startedAt, "brief");
|
|
3435
|
+
const union = oneCallReceipt === undefined
|
|
3436
|
+
? undefined
|
|
3437
|
+
: Object.fromEntries(MERCURY_FLAGS.map((f) => [f, oneCallReceipt.flagsUnion.includes(f)]));
|
|
3438
|
+
const assembly = {
|
|
775
3439
|
personWords: args.personWords,
|
|
776
3440
|
shape,
|
|
3441
|
+
...(union === undefined ? {} : { capabilityFlags: union }),
|
|
777
3442
|
fields: {
|
|
778
3443
|
opening: gated.texts.get("opening") ?? "",
|
|
779
3444
|
access: gated.texts.get("access") ?? "",
|
|
@@ -782,16 +3447,53 @@ async function pipeline(vctx, args) {
|
|
|
782
3447
|
icon: gated.texts.get("icon") ?? "",
|
|
783
3448
|
pairs: gated.pairs ?? [],
|
|
784
3449
|
},
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
3450
|
+
...(gated.rooms?.rooms === undefined ? {} : { rooms: gated.rooms.rooms }),
|
|
3451
|
+
...(gated.font?.font === undefined ? {} : { font: gated.font.font }),
|
|
3452
|
+
...(gated.screens?.screens === undefined ? {} : { screens: gated.screens.screens }),
|
|
3453
|
+
...(reply === "split"
|
|
3454
|
+
? {}
|
|
3455
|
+
: {
|
|
3456
|
+
reply,
|
|
3457
|
+
...(gated.story?.story === undefined ? {} : { story: gated.story.story }),
|
|
3458
|
+
...(gated.preamble === undefined ? {} : { preamble: gated.preamble }),
|
|
3459
|
+
...(partner === undefined ? {} : { partner: takeOf(partner) }),
|
|
3460
|
+
...(recordsRules
|
|
3461
|
+
? {
|
|
3462
|
+
recordsRules: true,
|
|
3463
|
+
...(gated.records?.screens === undefined ? {} : { records: gated.records.screens }),
|
|
3464
|
+
...(gated.rules?.screens === undefined ? {} : { rules: gated.rules.screens }),
|
|
3465
|
+
}
|
|
3466
|
+
: {}),
|
|
3467
|
+
}),
|
|
3468
|
+
};
|
|
3469
|
+
const input = assembleMercuryCreateVibe(assembly);
|
|
3470
|
+
if (reply === "whole") {
|
|
3471
|
+
return Result.Ok({
|
|
3472
|
+
input,
|
|
3473
|
+
preamble: gated.preamble ?? cleanMercuryText(gated.texts.get("opening") ?? ""),
|
|
3474
|
+
preambleFrom: "brief",
|
|
3475
|
+
storyChars: gated.story?.story === undefined ? 0 : cleanMercuryText(gated.story.story).length,
|
|
3476
|
+
...(duo === undefined ? {} : { duo }),
|
|
3477
|
+
...(oneCallReceipt === undefined ? {} : { oneCall: oneCallReceipt }),
|
|
3478
|
+
...(opus ? { planner: "opus" } : {}),
|
|
3479
|
+
shape,
|
|
3480
|
+
elapsedMs: Date.now() - startedAt,
|
|
3481
|
+
sends: stage.sends,
|
|
3482
|
+
retries: stage.retries,
|
|
3483
|
+
...named(),
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
const enrichedPrompt = mercuryPreambleBrief(assembly);
|
|
3487
|
+
const rPreamble = tally(await mercuryCall(vctx, {
|
|
788
3488
|
call: "preamble",
|
|
789
3489
|
row,
|
|
790
3490
|
deadlineMs,
|
|
3491
|
+
budget,
|
|
791
3492
|
maxTokens: 300,
|
|
3493
|
+
onCounts: counted,
|
|
792
3494
|
request: () => ({
|
|
793
3495
|
schema: mercuryPreambleSchema(),
|
|
794
|
-
messages: mercuryPreambleMessages({ personWords: args.personWords, shape, enrichedPrompt,
|
|
3496
|
+
messages: mercuryPreambleMessages({ personWords: args.personWords, shape, enrichedPrompt, style }),
|
|
795
3497
|
}),
|
|
796
3498
|
accept: (json) => {
|
|
797
3499
|
const p = preambleParsed(json);
|
|
@@ -800,9 +3502,18 @@ async function pipeline(vctx, args) {
|
|
|
800
3502
|
const fails = gateMercuryPreamble({ text: p.preamble, enrichedPrompt });
|
|
801
3503
|
return fails.length === 0 ? Result.Ok(cleanMercuryText(p.preamble)) : Result.Err(`gate ${fails.join("+")}`);
|
|
802
3504
|
},
|
|
803
|
-
});
|
|
3505
|
+
}));
|
|
804
3506
|
if (rPreamble.isErr())
|
|
805
|
-
return Result.Err(rPreamble);
|
|
806
|
-
return Result.Ok({
|
|
3507
|
+
return Result.Err(rPreamble.Err());
|
|
3508
|
+
return Result.Ok({
|
|
3509
|
+
input,
|
|
3510
|
+
preamble: rPreamble.Ok(),
|
|
3511
|
+
...(oneCallReceipt === undefined ? {} : { oneCall: oneCallReceipt }),
|
|
3512
|
+
shape,
|
|
3513
|
+
elapsedMs: Date.now() - startedAt,
|
|
3514
|
+
sends: stage.sends,
|
|
3515
|
+
retries: stage.retries,
|
|
3516
|
+
...named(),
|
|
3517
|
+
});
|
|
807
3518
|
}
|
|
808
3519
|
//# sourceMappingURL=mercury-first-build.js.map
|