@vibes.diy/api-svc 14.3.84 → 14.3.85
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/intern/account-vibe-activity.js +2 -1
- package/intern/account-vibe-activity.js.map +1 -1
- package/intern/agent-account-tools.js +2 -1
- package/intern/agent-account-tools.js.map +1 -1
- package/intern/agent-assembly.js +8 -2
- package/intern/agent-assembly.js.map +1 -1
- package/intern/agent-belt.js +2 -6
- package/intern/agent-belt.js.map +1 -1
- package/intern/agent-build-tools.js +0 -17
- package/intern/agent-build-tools.js.map +1 -1
- package/intern/agent-discussions.js +2 -1
- package/intern/agent-discussions.js.map +1 -1
- package/intern/agent-evidence.js +5 -4
- package/intern/agent-evidence.js.map +1 -1
- package/intern/agent-file-tools.js +5 -4
- package/intern/agent-file-tools.js.map +1 -1
- package/intern/agent-options-tool.js +46 -46
- package/intern/agent-options-tool.js.map +1 -1
- package/intern/agent-read-tools.js +43 -25
- package/intern/agent-read-tools.js.map +1 -1
- package/intern/agent-session-instruction.d.ts +2 -2
- package/intern/agent-session-instruction.js +3 -3
- package/intern/agent-session-instruction.js.map +1 -1
- package/intern/agent-session-runtime.js +2 -1
- package/intern/agent-session-runtime.js.map +1 -1
- package/intern/agent-session.js +8 -7
- package/intern/agent-session.js.map +1 -1
- package/intern/agent-wake.d.ts +1 -1
- package/intern/agent-wake.js +1 -1
- package/intern/agent-wake.js.map +1 -1
- package/intern/agent-with-vibe.d.ts +1 -0
- package/intern/agent-with-vibe.js +19 -18
- package/intern/agent-with-vibe.js.map +1 -1
- package/intern/autumn-recent-usage.js +11 -10
- package/intern/autumn-recent-usage.js.map +1 -1
- package/intern/backend-db-callback.js +3 -2
- package/intern/backend-db-callback.js.map +1 -1
- package/intern/draft-chat-store.js +3 -2
- package/intern/draft-chat-store.js.map +1 -1
- package/intern/first-brief.js +3 -2
- package/intern/first-brief.js.map +1 -1
- package/intern/first-build-classifier.js +3 -2
- package/intern/first-build-classifier.js.map +1 -1
- package/intern/guest-credit-meter.js +3 -2
- package/intern/guest-credit-meter.js.map +1 -1
- package/intern/image-survey.js +6 -5
- package/intern/image-survey.js.map +1 -1
- package/intern/instruction-variant.d.ts +3 -1
- package/intern/instruction-variant.js +31 -1
- package/intern/instruction-variant.js.map +1 -1
- package/intern/manifest-assets-line.d.ts +2 -0
- package/intern/manifest-assets-line.js +14 -1
- package/intern/manifest-assets-line.js.map +1 -1
- package/intern/mercury-first-build-seam.d.ts +42 -0
- package/intern/mercury-first-build-seam.js +129 -0
- package/intern/mercury-first-build-seam.js.map +1 -0
- package/intern/mercury-first-build.d.ts +121 -0
- package/intern/mercury-first-build.js +808 -0
- package/intern/mercury-first-build.js.map +1 -0
- package/intern/pinned-app-slug.js +2 -1
- package/intern/pinned-app-slug.js.map +1 -1
- package/intern/prompt-step.d.ts +1 -1
- package/intern/prompt-step.js.map +1 -1
- package/intern/prompt-usage.js +5 -4
- package/intern/prompt-usage.js.map +1 -1
- package/intern/save-repair-io.js +3 -2
- package/intern/save-repair-io.js.map +1 -1
- package/intern/url-research.js +3 -2
- package/intern/url-research.js.map +1 -1
- package/intern/work-tools.js +8 -5
- package/intern/work-tools.js.map +1 -1
- package/not-implemented-message.js +2 -1
- package/not-implemented-message.js.map +1 -1
- package/package.json +13 -13
- package/public/access-output-decoder.js +4 -3
- package/public/access-output-decoder.js.map +1 -1
- package/public/agent-turn-action.js +75 -16
- package/public/agent-turn-action.js.map +1 -1
- package/public/draft-runtime.js +9 -5
- package/public/draft-runtime.js.map +1 -1
- package/public/list-memberships.js +3 -2
- package/public/list-memberships.js.map +1 -1
- package/public/list-notifications.js +2 -1
- package/public/list-notifications.js.map +1 -1
- package/public/list-recent-vibes.js +4 -3
- package/public/list-recent-vibes.js.map +1 -1
- package/public/prompt-chat-section.js +8 -1
- package/public/prompt-chat-section.js.map +1 -1
- package/public/provider-errors.js +3 -2
- package/public/provider-errors.js.map +1 -1
- package/public/report-cache.js +2 -1
- package/public/report-cache.js.map +1 -1
- package/public/serv-entry-point.js +2 -1
- package/public/serv-entry-point.js.map +1 -1
- package/types.js +7 -6
- package/types.js.map +1 -1
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
import { Result, exception2Result } from "@adviser/cement";
|
|
2
|
+
import { type } from "arktype";
|
|
3
|
+
import { lazyType } from "@vibes.diy/api-types/lazy-type";
|
|
4
|
+
import { KNOWN_VALUES_DIRECT_CONTROLS } from "./agent-session-instruction.js";
|
|
5
|
+
import { addressWordsSentence } from "./instruction-variant.js";
|
|
6
|
+
import { capRetryDelayMs, parseRetryAfterMs, readFailureBody, waitMs } from "../public/provider-errors.js";
|
|
7
|
+
export const MERCURY_FIRST_BUILD_MODEL = "inception/mercury-2.5";
|
|
8
|
+
export const MERCURY_FIRST_BUILD_REASONING = { effort: "none" };
|
|
9
|
+
export const MERCURY_CALL_DEADLINE_MS = 8_000;
|
|
10
|
+
export const MERCURY_CALL_ATTEMPTS = 2;
|
|
11
|
+
export const MERCURY_TRANSIENT_RETRIES = 3;
|
|
12
|
+
export const MERCURY_TRANSIENT_BACKOFF_MS = 400;
|
|
13
|
+
const MERCURY_TRANSIENT_MAX_WAIT_MS = 2_000;
|
|
14
|
+
const MERCURY_TRANSIENT_STATUSES = new Set([429, 502, 503, 504]);
|
|
15
|
+
class MercuryTransientError extends Error {
|
|
16
|
+
retryAfterMs;
|
|
17
|
+
constructor(message, retryAfterMs) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.retryAfterMs = retryAfterMs;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const MERCURY_ARCHETYPES = ["personal", "group", "publish", "intake", "open", "toy"];
|
|
23
|
+
export const MERCURY_FLAGS = [
|
|
24
|
+
"ai",
|
|
25
|
+
"composer",
|
|
26
|
+
"imageGen",
|
|
27
|
+
"backend",
|
|
28
|
+
"calendar",
|
|
29
|
+
"audio",
|
|
30
|
+
"threeD",
|
|
31
|
+
"charts",
|
|
32
|
+
"boardGame",
|
|
33
|
+
"sensitive",
|
|
34
|
+
];
|
|
35
|
+
export const MERCURY_BRIEF_FIELDS = ["opening", "access", "seed", "look", "pairs", "icon"];
|
|
36
|
+
export const MERCURY_SHAPE_SYSTEM = `You classify a person's one-message request for a small web app before a build brief is written. Return only JSON.
|
|
37
|
+
|
|
38
|
+
archetype, exactly one:
|
|
39
|
+
- personal: one person's own tool ("for me", "my", "private", a tracker, a log, a list, a journal) with no other people named.
|
|
40
|
+
- group: the person names other people who use it with them (we, our, friends, family, team, club, kids, housemates, partner, members, a foursome).
|
|
41
|
+
- publish: the person writes and other people only read (a blog, menu, gallery, portfolio, schedule, zine, "everyone can read").
|
|
42
|
+
- intake: strangers or customers submit something without signing up and the person sees the submissions (orders, bookings, requests, sign-ups, replies, reports from the public).
|
|
43
|
+
- open: anyone with the link joins in and adds their own bit (a shared wall, canvas, feed, leaderboard game, community board, swap board, "anyone can post").
|
|
44
|
+
- toy: a game, simulator, visual, sound or generator that keeps little or nothing (scores at most).
|
|
45
|
+
Tie-breaks: group means other people open the app on their own devices; a calculator, splitter or tool used on one phone "with friends" at the table is personal. toy is for games, simulators and visual or sound toys; a tool that analyzes, estimates, designs, counts, tracks or logs stores records and is personal. A "feed of information" pulled from the internet is personal with backend true. A test or nonsense phrase is still a request: make the most playful literal app of it (a "squash bloom soothsayer" is a fortune teller about squash) and classify that.
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
kind: 2-6 word noun phrase for the app in the person's own nouns. who: who it is for, 2-8 words.
|
|
49
|
+
recordNames: 1-5 short singular record type names the app stores (toys may have just "score" or none).
|
|
50
|
+
flags, each true only when the request itself needs it:
|
|
51
|
+
- ai: the app must read, write or judge free text or photos with a language model (extract facts from pasted words, answer questions, generate text, chat, analyze a picture, suggest recipes).
|
|
52
|
+
- composer: ai is true AND the person types or pastes their own words or photos as the way records get in.
|
|
53
|
+
- imageGen: the app generates pictures.
|
|
54
|
+
- backend: server work the page cannot do: scheduled reminders or digests, a live feed pulled from outside, secret dealing, email.
|
|
55
|
+
- calendar: dates on a calendar, bookings by slot, recurring events, paystub periods.
|
|
56
|
+
- audio: sound or music playback or synthesis.
|
|
57
|
+
- threeD: 3D graphics, a simulator, a world you move through.
|
|
58
|
+
- charts: heatmaps, graphs, statistics views.
|
|
59
|
+
- boardGame: a turn-based card or board game.
|
|
60
|
+
- sensitive: health, money of real people, kids, or the word private.`;
|
|
61
|
+
export function mercuryShapeSchema() {
|
|
62
|
+
return {
|
|
63
|
+
type: "object",
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
required: ["archetype", "coreAction", "kind", "who", "recordNames", ...MERCURY_FLAGS],
|
|
66
|
+
properties: {
|
|
67
|
+
archetype: { type: "string", enum: [...MERCURY_ARCHETYPES] },
|
|
68
|
+
coreAction: { type: "string" },
|
|
69
|
+
kind: { type: "string" },
|
|
70
|
+
who: { type: "string" },
|
|
71
|
+
recordNames: { type: "array", items: { type: "string" } },
|
|
72
|
+
...Object.fromEntries(MERCURY_FLAGS.map((f) => [f, { type: "boolean" }])),
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function mercuryNonce() {
|
|
77
|
+
return String(1000 + Math.floor(Math.random() * 9000));
|
|
78
|
+
}
|
|
79
|
+
export function mercuryShapeMessages(o) {
|
|
80
|
+
return [
|
|
81
|
+
{ 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
|
+
},
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
const shapeParsed = lazyType(() => type({
|
|
91
|
+
archetype: type.enumerated(...MERCURY_ARCHETYPES),
|
|
92
|
+
coreAction: "string",
|
|
93
|
+
kind: "string",
|
|
94
|
+
who: "string",
|
|
95
|
+
recordNames: "string[]",
|
|
96
|
+
ai: "boolean",
|
|
97
|
+
composer: "boolean",
|
|
98
|
+
imageGen: "boolean",
|
|
99
|
+
backend: "boolean",
|
|
100
|
+
calendar: "boolean",
|
|
101
|
+
audio: "boolean",
|
|
102
|
+
threeD: "boolean",
|
|
103
|
+
charts: "boolean",
|
|
104
|
+
boardGame: "boolean",
|
|
105
|
+
sensitive: "boolean",
|
|
106
|
+
}));
|
|
107
|
+
export function parseMercuryShape(raw) {
|
|
108
|
+
const p = shapeParsed(raw);
|
|
109
|
+
if (p instanceof type.errors)
|
|
110
|
+
return Result.Err(`mercury shape: ${p.summary}`);
|
|
111
|
+
if (p.coreAction.trim().length === 0)
|
|
112
|
+
return Result.Err("mercury shape: empty coreAction");
|
|
113
|
+
return Result.Ok({
|
|
114
|
+
archetype: p.archetype,
|
|
115
|
+
coreAction: p.coreAction.trim(),
|
|
116
|
+
kind: p.kind.trim(),
|
|
117
|
+
who: p.who.trim(),
|
|
118
|
+
recordNames: p.recordNames,
|
|
119
|
+
flags: {
|
|
120
|
+
ai: p.ai,
|
|
121
|
+
composer: p.composer,
|
|
122
|
+
imageGen: p.imageGen,
|
|
123
|
+
backend: p.backend,
|
|
124
|
+
calendar: p.calendar,
|
|
125
|
+
audio: p.audio,
|
|
126
|
+
threeD: p.threeD,
|
|
127
|
+
charts: p.charts,
|
|
128
|
+
boardGame: p.boardGame,
|
|
129
|
+
sensitive: p.sensitive,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const COMPOSER_WORDS = /paste|photo|picture|own words|however they|repl(y|ies)|in their words|type in|jot/;
|
|
134
|
+
const BACKEND_WORDS = /remind|notif|email|digest|schedul|every (day|week|morning|monday)|nag|feed|sync|server|secret|deal|webhook|cron|poll/;
|
|
135
|
+
const PRIVATE_WORDS = /\b(private|just for me|only me|for myself)\b/;
|
|
136
|
+
const MULTIPLAYER_WORDS = /\b(friends? can join|multiplayer|same world)\b/;
|
|
137
|
+
export function fixMercuryShape(shape, personWords) {
|
|
138
|
+
const m = personWords.toLowerCase();
|
|
139
|
+
const composer = shape.flags.ai && shape.flags.composer && COMPOSER_WORDS.test(m);
|
|
140
|
+
const backend = shape.flags.backend && BACKEND_WORDS.test(m);
|
|
141
|
+
const archetype = PRIVATE_WORDS.test(m) && (shape.archetype === "open" || shape.archetype === "publish")
|
|
142
|
+
? "personal"
|
|
143
|
+
: shape.archetype === "toy" && MULTIPLAYER_WORDS.test(m)
|
|
144
|
+
? "open"
|
|
145
|
+
: shape.archetype;
|
|
146
|
+
return {
|
|
147
|
+
...shape,
|
|
148
|
+
archetype,
|
|
149
|
+
recordNames: shape.recordNames
|
|
150
|
+
.map((r) => r.trim())
|
|
151
|
+
.filter((r) => r.length > 0)
|
|
152
|
+
.slice(0, 5),
|
|
153
|
+
flags: { ...shape.flags, composer, backend },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export const MERCURY_EXEMPLAR_FIELD = `## How to read this field
|
|
157
|
+
|
|
158
|
+
Every brief has the same parts, in this order. Below, each part is shown as a SPREAD of alternatives, side by side. Each row is tagged with the ownership archetype it fits:
|
|
159
|
+
|
|
160
|
+
- [personal] one person's own tool: private, records keyed to their own object so a friend could be let in later.
|
|
161
|
+
- [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.
|
|
162
|
+
- [publish] the owner writes, everyone reads: a blog, menu, gallery, zine, schedule.
|
|
163
|
+
- [intake] strangers submit without signing in (orders, bookings, requests, RSVPs); the owner sees and manages what came in.
|
|
164
|
+
- [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
|
+
- [toy] a game or visual that keeps little or nothing: scores or a save slot at most.
|
|
166
|
+
|
|
167
|
+
Pick the rows that match the archetype you were given. Take every noun from the person's own words.
|
|
168
|
+
|
|
169
|
+
## 1. Opening — one sentence: what it is and who it is for
|
|
170
|
+
|
|
171
|
+
- [personal] A glaze test notebook for a home potter.
|
|
172
|
+
- [personal] A sourdough starter log for one baker's kitchen.
|
|
173
|
+
- [group] A tool library for one neighbourhood block, shared by the households on it.
|
|
174
|
+
- [group] A setlist planner for a four-piece cover band.
|
|
175
|
+
- [publish] A taco truck menu board the owner updates from their phone and customers read.
|
|
176
|
+
- [publish] A micro-poetry zine where one editor posts and everyone reads.
|
|
177
|
+
- [intake] An appointment book for a dog grooming salon: clients book without signing up, the groomer sees every slot.
|
|
178
|
+
- [intake] A repair drop-off form for a bike shop, with the owner's queue on the other side.
|
|
179
|
+
- [open] A lost-and-found board for a music festival that anyone with the link can post to.
|
|
180
|
+
- [open] A wedding photo wall where every guest drops pictures from their phone.
|
|
181
|
+
- [toy] A hex-grid pong game with a slow-motion ball and a local best score.
|
|
182
|
+
- [toy] A kaleidoscope you steer with the mouse, with nothing saved.
|
|
183
|
+
|
|
184
|
+
## 2. Ownership and access — who owns what, who gets in, who sees what
|
|
185
|
+
|
|
186
|
+
- [personal] 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.
|
|
187
|
+
- [personal] The person's own starter log is ready on arrival. Feedings belong to that log; rules key on its creator, so the entries stay theirs, shaped so a housemate could be admitted later.
|
|
188
|
+
- [group] 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.
|
|
189
|
+
- [group] The owner's band object holds memberships for the other three players, written by the HandleInput invite screen. Any member adds songs and reorders the set; the creator manages members and deletes songs.
|
|
190
|
+
- [publish] 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.
|
|
191
|
+
- [publish] One editor owns the zine; poem records are creator-writable and world-readable. Readers see the issues.
|
|
192
|
+
- [intake] 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.
|
|
193
|
+
- [intake] Drop-off requests are created by visitors without accounts; each request is readable by the shop owner (and staff let in later by membership). The public form page shows the visitor their own request.
|
|
194
|
+
- [open] 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.
|
|
195
|
+
- [open] Anyone with the link uploads photos to the wall; everyone reads; a photo is deletable by whoever posted it or the couple who own the wall.
|
|
196
|
+
- [toy] Each signed-in player keeps a best score record keyed on their handle; a shared Top Scores list shows everyone's best with ViewerTag.
|
|
197
|
+
- [toy] The toy keeps its state in memory: it opens instantly, with no sign-in and no records.
|
|
198
|
+
|
|
199
|
+
## 3. Seed cast — seed.json demo with a named cast, counts and dates near today
|
|
200
|
+
|
|
201
|
+
- [personal] seed.json demo: the owner's notebook plus 5 glazes (Tenmoku, Shino, Celadon, Chun blue, Ash) and 9 tiles fired between 2026-09-20 and 2026-10-05, two rated 5.
|
|
202
|
+
- [personal] seed.json demo: one starter "Gus" born 2026-03-02 with 10 feedings over the last two weeks, rise heights between 3 and 7 cm.
|
|
203
|
+
- [group] seed.json demo: the owner's library "Maple Street Tools" with the owner's own membership plus @priya and @tomas, 8 tools (a ladder, a tile saw, a pressure washer...), two loans out due this week and one returned.
|
|
204
|
+
- [group] seed.json demo: the band "Low Tide" with the owner's membership and three members (@dee, @marco, @sun), 14 songs with keys and bpm, one setlist for a gig dated 2026-10-18.
|
|
205
|
+
- [publish] seed.json demo: the owner's menu with 6 items across tacos, sides and drinks, one marked sold out, and a note of the day dated 2026-10-07.
|
|
206
|
+
- [intake] seed.json demo: the owner's salon with 7 bookings spread over 2026-10-07 to 2026-10-11 in mixed statuses, dog names like Biscuit and Juno.
|
|
207
|
+
- [open] seed.json demo: the festival board with 9 posts from five handles, five lost and four found, two already resolved, dated across the last three days.
|
|
208
|
+
- [toy] seed.json demo: 6 score records from a small cast including the owner so the Top Scores list opens filled.
|
|
209
|
+
|
|
210
|
+
## 4. Look note — one short line: background, accent, card shape, mood
|
|
211
|
+
|
|
212
|
+
- [personal] Earthy studio: oatmeal background, iron-red accents, chunky rounded tile cards.
|
|
213
|
+
- [personal] Flour-dusted kitchen: warm white, rye brown, a tall rising bar as the hero number.
|
|
214
|
+
- [group] Workshop pegboard: plywood tones, safety-orange accents, square tool cards with big status chips.
|
|
215
|
+
- [group] Backstage: near-black, amber stage light accents, setlist rows like gaffer-tape strips.
|
|
216
|
+
- [publish] Street-food bright: chalkboard dark with chalk-white type and chili-red price tags.
|
|
217
|
+
- [intake] Clean salon: pale mint, charcoal type, generous slot buttons easy to tap one-handed.
|
|
218
|
+
- [open] Festival poster: cream paper, bold two-colour print, pinned-note cards with torn edges.
|
|
219
|
+
- [toy] Neon arcade: deep navy, electric cyan ball, glowing hex outlines.
|
|
220
|
+
|
|
221
|
+
## 5. Titles, addresses and icon
|
|
222
|
+
|
|
223
|
+
- pairs: Glaze Ledger / glaze-ledger, Tile Book / tile-book, Kiln Notes / kiln-notes — icon: a glazed test tile beside a small kiln
|
|
224
|
+
- pairs: Block Tools / block-tools, Maple Street Shelf / maple-street-shelf, Borrow Board / borrow-board — icon: a hand drill inside a house outline
|
|
225
|
+
- pairs: Truck Board / truck-board, Taco Today / taco-today, Menu Chalk / menu-chalk — icon: a chalkboard with a taco drawn on it
|
|
226
|
+
- pairs: Groom Book / groom-book, Salon Slots / salon-slots, Paws Diary / paws-diary — icon: a dog brush beside a clock face
|
|
227
|
+
- pairs: Festival Finds / festival-finds, Lost Tent / lost-tent, Found It / found-it — icon: a tent peg tied with a ribbon
|
|
228
|
+
- pairs: Hex Pong / hex-pong, Slow Rally / slow-rally, Honey Paddle / honey-paddle — icon: a hexagonal paddle and a glowing ball`;
|
|
229
|
+
export const MERCURY_BRIEF_RULES = `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.
|
|
230
|
+
|
|
231
|
+
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. The seed part starts with "seed.json demo:" and gives a named cast, counts and dates near today, using the given record names. 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. Each pair is a Title Case title of two or three words and a kebab-case address made from it.`;
|
|
232
|
+
const FIELD_BOUNDS = {
|
|
233
|
+
opening: [15, 200],
|
|
234
|
+
access: [60, 600],
|
|
235
|
+
seed: [60, 650],
|
|
236
|
+
look: [15, 150],
|
|
237
|
+
icon: [8, 100],
|
|
238
|
+
};
|
|
239
|
+
export function mercuryBriefSchema(fields) {
|
|
240
|
+
return {
|
|
241
|
+
type: "object",
|
|
242
|
+
additionalProperties: false,
|
|
243
|
+
required: [...fields],
|
|
244
|
+
properties: Object.fromEntries(fields.map((f) => [
|
|
245
|
+
f,
|
|
246
|
+
f === "pairs"
|
|
247
|
+
? {
|
|
248
|
+
type: "array",
|
|
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
|
+
])),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function capabilityDecisions(shape) {
|
|
261
|
+
return [
|
|
262
|
+
`AI text features: ${shape.flags.ai ? "yes (callAI)" : "none; the app works from its own records"}`,
|
|
263
|
+
`generated pictures: ${shape.flags.imageGen ? "yes (ImgGen)" : "none"}`,
|
|
264
|
+
...(shape.flags.composer
|
|
265
|
+
? [
|
|
266
|
+
"the records come in through a sentence composer (Add and Ask); name it as the front door and leave its mechanics to the builder",
|
|
267
|
+
]
|
|
268
|
+
: []),
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
export function mercuryBriefMessages(o) {
|
|
272
|
+
const s = o.shape;
|
|
273
|
+
const decisions = [
|
|
274
|
+
`archetype: ${s.archetype}`,
|
|
275
|
+
`core action: ${s.coreAction}`,
|
|
276
|
+
`kind: ${s.kind}`,
|
|
277
|
+
`for: ${s.who}`,
|
|
278
|
+
`records: ${s.recordNames.length === 0 ? "(you choose, at most three)" : s.recordNames.join(", ")}`,
|
|
279
|
+
...capabilityDecisions(s),
|
|
280
|
+
...(s.archetype === "toy"
|
|
281
|
+
? [
|
|
282
|
+
`saved: ${s.recordNames.length > 0 ? "best scores or a save slot per signed-in player, and nothing else" : "the toy keeps its state in memory; the access part says it opens with no sign-in and no records"}`,
|
|
283
|
+
]
|
|
284
|
+
: []),
|
|
285
|
+
];
|
|
286
|
+
const user = [
|
|
287
|
+
`Person's words: "${o.personWords}"`,
|
|
288
|
+
`Today is ${o.today}.`,
|
|
289
|
+
"",
|
|
290
|
+
"Decisions:",
|
|
291
|
+
...decisions.map((d) => `- ${d}`),
|
|
292
|
+
"",
|
|
293
|
+
`Follow the rows tagged [${s.archetype}] in each part of the field. Return JSON with these keys: ${o.fields.join(", ")}` +
|
|
294
|
+
(o.fields.includes("pairs") ? " (pairs: an array of three {title, slug} candidates)" : "") +
|
|
295
|
+
".",
|
|
296
|
+
...(o.fields.includes("pairs") && o.words.length > 0 ? [addressWordsSentence(o.words).trim()] : []),
|
|
297
|
+
`(variant #${o.nonce}: a seed for wording, never written)`,
|
|
298
|
+
].join("\n");
|
|
299
|
+
return [
|
|
300
|
+
{ role: "system", content: [{ type: "text", text: `${MERCURY_BRIEF_RULES}\n\n${MERCURY_EXEMPLAR_FIELD}` }] },
|
|
301
|
+
{ role: "user", content: [{ type: "text", text: user }] },
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
const FIELD_LABEL = /^\s*(?:\*\*)?(opening|access|seed|look|icon|preamble)(?:\*\*)?\s*:\s*/i;
|
|
305
|
+
const FENCED = /^```[a-zA-Z]*\n([\s\S]*?)\n?```$/;
|
|
306
|
+
export function cleanMercuryText(text) {
|
|
307
|
+
const t = text.trim();
|
|
308
|
+
const fenced = FENCED.exec(t);
|
|
309
|
+
return (fenced === null ? t : fenced[1].trim()).replace(FIELD_LABEL, "").trim();
|
|
310
|
+
}
|
|
311
|
+
function stem(name) {
|
|
312
|
+
return name
|
|
313
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
314
|
+
.toLowerCase()
|
|
315
|
+
.split(/[\s_-]+/)
|
|
316
|
+
.filter((w) => w.length > 2)
|
|
317
|
+
.map((w) => w.replace(/s$/, ""));
|
|
318
|
+
}
|
|
319
|
+
function mentions(name, text) {
|
|
320
|
+
const lower = text.toLowerCase();
|
|
321
|
+
return stem(name).every((w) => lower.includes(w));
|
|
322
|
+
}
|
|
323
|
+
const TAG_LEAK = /\[(personal|group|publish|intake|open|toy)\]|archetype/i;
|
|
324
|
+
const NONCE_LEAK = /variant|#\d{4}/i;
|
|
325
|
+
const MARKUP = /```|^\s*#/m;
|
|
326
|
+
const AI_LEAK = /\bcallai\b|\bAI\b|language model/;
|
|
327
|
+
const pairShape = lazyType(() => type({ title: "string", slug: "string" }).array());
|
|
328
|
+
function kebab(s) {
|
|
329
|
+
return s
|
|
330
|
+
.toLowerCase()
|
|
331
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
332
|
+
.replace(/^-+|-+$/g, "");
|
|
333
|
+
}
|
|
334
|
+
export function gateMercuryPairs(raw) {
|
|
335
|
+
const parsed = pairShape(raw);
|
|
336
|
+
if (parsed instanceof type.errors)
|
|
337
|
+
return Result.Err("pairs-shape");
|
|
338
|
+
const pairs = parsed
|
|
339
|
+
.map((p) => {
|
|
340
|
+
const title = p.title.trim().replace(/^["*]+|["*]+$/g, "");
|
|
341
|
+
const slug = kebab(p.slug.trim().length > 0 ? p.slug : title);
|
|
342
|
+
return { title, slug };
|
|
343
|
+
})
|
|
344
|
+
.filter((p) => p.title.length > 0 && p.slug.length > 0);
|
|
345
|
+
const words = pairs.map((p) => p.title.split(/\s+/).length);
|
|
346
|
+
if (pairs.length !== 3)
|
|
347
|
+
return Result.Err("pairs-count");
|
|
348
|
+
if (new Set(pairs.map((p) => p.title.toLowerCase())).size !== 3)
|
|
349
|
+
return Result.Err("pairs-duplicate");
|
|
350
|
+
if (new Set(pairs.map((p) => p.slug)).size !== 3)
|
|
351
|
+
return Result.Err("pairs-duplicate");
|
|
352
|
+
if (words.some((n) => n < 1 || n > 4))
|
|
353
|
+
return Result.Err("pairs-length");
|
|
354
|
+
return Result.Ok(pairs);
|
|
355
|
+
}
|
|
356
|
+
export function gateMercuryField(o) {
|
|
357
|
+
const t = cleanMercuryText(o.value);
|
|
358
|
+
const a = o.shape.archetype;
|
|
359
|
+
if (o.field === "seed" && a === "toy" && o.shape.recordNames.length === 0)
|
|
360
|
+
return [];
|
|
361
|
+
const [lo, hi] = FIELD_BOUNDS[o.field];
|
|
362
|
+
const fails = [];
|
|
363
|
+
if (t.length === 0)
|
|
364
|
+
fails.push("empty");
|
|
365
|
+
else if (t.length < lo)
|
|
366
|
+
fails.push("short");
|
|
367
|
+
else if (t.length > hi)
|
|
368
|
+
fails.push("long");
|
|
369
|
+
if (NONCE_LEAK.test(t))
|
|
370
|
+
fails.push("nonce");
|
|
371
|
+
if (MARKUP.test(t))
|
|
372
|
+
fails.push("markup");
|
|
373
|
+
if (TAG_LEAK.test(t))
|
|
374
|
+
fails.push("tag-leak");
|
|
375
|
+
if (o.field !== "look" && o.field !== "icon" && o.shape.flags.ai === false && AI_LEAK.test(t))
|
|
376
|
+
fails.push("ai-leak");
|
|
377
|
+
if (o.field === "seed" && t.toLowerCase().includes("seed") === false)
|
|
378
|
+
fails.push("seed-word");
|
|
379
|
+
if (o.field === "seed" && o.shape.recordNames.length > 0 && o.shape.recordNames.some((r) => mentions(r, t)) === false)
|
|
380
|
+
fails.push("seed-no-record");
|
|
381
|
+
if (o.field === "access") {
|
|
382
|
+
if (a === "group" && /invite|member/i.test(t) === false)
|
|
383
|
+
fails.push("access-no-invite");
|
|
384
|
+
if (a === "personal" && /invite screen (is|ships)|members (read|can)/i.test(t))
|
|
385
|
+
fails.push("access-personal-invites");
|
|
386
|
+
if (a === "toy" && o.shape.recordNames.length > 0 && /nothing is saved|no records/i.test(t))
|
|
387
|
+
fails.push("toy-contradiction");
|
|
388
|
+
if ((a === "publish" || a === "intake") && /read|see|visitor|customer|client|anyone|everyone|public/i.test(t) === false)
|
|
389
|
+
fails.push("access-no-readers");
|
|
390
|
+
}
|
|
391
|
+
return fails;
|
|
392
|
+
}
|
|
393
|
+
export const MERCURY_ACCESS_TAIL = {
|
|
394
|
+
personal: "Rules key on the object's creator; records carry the object id so someone could be let in later. The invite screen waits for a later build.",
|
|
395
|
+
group: "Memberships are written by the platform's HandleInput invite screen, which is part of this first build; shared objects show who made them with ViewerTag.",
|
|
396
|
+
publish: "The owner writes; everything they publish is readable by anyone with the link, signed in or not. Shaped so a co-owner could be let in later.",
|
|
397
|
+
intake: "Every visitor writes their own submissions into the shared list, signed in or not, and edits only their own; the signed-in owner reads and manages all of them. Shaped so staff could be let in later.",
|
|
398
|
+
open: "Everyone with the link writes the shared records — posts, votes, entries — and edits only their own; the owner can remove anything.",
|
|
399
|
+
toy: "",
|
|
400
|
+
};
|
|
401
|
+
const TAIL_ALREADY_SAID = {
|
|
402
|
+
personal: /invite screen waits|later build|no invite screen/i,
|
|
403
|
+
group: /HandleInput/,
|
|
404
|
+
publish: /readable by anyone|anyone with the link/i,
|
|
405
|
+
};
|
|
406
|
+
export function mercuryAccessLine(shape, access) {
|
|
407
|
+
const said = TAIL_ALREADY_SAID[shape.archetype];
|
|
408
|
+
const tail = said !== undefined && said.test(access) ? "" : MERCURY_ACCESS_TAIL[shape.archetype];
|
|
409
|
+
return `${access} ${tail}`.trim();
|
|
410
|
+
}
|
|
411
|
+
const CAPABILITY_NAMES = [
|
|
412
|
+
["ai", "AI text (callAI)"],
|
|
413
|
+
["composer", "a sentence composer (Add and Ask) as the front door"],
|
|
414
|
+
["imageGen", "generated pictures (ImgGen)"],
|
|
415
|
+
["backend", "server work (backend.js)"],
|
|
416
|
+
["calendar", "calendar dates"],
|
|
417
|
+
["audio", "sound"],
|
|
418
|
+
["threeD", "3D graphics"],
|
|
419
|
+
["charts", "charts"],
|
|
420
|
+
["boardGame", "a turn-based board or card game"],
|
|
421
|
+
["sensitive", "private records"],
|
|
422
|
+
];
|
|
423
|
+
const FLAG_SKILLS = [
|
|
424
|
+
["ai", "callai"],
|
|
425
|
+
["imageGen", "image-gen"],
|
|
426
|
+
["backend", "backend"],
|
|
427
|
+
["calendar", "calendar"],
|
|
428
|
+
["audio", "web-audio"],
|
|
429
|
+
["threeD", "three-js"],
|
|
430
|
+
["charts", "d3"],
|
|
431
|
+
["boardGame", "board-games"],
|
|
432
|
+
];
|
|
433
|
+
export function mercurySkills(shape) {
|
|
434
|
+
return ["access", "fireproof", ...FLAG_SKILLS.filter(([f]) => shape.flags[f]).map(([, skill]) => skill)];
|
|
435
|
+
}
|
|
436
|
+
export function mercuryNature(shape) {
|
|
437
|
+
const a = shape.archetype;
|
|
438
|
+
if (a === "open" || a === "toy" || a === "publish")
|
|
439
|
+
return "open";
|
|
440
|
+
return shape.flags.sensitive ? "sensitive" : "collaborative";
|
|
441
|
+
}
|
|
442
|
+
export function mercuryWorkflowLine(shape) {
|
|
443
|
+
const caps = CAPABILITY_NAMES.filter(([f]) => shape.flags[f]).map(([, name]) => name);
|
|
444
|
+
return [
|
|
445
|
+
`Archetype: ${shape.archetype}.`,
|
|
446
|
+
`Core action: "${shape.coreAction}".`,
|
|
447
|
+
...(shape.recordNames.length === 0 ? [] : [`Records: ${shape.recordNames.join(", ")}.`]),
|
|
448
|
+
...(caps.length === 0 ? [] : [`Capabilities: ${caps.join(", ")}.`]),
|
|
449
|
+
].join(" ");
|
|
450
|
+
}
|
|
451
|
+
export function assembleMercuryCreateVibe(o) {
|
|
452
|
+
const s = o.shape;
|
|
453
|
+
const keepsNothing = s.archetype === "toy" && s.recordNames.length === 0;
|
|
454
|
+
const access = cleanMercuryText(o.fields.access);
|
|
455
|
+
const seed = keepsNothing ? "" : cleanMercuryText(o.fields.seed);
|
|
456
|
+
const parts = [
|
|
457
|
+
cleanMercuryText(o.fields.opening),
|
|
458
|
+
keepsNothing && access.length === 0 ? "" : mercuryAccessLine(s, access),
|
|
459
|
+
mercuryWorkflowLine(s),
|
|
460
|
+
seed.length === 0 ? "" : `${seed} ${KNOWN_VALUES_DIRECT_CONTROLS}`,
|
|
461
|
+
`The person asked: "${o.personWords}".`,
|
|
462
|
+
];
|
|
463
|
+
const look = cleanMercuryText(o.fields.look);
|
|
464
|
+
const icon = cleanMercuryText(o.fields.icon);
|
|
465
|
+
const nature = mercuryNature(s);
|
|
466
|
+
return {
|
|
467
|
+
prompt: o.personWords,
|
|
468
|
+
enrichedPrompt: parts.filter((p) => p.length > 0).join("\n\n"),
|
|
469
|
+
skills: mercurySkills(s),
|
|
470
|
+
pairs: o.fields.pairs.map((p) => ({ title: p.title, slug: p.slug })),
|
|
471
|
+
...(icon.length === 0 ? {} : { iconDescription: icon }),
|
|
472
|
+
...(look.length === 0 ? {} : { textureNote: look }),
|
|
473
|
+
nature,
|
|
474
|
+
...(nature === "sensitive" ? { privacySensitive: true } : {}),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
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.`;
|
|
478
|
+
export function mercuryPreambleMessages(o) {
|
|
479
|
+
const user = [
|
|
480
|
+
`Person's words: "${o.personWords}"`,
|
|
481
|
+
`Core action: ${o.shape.coreAction}`,
|
|
482
|
+
"",
|
|
483
|
+
"What is being built:",
|
|
484
|
+
o.enrichedPrompt,
|
|
485
|
+
"",
|
|
486
|
+
`Write the preamble. (variant #${o.nonce}: a seed for wording, never written)`,
|
|
487
|
+
].join("\n");
|
|
488
|
+
return [
|
|
489
|
+
{ role: "system", content: [{ type: "text", text: MERCURY_PREAMBLE_SYSTEM }] },
|
|
490
|
+
{ role: "user", content: [{ type: "text", text: user }] },
|
|
491
|
+
];
|
|
492
|
+
}
|
|
493
|
+
const preambleParsed = lazyType(() => type({ preamble: "string" }));
|
|
494
|
+
export function mercuryPreambleSchema() {
|
|
495
|
+
return {
|
|
496
|
+
type: "object",
|
|
497
|
+
additionalProperties: false,
|
|
498
|
+
required: ["preamble"],
|
|
499
|
+
properties: { preamble: { type: "string" } },
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
export function gateMercuryPreamble(o) {
|
|
503
|
+
const t = cleanMercuryText(o.text);
|
|
504
|
+
const fails = [];
|
|
505
|
+
if (t.length === 0)
|
|
506
|
+
fails.push("empty");
|
|
507
|
+
else if (t.length < 60)
|
|
508
|
+
fails.push("short");
|
|
509
|
+
else if (t.length > 480)
|
|
510
|
+
fails.push("long");
|
|
511
|
+
if (/variant|#\d{4}|```|^\s*#|\bbrief\b|seed\.json/im.test(t))
|
|
512
|
+
fails.push("markup");
|
|
513
|
+
if ((t.match(/[.!?](\s|$)/g) ?? []).length > 3)
|
|
514
|
+
fails.push("too-many-sentences");
|
|
515
|
+
const briefWords = new Set(o.enrichedPrompt.toLowerCase().match(/[a-z]{5,}/g) ?? []);
|
|
516
|
+
const shared = new Set((t.toLowerCase().match(/[a-z]{5,}/g) ?? []).filter((w) => briefWords.has(w)));
|
|
517
|
+
if (shared.size < 3)
|
|
518
|
+
fails.push("unanchored");
|
|
519
|
+
return fails;
|
|
520
|
+
}
|
|
521
|
+
const completionBody = lazyType(() => type({
|
|
522
|
+
"model?": "string",
|
|
523
|
+
choices: type({ message: { content: "string | null" } }).array(),
|
|
524
|
+
"usage?": { prompt_tokens: "number", completion_tokens: "number", "total_tokens?": "number", "cost?": "number" },
|
|
525
|
+
}));
|
|
526
|
+
async function withDeadline(call, timeoutMs) {
|
|
527
|
+
const abort = new AbortController();
|
|
528
|
+
const timer = setTimeout(function expire() {
|
|
529
|
+
abort.abort();
|
|
530
|
+
}, timeoutMs);
|
|
531
|
+
return Promise.resolve()
|
|
532
|
+
.then(function invoke() {
|
|
533
|
+
return call(abort.signal);
|
|
534
|
+
})
|
|
535
|
+
.then(function settle(reply) {
|
|
536
|
+
return abort.signal.aborted ? { timedOut: true } : { timedOut: false, reply };
|
|
537
|
+
}, function failed(e) {
|
|
538
|
+
if (abort.signal.aborted)
|
|
539
|
+
return { timedOut: true };
|
|
540
|
+
return { timedOut: false, reply: Result.Err(e instanceof Error ? e : new Error(String(e))) };
|
|
541
|
+
})
|
|
542
|
+
.finally(function disarm() {
|
|
543
|
+
clearTimeout(timer);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
async function requestMercury(vctx, o, signal) {
|
|
547
|
+
const req = {
|
|
548
|
+
model: MERCURY_FIRST_BUILD_MODEL,
|
|
549
|
+
stream: false,
|
|
550
|
+
max_tokens: o.maxTokens,
|
|
551
|
+
reasoning: { ...MERCURY_FIRST_BUILD_REASONING },
|
|
552
|
+
response_format: { type: "json_schema", json_schema: { name: o.name, strict: true, schema: o.schema } },
|
|
553
|
+
messages: o.messages,
|
|
554
|
+
};
|
|
555
|
+
const rRes = await exception2Result(() => vctx.llmRequest({ ...req, headers: vctx.params.llm.headers }, { signal }));
|
|
556
|
+
if (rRes.isErr())
|
|
557
|
+
return Result.Err(rRes.Err());
|
|
558
|
+
const res = rRes.Ok();
|
|
559
|
+
if (res.ok === false) {
|
|
560
|
+
const { snippet } = await readFailureBody(res);
|
|
561
|
+
const message = `mercury ${o.name}: status ${res.status}${snippet.length === 0 ? "" : ` ${snippet.slice(0, 200)}`}`;
|
|
562
|
+
return MERCURY_TRANSIENT_STATUSES.has(res.status)
|
|
563
|
+
? Result.Err(new MercuryTransientError(message, parseRetryAfterMs(res.headers)))
|
|
564
|
+
: Result.Err(message);
|
|
565
|
+
}
|
|
566
|
+
const rBody = await exception2Result(() => res.json());
|
|
567
|
+
if (rBody.isErr())
|
|
568
|
+
return Result.Err(`mercury ${o.name}: body: ${rBody.Err().message}`);
|
|
569
|
+
const body = completionBody(rBody.Ok());
|
|
570
|
+
if (body instanceof type.errors)
|
|
571
|
+
return Result.Err(`mercury ${o.name}: reply: ${body.summary}`);
|
|
572
|
+
const content = body.choices[0]?.message.content;
|
|
573
|
+
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)));
|
|
576
|
+
if (rJson.isErr())
|
|
577
|
+
return Result.Err(`mercury ${o.name}: JSON: ${rJson.Err().message}`);
|
|
578
|
+
const u = body.usage;
|
|
579
|
+
return Result.Ok({
|
|
580
|
+
json: rJson.Ok(),
|
|
581
|
+
...(body.model === undefined ? {} : { servedModel: body.model }),
|
|
582
|
+
...(u === undefined
|
|
583
|
+
? {}
|
|
584
|
+
: {
|
|
585
|
+
usage: {
|
|
586
|
+
prompt_tokens: u.prompt_tokens,
|
|
587
|
+
completion_tokens: u.completion_tokens,
|
|
588
|
+
total_tokens: u.total_tokens ?? u.prompt_tokens + u.completion_tokens,
|
|
589
|
+
...(u.cost === undefined ? {} : { cost: u.cost }),
|
|
590
|
+
},
|
|
591
|
+
}),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
async function mercuryCall(vctx, o) {
|
|
595
|
+
const startedAt = Date.now();
|
|
596
|
+
const usage = [];
|
|
597
|
+
let servedModel;
|
|
598
|
+
let lastError = "no attempt";
|
|
599
|
+
let transientRetries = 0;
|
|
600
|
+
for (let attempt = 0; attempt < MERCURY_CALL_ATTEMPTS; attempt++) {
|
|
601
|
+
const ask = o.request();
|
|
602
|
+
const r = await withDeadline((signal) => requestMercury(vctx, { name: o.call, schema: ask.schema, messages: ask.messages, maxTokens: o.maxTokens }, signal), o.deadlineMs);
|
|
603
|
+
if (r.timedOut) {
|
|
604
|
+
lastError = `mercury ${o.call}: timed out after ${o.deadlineMs}ms`;
|
|
605
|
+
transientRetries = 0;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (r.reply.isErr()) {
|
|
609
|
+
const err = r.reply.Err();
|
|
610
|
+
lastError = err.message;
|
|
611
|
+
if (err instanceof MercuryTransientError && transientRetries < MERCURY_TRANSIENT_RETRIES) {
|
|
612
|
+
transientRetries += 1;
|
|
613
|
+
await waitMs(Math.min(capRetryDelayMs(err.retryAfterMs, MERCURY_TRANSIENT_BACKOFF_MS * transientRetries), MERCURY_TRANSIENT_MAX_WAIT_MS));
|
|
614
|
+
attempt -= 1;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
transientRetries = 0;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const reply = r.reply.Ok();
|
|
621
|
+
if (reply.usage !== undefined)
|
|
622
|
+
usage.push(reply.usage);
|
|
623
|
+
if (reply.servedModel !== undefined)
|
|
624
|
+
servedModel = reply.servedModel;
|
|
625
|
+
const accepted = o.accept(reply.json);
|
|
626
|
+
if (accepted.isOk()) {
|
|
627
|
+
await recordMercuryRow(vctx, {
|
|
628
|
+
...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
|
+
},
|
|
637
|
+
});
|
|
638
|
+
return accepted;
|
|
639
|
+
}
|
|
640
|
+
lastError = `mercury ${o.call}: ${accepted.Err().message}`;
|
|
641
|
+
transientRetries = 0;
|
|
642
|
+
}
|
|
643
|
+
vctx.logger.Warn().Any("event", { chatId: o.row.chatId, call: o.call, error: lastError }).Msg("mercury-first-build-call-failed");
|
|
644
|
+
await recordMercuryRow(vctx, {
|
|
645
|
+
...o.row,
|
|
646
|
+
record: {
|
|
647
|
+
call: o.call,
|
|
648
|
+
ok: false,
|
|
649
|
+
attempts: MERCURY_CALL_ATTEMPTS,
|
|
650
|
+
elapsedMs: Date.now() - startedAt,
|
|
651
|
+
...(servedModel === undefined ? {} : { servedModel }),
|
|
652
|
+
usage,
|
|
653
|
+
error: lastError,
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
return Result.Err(lastError);
|
|
657
|
+
}
|
|
658
|
+
async function recordMercuryRow(vctx, o) {
|
|
659
|
+
const zero = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
660
|
+
const model = o.record.servedModel ?? MERCURY_FIRST_BUILD_MODEL;
|
|
661
|
+
const rInsert = await exception2Result(() => vctx.sql.db.insert(vctx.sql.tables.promptContexts).values({
|
|
662
|
+
userId: o.userId,
|
|
663
|
+
chatId: o.chatId,
|
|
664
|
+
promptId: vctx.sthis.nextId(12).str,
|
|
665
|
+
nethash: vctx.netHash(),
|
|
666
|
+
promptTokens: 0,
|
|
667
|
+
completionTokens: 0,
|
|
668
|
+
totalTokens: 0,
|
|
669
|
+
ref: {
|
|
670
|
+
type: "prompt.usage.sql",
|
|
671
|
+
usage: { given: o.record.usage.map((u) => ({ ...u, model })), calculated: zero },
|
|
672
|
+
model: MERCURY_FIRST_BUILD_MODEL,
|
|
673
|
+
step: "first-mercury",
|
|
674
|
+
mercury: {
|
|
675
|
+
call: o.record.call,
|
|
676
|
+
ok: o.record.ok,
|
|
677
|
+
attempts: o.record.attempts,
|
|
678
|
+
elapsedMs: o.record.elapsedMs,
|
|
679
|
+
...(o.record.servedModel === undefined ? {} : { servedModel: o.record.servedModel }),
|
|
680
|
+
...(o.record.error === undefined ? {} : { error: o.record.error.slice(0, 300) }),
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
created: new Date().toISOString(),
|
|
684
|
+
}));
|
|
685
|
+
if (rInsert.isErr())
|
|
686
|
+
vctx.logger.Warn().Err(rInsert).Str("chatId", o.chatId).Msg("mercury-first-build-row-failed");
|
|
687
|
+
}
|
|
688
|
+
function owedBriefFields(gated) {
|
|
689
|
+
return MERCURY_BRIEF_FIELDS.filter((f) => (f === "pairs" ? gated.pairs === undefined : gated.texts.has(f) === false));
|
|
690
|
+
}
|
|
691
|
+
const replyObject = lazyType(() => type("Record<string, unknown>"));
|
|
692
|
+
function acceptBriefFields(json, o) {
|
|
693
|
+
const parsed = replyObject(json);
|
|
694
|
+
const body = parsed instanceof type.errors ? {} : parsed;
|
|
695
|
+
const failed = [];
|
|
696
|
+
for (const f of o.owed) {
|
|
697
|
+
const v = body[f];
|
|
698
|
+
if (f === "pairs") {
|
|
699
|
+
const pairs = gateMercuryPairs(v);
|
|
700
|
+
if (pairs.isOk())
|
|
701
|
+
o.into.pairs = pairs.Ok();
|
|
702
|
+
else
|
|
703
|
+
failed.push(`pairs:${pairs.Err().message}`);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const fails = typeof v === "string" ? gateMercuryField({ field: f, value: v, shape: o.shape }) : ["not-a-string"];
|
|
707
|
+
if (fails.length === 0 && typeof v === "string")
|
|
708
|
+
o.into.texts.set(f, v);
|
|
709
|
+
else
|
|
710
|
+
failed.push(`${f}:${fails.join("+")}`);
|
|
711
|
+
}
|
|
712
|
+
return failed;
|
|
713
|
+
}
|
|
714
|
+
export async function runMercuryFirstBuild(vctx, args) {
|
|
715
|
+
return exception2Result(() => pipeline(vctx, args));
|
|
716
|
+
}
|
|
717
|
+
async function pipeline(vctx, args) {
|
|
718
|
+
if (args.personWords.trim().length === 0)
|
|
719
|
+
return Result.Err("mercury: no words to brief from");
|
|
720
|
+
if (vctx.billing.enforceEnabled) {
|
|
721
|
+
const rCheck = await vctx.billing.checkCredits({ userId: args.userId });
|
|
722
|
+
if (rCheck.isOk() && rCheck.Ok().allowed === false)
|
|
723
|
+
return Result.Err("mercury: no credits remaining");
|
|
724
|
+
}
|
|
725
|
+
const startedAt = Date.now();
|
|
726
|
+
const row = { chatId: args.chatId, userId: args.userId };
|
|
727
|
+
const deadlineMs = args.deadlineMs ?? MERCURY_CALL_DEADLINE_MS;
|
|
728
|
+
const nonce = args.nonce ?? mercuryNonce;
|
|
729
|
+
const today = args.today ?? new Date().toISOString().slice(0, 10);
|
|
730
|
+
const rShape = await mercuryCall(vctx, {
|
|
731
|
+
call: "shape",
|
|
732
|
+
row,
|
|
733
|
+
deadlineMs,
|
|
734
|
+
maxTokens: 400,
|
|
735
|
+
request: () => ({
|
|
736
|
+
schema: mercuryShapeSchema(),
|
|
737
|
+
messages: mercuryShapeMessages({ personWords: args.personWords, nonce: nonce() }),
|
|
738
|
+
}),
|
|
739
|
+
accept: (json) => {
|
|
740
|
+
const parsed = parseMercuryShape(json);
|
|
741
|
+
return parsed.isErr() ? parsed : Result.Ok(fixMercuryShape(parsed.Ok(), args.personWords));
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
if (rShape.isErr())
|
|
745
|
+
return Result.Err(rShape);
|
|
746
|
+
const shape = rShape.Ok();
|
|
747
|
+
const gated = { texts: new Map() };
|
|
748
|
+
const rBrief = await mercuryCall(vctx, {
|
|
749
|
+
call: "brief",
|
|
750
|
+
row,
|
|
751
|
+
deadlineMs,
|
|
752
|
+
maxTokens: 1600,
|
|
753
|
+
request: () => {
|
|
754
|
+
const owed = owedBriefFields(gated);
|
|
755
|
+
return {
|
|
756
|
+
schema: mercuryBriefSchema(owed),
|
|
757
|
+
messages: mercuryBriefMessages({
|
|
758
|
+
personWords: args.personWords,
|
|
759
|
+
shape,
|
|
760
|
+
fields: owed,
|
|
761
|
+
words: args.words,
|
|
762
|
+
today,
|
|
763
|
+
nonce: nonce(),
|
|
764
|
+
}),
|
|
765
|
+
};
|
|
766
|
+
},
|
|
767
|
+
accept: (json) => {
|
|
768
|
+
const failed = acceptBriefFields(json, { owed: owedBriefFields(gated), shape, into: gated });
|
|
769
|
+
return failed.length === 0 ? Result.Ok(true) : Result.Err(`gate ${failed.join(", ")}`);
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
if (rBrief.isErr())
|
|
773
|
+
return Result.Err(rBrief);
|
|
774
|
+
const input = assembleMercuryCreateVibe({
|
|
775
|
+
personWords: args.personWords,
|
|
776
|
+
shape,
|
|
777
|
+
fields: {
|
|
778
|
+
opening: gated.texts.get("opening") ?? "",
|
|
779
|
+
access: gated.texts.get("access") ?? "",
|
|
780
|
+
seed: gated.texts.get("seed") ?? "",
|
|
781
|
+
look: gated.texts.get("look") ?? "",
|
|
782
|
+
icon: gated.texts.get("icon") ?? "",
|
|
783
|
+
pairs: gated.pairs ?? [],
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
const enrichedPrompt = input.enrichedPrompt ?? "";
|
|
787
|
+
const rPreamble = await mercuryCall(vctx, {
|
|
788
|
+
call: "preamble",
|
|
789
|
+
row,
|
|
790
|
+
deadlineMs,
|
|
791
|
+
maxTokens: 300,
|
|
792
|
+
request: () => ({
|
|
793
|
+
schema: mercuryPreambleSchema(),
|
|
794
|
+
messages: mercuryPreambleMessages({ personWords: args.personWords, shape, enrichedPrompt, nonce: nonce() }),
|
|
795
|
+
}),
|
|
796
|
+
accept: (json) => {
|
|
797
|
+
const p = preambleParsed(json);
|
|
798
|
+
if (p instanceof type.errors)
|
|
799
|
+
return Result.Err(p.summary);
|
|
800
|
+
const fails = gateMercuryPreamble({ text: p.preamble, enrichedPrompt });
|
|
801
|
+
return fails.length === 0 ? Result.Ok(cleanMercuryText(p.preamble)) : Result.Err(`gate ${fails.join("+")}`);
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
if (rPreamble.isErr())
|
|
805
|
+
return Result.Err(rPreamble);
|
|
806
|
+
return Result.Ok({ input, preamble: rPreamble.Ok(), shape, elapsedMs: Date.now() - startedAt });
|
|
807
|
+
}
|
|
808
|
+
//# sourceMappingURL=mercury-first-build.js.map
|