@vibes.diy/api-svc 4.4.2 → 4.5.4

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.
Files changed (63) hide show
  1. package/app-msg-evento.js.map +1 -1
  2. package/chat-msg-evento.js.map +1 -1
  3. package/intern/prompt-assembly.d.ts +1 -0
  4. package/intern/prompt-assembly.js +11 -7
  5. package/intern/prompt-assembly.js.map +1 -1
  6. package/package.json +12 -15
  7. package/public/access-function.js +73 -15
  8. package/public/access-function.js.map +1 -1
  9. package/public/app-documents-broadcast.d.ts +36 -0
  10. package/public/app-documents-broadcast.js +99 -0
  11. package/public/app-documents-broadcast.js.map +1 -0
  12. package/public/app-documents-grant-delta.d.ts +13 -0
  13. package/public/app-documents-grant-delta.js +76 -0
  14. package/public/app-documents-grant-delta.js.map +1 -0
  15. package/public/app-documents-put-persist.d.ts +28 -0
  16. package/public/app-documents-put-persist.js +106 -0
  17. package/public/app-documents-put-persist.js.map +1 -0
  18. package/public/app-documents-read-eventos.js +10 -4
  19. package/public/app-documents-read-eventos.js.map +1 -1
  20. package/public/app-documents-write-eventos.js +71 -280
  21. package/public/app-documents-write-eventos.js.map +1 -1
  22. package/public/app-settings-icon-gen.d.ts +8 -0
  23. package/public/app-settings-icon-gen.js +29 -0
  24. package/public/app-settings-icon-gen.js.map +1 -0
  25. package/public/app-settings-secrets.d.ts +5 -0
  26. package/public/app-settings-secrets.js +92 -0
  27. package/public/app-settings-secrets.js.map +1 -0
  28. package/public/app-settings-sql-upsert.d.ts +5 -0
  29. package/public/app-settings-sql-upsert.js +139 -0
  30. package/public/app-settings-sql-upsert.js.map +1 -0
  31. package/public/app-settings-suggestion-bless.d.ts +4 -0
  32. package/public/app-settings-suggestion-bless.js +88 -0
  33. package/public/app-settings-suggestion-bless.js.map +1 -0
  34. package/public/ensure-app-settings.d.ts +5 -5
  35. package/public/ensure-app-settings.js +6 -337
  36. package/public/ensure-app-settings.js.map +1 -1
  37. package/public/fs-prompt.d.ts +16 -0
  38. package/public/fs-prompt.js +173 -0
  39. package/public/fs-prompt.js.map +1 -0
  40. package/public/image-gen.d.ts +15 -0
  41. package/public/image-gen.js +222 -0
  42. package/public/image-gen.js.map +1 -0
  43. package/public/llm-dispatch.d.ts +113 -0
  44. package/public/llm-dispatch.js +621 -0
  45. package/public/llm-dispatch.js.map +1 -0
  46. package/public/prompt-chat-section.d.ts +5 -41
  47. package/public/prompt-chat-section.js +10 -1607
  48. package/public/prompt-chat-section.js.map +1 -1
  49. package/public/provider-errors.d.ts +55 -0
  50. package/public/provider-errors.js +150 -0
  51. package/public/provider-errors.js.map +1 -0
  52. package/public/request-flow.js +4 -2
  53. package/public/request-flow.js.map +1 -1
  54. package/public/set-mode-fsid.js +2 -2
  55. package/public/set-mode-fsid.js.map +1 -1
  56. package/public/stream-assembly.d.ts +38 -0
  57. package/public/stream-assembly.js +470 -0
  58. package/public/stream-assembly.js.map +1 -0
  59. package/shared-msg-evento.js.map +1 -1
  60. package/types.js.map +1 -1
  61. package/intern/get-slug-binding.d.ts +0 -7
  62. package/intern/get-slug-binding.js +0 -34
  63. package/intern/get-slug-binding.js.map +0 -1
@@ -1,628 +1,32 @@
1
- import { Result, Option, EventoResult, exception2Result, chunkyAsync } from "@adviser/cement";
2
- import { storeAndAuditAsset } from "./store-and-audit-asset.js";
3
- import { convertImageEvtToFileRef } from "./convert-image-evt.js";
1
+ import { Result, Option, EventoResult, exception2Result } from "@adviser/cement";
4
2
  import { scopey } from "@adviser/scopey";
5
- import { isReqCreationPromptChatSection, isReqPromptApplicationChatSection, isPromptFSStyle, isReqPromptFSChatSection, isReqPromptFSUpdateChatSection, isReqPromptImageChatSection, isReqPromptLLMChatSection, reqPromptChatSection, isReqPromptFSSetChatSection, parseArray, fileSystemItem, isVibeCodeBlock, } from "@vibes.diy/api-types";
6
- import { ensureLogger } from "@vibes.diy/identity";
3
+ import { isReqCreationPromptChatSection, isReqPromptApplicationChatSection, isReqPromptFSChatSection, isReqPromptImageChatSection, isReqPromptLLMChatSection, reqPromptChatSection, } from "@vibes.diy/api-types";
7
4
  import { type } from "arktype";
8
- import { costCentsFromUsage, prodiaCostCents } from "../billing.js";
9
5
  import { checkAuth } from "../check-auth.js";
10
6
  import { unwrapMsgBase, wrapMsgBase } from "../unwrap-msg-base.js";
11
7
  import { assertChatShardIdentity } from "../shard-gate.js";
12
- import { withHeartbeat } from "./stream-heartbeat.js";
13
8
  import { and, desc, eq } from "drizzle-orm/sql/expressions";
14
- import { createStatsCollector, createLineStream, createDataStream, createSseStream, createDeltaStream, createSectionsStream, isBlockEnd, isDeltaLine, FileSystemRef, isBlockStreamMsg, isBlockImage, } from "@vibes.diy/call-ai-v2";
15
- import { getRecoveryAddendum, getRecoveryStitchAddendum } from "@vibes.diy/prompts";
16
- import { ensureAppSlugItem } from "./ensure-app-slug-item.js";
17
9
  import { getModelDefaults } from "../intern/get-model-defaults.js";
18
- import { buildRecoveryRequest, buildTruncatedEvent, shouldAttemptRecovery, updateRecoveryCounter, } from "../intern/recovery.js";
19
- import { loadVersionTimeline, resolveVfsFromFileSystem } from "../intern/version-timeline.js";
20
- import { langForFilename } from "../intern/ensure-push-seeded-chat.js";
21
10
  import { resolveSlotConfig } from "../intern/slot-assembler.js";
22
11
  import { preAllocate } from "../intern/pre-allocate.js";
23
12
  import { dryRunHandleIsForeign } from "../intern/ensure-chat-id.js";
24
13
  import { createPromptAssetFetch, promptsPkgBaseUrl } from "../intern/prompt-asset-fetch.js";
25
14
  import { resolveCodeBlocksToFileSystem, createStreamingResolver, createBlockAccumulator, logApplyError, } from "../intern/prompt-streaming.js";
26
15
  import { reconstructConversationMessages, assemblePromptPayload, persistCodegenThemeMarker, } from "../intern/prompt-assembly.js";
27
- import { stripVibesFileBlocks } from "../intern/inflate-file-attachments.js";
28
16
  import { bumpAppRecency } from "../intern/bump-app-recency.js";
29
- import { acquireChatPersistGate, waitChatPersistIdle } from "../intern/chat-persist-gate.js";
30
- import { loadModels } from "./list-models.js";
17
+ import { waitChatPersistIdle } from "../intern/chat-persist-gate.js";
18
+ import { appendBlockEvent, handlerLlmRequest } from "./llm-dispatch.js";
19
+ import { handleLlmResponse } from "./stream-assembly.js";
20
+ import { handleProdiaImageRequest } from "./image-gen.js";
21
+ import { handleFSPrompt } from "./fs-prompt.js";
31
22
  function clientWsSend(ctx) {
32
23
  return ctx.send.provider;
33
24
  }
34
25
  export { createPromptAssetFetch, promptsPkgBaseUrl };
35
26
  export { resolveCodeBlocksToFileSystem, createStreamingResolver, createBlockAccumulator, logApplyError };
36
- const TRANSIENT_LLM_STATUSES = new Set([429, 502, 503, 504]);
37
- const PRIMARY_RETRY_DELAY_MS = 100;
38
- const FALLBACK_RETRY_DELAY_MS = 250;
39
- const MAX_RETRY_AFTER_MS = 10_000;
40
- const MAX_ATTEMPT_BODY_SNIPPET_CHARS = 2000;
41
- const DISPATCH_FIRST_TOKEN_TIMEOUT_MS = 45_000;
42
- const DISPATCH_TOTAL_BUDGET_MS = 120_000;
43
- function envPositiveInt(vctx, key, fallback) {
44
- const raw = vctx.sthis.env.get(key);
45
- if (typeof raw !== "string")
46
- return fallback;
47
- const n = Number.parseInt(raw, 10);
48
- return Number.isFinite(n) && n > 0 ? n : fallback;
49
- }
50
- function formatAttemptFailure(failure) {
51
- const prefix = `${failure.label} ${failure.model}`;
52
- switch (failure.kind) {
53
- case "http-status":
54
- case "missing-body": {
55
- const statusText = failure.statusText.length > 0 ? ` ${failure.statusText}` : "";
56
- const errorText = failure.kind === "missing-body" ? `: ${failure.error}` : "";
57
- const bodyText = failure.kind === "http-status" && typeof failure.bodySnippet === "string" && failure.bodySnippet.length > 0
58
- ? `: ${failure.bodySnippet}`
59
- : "";
60
- return `${prefix} failed with status ${failure.status}${statusText}${errorText}${bodyText}`;
61
- }
62
- case "exception":
63
- return `${prefix} failed with error ${failure.error}`;
64
- case "timeout":
65
- return `${prefix} timed out after ${failure.elapsedMs}ms with no response`;
66
- }
67
- }
68
- function formatLlmAttemptFailures(failures) {
69
- return `LLM request failed after ${failures.length} attempt${failures.length === 1 ? "" : "s"}: ${failures
70
- .map(formatAttemptFailure)
71
- .join("; ")}`;
72
- }
73
- function parseRetryAfterMs(headers) {
74
- const raw = headers.get("retry-after");
75
- if (raw === null)
76
- return;
77
- const seconds = Number(raw);
78
- if (Number.isFinite(seconds) && seconds >= 0)
79
- return seconds * 1000;
80
- const retryAt = Date.parse(raw);
81
- if (Number.isFinite(retryAt))
82
- return Math.max(0, retryAt - Date.now());
83
- }
84
- function delayForAttempt(failure, defaultDelayMs) {
85
- const retryAfterMs = failure.kind === "http-status" ? failure.retryAfterMs : undefined;
86
- return Math.min(retryAfterMs ?? defaultDelayMs, MAX_RETRY_AFTER_MS);
87
- }
88
- async function waitMs(ms) {
89
- await new Promise((resolve) => {
90
- setTimeout(resolve, ms);
91
- });
92
- }
93
- function boundSnippet(text) {
94
- return text.length > MAX_ATTEMPT_BODY_SNIPPET_CHARS
95
- ? `${text.slice(0, MAX_ATTEMPT_BODY_SNIPPET_CHARS)}...[+${text.length - MAX_ATTEMPT_BODY_SNIPPET_CHARS}b]`
96
- : text;
97
- }
98
- async function readFailureBody(res) {
99
- if (res.body === null)
100
- return { snippet: "", full: "" };
101
- const rText = await exception2Result(() => res.text());
102
- const full = rText.isOk() ? rText.Ok() : `<read-failed: ${String(rText.Err())}>`;
103
- return { snippet: boundSnippet(full), full };
104
- }
105
- const DIAGNOSTIC_HEADER_NAMES = ["cf-ray", "cf-ew-via", "server", "retry-after", "x-generation-id", "content-type"];
106
- function pickDiagnosticHeaders(headers) {
107
- const out = {};
108
- for (const name of DIAGNOSTIC_HEADER_NAMES) {
109
- const value = headers.get(name);
110
- if (value !== null)
111
- out[name] = value;
112
- }
113
- return Object.keys(out).length > 0 ? out : undefined;
114
- }
115
- const OPENROUTER_META_FIELDS = ["requested", "strategy", "region", "summary", "attempt", "is_byok"];
116
- const providerErrorBody = type({
117
- "error?": {
118
- "code?": "unknown",
119
- "message?": "unknown",
120
- "metadata?": {
121
- "provider_name?": "unknown",
122
- "raw?": "unknown",
123
- },
124
- },
125
- "openrouter_metadata?": {
126
- "requested?": "unknown",
127
- "strategy?": "unknown",
128
- "region?": "unknown",
129
- "summary?": "unknown",
130
- "attempt?": "unknown",
131
- "is_byok?": "unknown",
132
- },
133
- });
134
- function extractProviderErrorFields(bodyFull) {
135
- const rParsed = exception2Result(() => providerErrorBody(JSON.parse(bodyFull)));
136
- if (rParsed.isErr())
137
- return {};
138
- const parsed = rParsed.Ok();
139
- if (parsed instanceof type.errors)
140
- return {};
141
- const out = {};
142
- const code = parsed.error?.code;
143
- if (typeof code === "string" || typeof code === "number")
144
- out.errorCode = code;
145
- if (typeof parsed.error?.message === "string")
146
- out.errorMessage = parsed.error.message;
147
- const providerName = parsed.error?.metadata?.provider_name;
148
- if (typeof providerName === "string")
149
- out.providerName = providerName;
150
- const raw = parsed.error?.metadata?.raw;
151
- if (typeof raw === "string")
152
- out.rawSnippet = boundSnippet(raw);
153
- else if (raw !== undefined)
154
- out.rawSnippet = boundSnippet(JSON.stringify(raw));
155
- const orMeta = parsed.openrouter_metadata;
156
- if (orMeta !== undefined) {
157
- const picked = {};
158
- for (const field of OPENROUTER_META_FIELDS) {
159
- if (orMeta[field] !== undefined)
160
- picked[field] = orMeta[field];
161
- }
162
- if (Object.keys(picked).length > 0)
163
- out.openrouterMeta = picked;
164
- }
165
- return out;
166
- }
167
- const INVALID_MODEL_STATUSES = new Set([400, 404]);
168
- const INVALID_MODEL_MESSAGE_RE = /not a valid model|invalid model|unknown model|no endpoints found for|model\b.{0,120}\b(not found|does not exist|no longer (available|supported)|deprecated|retired)/i;
169
- function isInvalidModelError(status, provider) {
170
- if (!INVALID_MODEL_STATUSES.has(status))
171
- return false;
172
- return typeof provider.errorMessage === "string" && INVALID_MODEL_MESSAGE_RE.test(provider.errorMessage);
173
- }
174
- function extractEdgeError(bodySnippet, rawSnippet, headers) {
175
- const haystacks = [bodySnippet, rawSnippet].filter((s) => typeof s === "string" && s.length > 0);
176
- let code;
177
- for (const hay of haystacks) {
178
- const match = hay.match(/error code:\s*(\d{3,4})/i) ?? hay.match(/\bError\s+(\d{3,4})\b/);
179
- if (match !== null) {
180
- code = match[1];
181
- break;
182
- }
183
- }
184
- if (code === undefined)
185
- return {};
186
- const server = headers.get("server") ?? "";
187
- const cloudflareMarker = headers.get("cf-ray") !== null ||
188
- headers.get("cf-ew-via") !== null ||
189
- /cloudflare/i.test(server) ||
190
- haystacks.some((hay) => /cloudflare/i.test(hay));
191
- const edgeErrorVendor = cloudflareMarker ? "cloudflare" : "unknown";
192
- const edgeErrorFamily = edgeErrorVendor === "cloudflare" && /^1\d{3}$/.test(code) ? "cloudflare-1xxx" : "unknown";
193
- return { edgeErrorCode: code, edgeErrorVendor, edgeErrorFamily };
194
- }
195
- function logLlmAttemptFailure(args) {
196
- const { vctx, diag, label, model, elapsedMs, details } = args;
197
- vctx.logger
198
- .Warn()
199
- .Any("event", {
200
- chatId: diag.chatId,
201
- promptId: diag.promptId,
202
- phase: diag.phase,
203
- mode: diag.mode,
204
- label,
205
- model,
206
- elapsedMs,
207
- ...details,
208
- })
209
- .Msg("llm-request-attempt-failed");
210
- }
211
- async function findFallbackModel(vctx, mode, failedModel) {
212
- if (mode !== "codegen" && mode !== "runtime") {
213
- return Result.Err(`Mode ${mode} does not support LLM fallback`);
214
- }
215
- const rModels = await loadModels(vctx);
216
- if (rModels.isErr()) {
217
- vctx.logger.Warn().Err(rModels).Msg("llm-fallback-model-load-failed");
218
- return Result.Err(`Failed to load fallback model catalog: ${String(rModels.Err())}`);
219
- }
220
- for (const model of rModels.Ok().models) {
221
- if (model.id !== failedModel && Array.isArray(model.fallbackFor) && model.fallbackFor.includes(mode)) {
222
- return Result.Ok(model.id);
223
- }
224
- }
225
- return Result.Err(`No fallback model configured for ${mode} after ${failedModel}`);
226
- }
227
- async function attemptLlmRequest(args) {
228
- const { vctx, diag, llmReq, label, firstTokenTimeoutMs } = args;
229
- const abort = new AbortController();
230
- const startedAt = Date.now();
231
- let timedOut = false;
232
- const timer = setTimeout(() => {
233
- timedOut = true;
234
- abort.abort();
235
- }, firstTokenTimeoutMs);
236
- const rRes = await exception2Result(() => vctx.llmRequest(llmReq, { signal: abort.signal }));
237
- clearTimeout(timer);
238
- const elapsedMs = Date.now() - startedAt;
239
- if (rRes.isErr()) {
240
- abort.abort();
241
- if (timedOut) {
242
- logLlmAttemptFailure({
243
- vctx,
244
- diag,
245
- label,
246
- model: llmReq.model,
247
- elapsedMs,
248
- details: { kind: "timeout", retryable: true, timeoutMs: firstTokenTimeoutMs },
249
- });
250
- return {
251
- ok: false,
252
- abort,
253
- failure: {
254
- kind: "timeout",
255
- label,
256
- model: llmReq.model,
257
- elapsedMs,
258
- retryable: true,
259
- },
260
- };
261
- }
262
- const error = String(rRes.Err());
263
- logLlmAttemptFailure({
264
- vctx,
265
- diag,
266
- label,
267
- model: llmReq.model,
268
- elapsedMs,
269
- details: { kind: "exception", retryable: false, error },
270
- });
271
- return {
272
- ok: false,
273
- abort,
274
- failure: {
275
- kind: "exception",
276
- label,
277
- model: llmReq.model,
278
- error,
279
- retryable: false,
280
- },
281
- };
282
- }
283
- const res = rRes.Ok();
284
- if (res.ok && res.body !== null) {
285
- return { ok: true, res, abort };
286
- }
287
- if (res.ok) {
288
- abort.abort();
289
- logLlmAttemptFailure({
290
- vctx,
291
- diag,
292
- label,
293
- model: llmReq.model,
294
- elapsedMs,
295
- details: {
296
- kind: "missing-body",
297
- status: res.status,
298
- statusText: res.statusText,
299
- retryable: false,
300
- error: "response body missing",
301
- },
302
- });
303
- return {
304
- ok: false,
305
- abort,
306
- failure: {
307
- kind: "missing-body",
308
- label,
309
- model: llmReq.model,
310
- status: res.status,
311
- statusText: res.statusText,
312
- error: "response body missing",
313
- retryable: false,
314
- },
315
- };
316
- }
317
- const { snippet: bodySnippet, full: bodyFull } = await readFailureBody(res);
318
- const retryAfterMs = parseRetryAfterMs(res.headers);
319
- abort.abort();
320
- const retryable = TRANSIENT_LLM_STATUSES.has(res.status);
321
- const provider = extractProviderErrorFields(bodyFull);
322
- const invalidModel = isInvalidModelError(res.status, provider);
323
- const edge = extractEdgeError(bodySnippet, provider.rawSnippet, res.headers);
324
- const headers = pickDiagnosticHeaders(res.headers);
325
- logLlmAttemptFailure({
326
- vctx,
327
- diag,
328
- label,
329
- model: llmReq.model,
330
- elapsedMs,
331
- details: {
332
- kind: "http-status",
333
- status: res.status,
334
- statusText: res.statusText,
335
- retryable,
336
- ...(invalidModel ? { invalidModel } : {}),
337
- ...(typeof retryAfterMs === "number" ? { retryAfterMs } : {}),
338
- ...(headers ? { headers } : {}),
339
- ...(bodySnippet.length > 0 ? { bodySnippet } : {}),
340
- ...provider,
341
- ...edge,
342
- },
343
- });
344
- const failure = {
345
- kind: "http-status",
346
- label,
347
- model: llmReq.model,
348
- status: res.status,
349
- statusText: res.statusText,
350
- retryable,
351
- ...(invalidModel ? { invalidModel } : {}),
352
- ...(bodySnippet.length > 0 ? { bodySnippet } : {}),
353
- ...(typeof retryAfterMs === "number" ? { retryAfterMs } : {}),
354
- };
355
- return {
356
- ok: false,
357
- abort,
358
- failure,
359
- };
360
- }
361
- async function dispatchLlmRequestWithFallback(args) {
362
- const attempts = [];
363
- const diag = {
364
- chatId: args.chatId,
365
- promptId: args.promptId,
366
- phase: args.phase,
367
- mode: args.mode,
368
- };
369
- const firstTokenTimeoutMs = envPositiveInt(args.vctx, "DISPATCH_FIRST_TOKEN_TIMEOUT_MS", DISPATCH_FIRST_TOKEN_TIMEOUT_MS);
370
- const totalBudgetMs = envPositiveInt(args.vctx, "DISPATCH_TOTAL_BUDGET_MS", DISPATCH_TOTAL_BUDGET_MS);
371
- const dispatchDeadline = Date.now() + totalBudgetMs;
372
- const budgetRemainingMs = () => Math.max(0, dispatchDeadline - Date.now());
373
- const attemptTimeoutMs = () => Math.min(firstTokenTimeoutMs, budgetRemainingMs());
374
- const backoffMs = (failure, defaultDelayMs) => Math.min(delayForAttempt(failure, defaultDelayMs), budgetRemainingMs());
375
- const budgetExhausted = () => {
376
- const abort = new AbortController();
377
- abort.abort();
378
- const note = `LLM dispatch exceeded ${totalBudgetMs}ms budget before a response`;
379
- const error = attempts.length > 0 ? `${formatLlmAttemptFailures(attempts)}; ${note}` : note;
380
- args.vctx.logger
381
- .Warn()
382
- .Any("event", { chatId: args.chatId, promptId: args.promptId, phase: args.phase, mode: args.mode, attempts: attempts.length })
383
- .Msg("llm-dispatch-budget-exhausted");
384
- return { ok: false, llmReq: args.llmReq, abort, attempts, error };
385
- };
386
- let lastPrimary;
387
- for (const label of ["primary", "primary-retry"]) {
388
- if (lastPrimary) {
389
- await waitMs(backoffMs(lastPrimary.failure, PRIMARY_RETRY_DELAY_MS));
390
- }
391
- if (Date.now() >= dispatchDeadline)
392
- return budgetExhausted();
393
- const attempt = await attemptLlmRequest({
394
- vctx: args.vctx,
395
- diag,
396
- llmReq: args.llmReq,
397
- label,
398
- firstTokenTimeoutMs: attemptTimeoutMs(),
399
- });
400
- if (attempt.ok)
401
- return { ok: true, res: attempt.res, llmReq: args.llmReq, abort: attempt.abort, attempts };
402
- attempts.push(attempt.failure);
403
- if (attempt.failure.retryable === false) {
404
- if (attempt.failure.kind === "http-status" && attempt.failure.invalidModel === true) {
405
- lastPrimary = attempt;
406
- break;
407
- }
408
- return { ok: false, llmReq: args.llmReq, abort: attempt.abort, attempts, error: formatLlmAttemptFailures(attempts) };
409
- }
410
- lastPrimary = attempt;
411
- }
412
- const failedPrimary = lastPrimary;
413
- const rFallbackModel = await findFallbackModel(args.vctx, args.mode, args.llmReq.model);
414
- if (rFallbackModel.isErr()) {
415
- const error = `${formatLlmAttemptFailures(attempts)}; ${String(rFallbackModel.Err())}`;
416
- return { ok: false, llmReq: args.llmReq, abort: failedPrimary.abort, attempts, error };
417
- }
418
- const fallbackModel = rFallbackModel.Ok();
419
- const fallbackReq = { ...args.llmReq, model: fallbackModel };
420
- await waitMs(backoffMs(failedPrimary.failure, FALLBACK_RETRY_DELAY_MS));
421
- if (Date.now() >= dispatchDeadline)
422
- return budgetExhausted();
423
- const fallback = await attemptLlmRequest({
424
- vctx: args.vctx,
425
- diag,
426
- llmReq: fallbackReq,
427
- label: "fallback",
428
- firstTokenTimeoutMs: attemptTimeoutMs(),
429
- });
430
- if (fallback.ok) {
431
- args.vctx.logger
432
- .Info()
433
- .Any("event", {
434
- mode: args.mode,
435
- fromModel: args.llmReq.model,
436
- toModel: fallbackModel,
437
- })
438
- .Msg("llm-fallback-succeeded");
439
- return { ok: true, res: fallback.res, llmReq: fallbackReq, abort: fallback.abort, attempts };
440
- }
441
- attempts.push(fallback.failure);
442
- return { ok: false, llmReq: fallbackReq, abort: fallback.abort, attempts, error: formatLlmAttemptFailures(attempts) };
443
- }
444
- async function appendBlockEvent({ ctx, vctx, req, promptId, blockSeq, evt, emitMode = "store", resChat, }) {
445
- let processedEvt = evt;
446
- if (isBlockImage(evt) && evt.url && resChat) {
447
- const rConv = await convertImageEvtToFileRef(vctx, {
448
- evt,
449
- userId: req._auth.verifiedAuth.claims.userId,
450
- ownerHandle: resChat.ownerHandle,
451
- appSlug: resChat.appSlug,
452
- });
453
- if (rConv.isOk()) {
454
- processedEvt = rConv.Ok();
455
- }
456
- else {
457
- vctx.logger.Error().Err(rConv).Msg("[block.image] convert failed");
458
- }
459
- }
460
- const now = new Date();
461
- const msgBase = wrapMsgBase(ctx.validated, {
462
- payload: {
463
- type: "vibes.diy.section-event",
464
- chatId: req.chatId,
465
- promptId,
466
- blockSeq,
467
- blocks: [processedEvt],
468
- timestamp: now,
469
- },
470
- tid: req.outerTid,
471
- src: "promptChatSection",
472
- });
473
- if (emitMode !== "store-only") {
474
- for (const conn of vctx.connections) {
475
- const chatCtx = conn.chatIds.get(req.chatId);
476
- if (chatCtx) {
477
- for (const tid of chatCtx.tids) {
478
- await conn.send(ctx, { ...msgBase, tid });
479
- }
480
- }
481
- }
482
- }
483
- if (emitMode === "store" || emitMode === "store-only") {
484
- const rUpdate = await exception2Result(() => vctx.sql.db.insert(vctx.sql.tables.chatSections).values({
485
- chatId: req.chatId,
486
- promptId,
487
- blockSeq,
488
- blocks: [processedEvt],
489
- created: now.toISOString(),
490
- }));
491
- if (rUpdate.isErr()) {
492
- return Result.Err(rUpdate);
493
- }
494
- }
495
- return Result.Ok();
496
- }
497
- export async function handlePromptContext({ vctx, req, resChat, promptId, blockSeq, value, collectedMsgs: iCollectedMsgs, blockChunks = 100, fileSystem, model, codegenTheme, onFsRef, }) {
498
- let fsRef = Option.None();
499
- const code = [];
500
- const sections = [];
501
- const collectedMsgs = [...iCollectedMsgs];
502
- const blockAcc = createBlockAccumulator();
503
- for (const msg of collectedMsgs) {
504
- if (!isBlockStreamMsg(msg)) {
505
- continue;
506
- }
507
- let sqlVal = sections[sections.length - 1];
508
- if (sections.length === 0 || sqlVal.blocks.length >= blockChunks) {
509
- sections.push({
510
- chatId: req.chatId,
511
- promptId,
512
- blockSeq: blockSeq++,
513
- blocks: [],
514
- created: msg.timestamp.toISOString(),
515
- });
516
- sqlVal = sections[sections.length - 1];
517
- }
518
- sqlVal.blocks.push(msg);
519
- const closed = blockAcc.ingest(msg);
520
- if (closed)
521
- code.push({ begin: closed.begin, lines: [...closed.lines], end: closed.end });
522
- }
523
- if (code.length > 0 && (resChat.mode === "codegen" || isPromptFSStyle(resChat.mode))) {
524
- let resolvedFileSystem;
525
- if (fileSystem) {
526
- resolvedFileSystem = fileSystem;
527
- }
528
- else {
529
- const timelineResult = await loadVersionTimeline(vctx, req.chatId);
530
- if (timelineResult.isErr())
531
- return Result.Err(timelineResult);
532
- const timeline = timelineResult.Ok();
533
- const seed = timeline.length > 0 ? timeline[timeline.length - 1].vfs : new Map();
534
- resolvedFileSystem = resolveCodeBlocksToFileSystem(code, seed);
535
- }
536
- const rFs = await ensureAppSlugItem(vctx, {
537
- type: "vibes.diy.req-ensure-app-slug",
538
- mode: "dev",
539
- runId: promptId,
540
- appSlug: resChat.appSlug,
541
- ownerHandle: resChat.ownerHandle,
542
- fileSystem: resolvedFileSystem,
543
- auth: req.auth,
544
- _auth: req._auth,
545
- });
546
- if (rFs.isErr()) {
547
- console.error("Failed to ensure app slug item for code blocks:", rFs.Err());
548
- return Result.Err(rFs);
549
- }
550
- const tFsRef = type(FileSystemRef).onDeepUndeclaredKey("delete")(rFs.Ok());
551
- if (tFsRef instanceof type.errors) {
552
- return Result.Err(`Failed to parse FileSystemRef: ${tFsRef.summary}`);
553
- }
554
- fsRef = Option.Some(tFsRef);
555
- if (onFsRef) {
556
- const rReveal = await exception2Result(() => onFsRef(tFsRef));
557
- if (rReveal.isErr()) {
558
- vctx.logger.Warn().Err(rReveal).Str("promptId", promptId).Msg("reveal-emit-failed");
559
- }
560
- }
561
- }
562
- value.fsRef = fsRef.toValue();
563
- const rSql = await exception2Result(() => vctx.sql.db.insert(vctx.sql.tables.promptContexts).values({
564
- userId: req._auth.verifiedAuth.claims.userId,
565
- chatId: req.chatId,
566
- promptId,
567
- fsId: fsRef.IsSome() ? fsRef.unwrap().fsId : undefined,
568
- nethash: vctx.netHash(),
569
- promptTokens: value.usage.calculated.prompt_tokens,
570
- completionTokens: value.usage.calculated.completion_tokens,
571
- totalTokens: value.usage.calculated.total_tokens,
572
- ref: {
573
- type: "prompt.usage.sql",
574
- usage: value.usage,
575
- fsRef: fsRef.toValue(),
576
- ...(model !== undefined && model !== "" ? { model } : {}),
577
- },
578
- created: new Date().toISOString(),
579
- }));
580
- if (rSql.isErr()) {
581
- return Result.Err(rSql);
582
- }
583
- const costCents = costCentsFromUsage(value.usage);
584
- const billingP = (async () => {
585
- if (costCents <= 0)
586
- return;
587
- const rTrack = await vctx.billing.trackUsage({
588
- userId: req._auth.verifiedAuth.claims.userId,
589
- costCents,
590
- promptId,
591
- ...(model !== undefined && model !== "" ? { model } : {}),
592
- mode: resChat.mode,
593
- });
594
- if (rTrack.isErr()) {
595
- vctx.logger.Warn().Err(rTrack).Str("promptId", promptId).Msg("billing-track-failed");
596
- }
597
- })();
598
- const markerP = (async () => {
599
- if (!(resChat.mode === "codegen" && fsRef.IsSome()))
600
- return;
601
- const rMark = await exception2Result(() => persistCodegenThemeMarker(vctx, {
602
- userId: req._auth.verifiedAuth.claims.userId,
603
- ownerHandle: resChat.ownerHandle,
604
- appSlug: resChat.appSlug,
605
- theme: codegenTheme,
606
- }));
607
- if (rMark.isErr()) {
608
- vctx.logger.Warn().Err(rMark).Str("chatId", req.chatId).Msg("persistCodegenThemeMarker failed");
609
- }
610
- })();
611
- await Promise.all([
612
- billingP,
613
- markerP,
614
- chunkyAsync({
615
- input: sections,
616
- splitCondition: (secChunk) => secChunk.length >= 20,
617
- commit: async (secChunk) => {
618
- await exception2Result(() => vctx.sql.db.insert(vctx.sql.tables.chatSections).values(secChunk));
619
- },
620
- }),
621
- ]);
622
- iCollectedMsgs.splice(0, collectedMsgs.length);
623
- return Result.Ok({ blockSeq, fsRef });
624
- }
625
27
  export { reconstructConversationMessages, assemblePromptPayload, persistCodegenThemeMarker };
28
+ export { handlePromptContext } from "./llm-dispatch.js";
29
+ export { handleFSPrompt };
626
30
  function firstUserText(messages) {
627
31
  const firstUser = messages.find((m) => m.role === "user");
628
32
  if (!firstUser)
@@ -685,1008 +89,6 @@ async function getResChatFromMode(vctx, req, orig) {
685
89
  const resChat = { ...iResChat, mode: orig.mode };
686
90
  return Result.Ok(resChat);
687
91
  }
688
- async function handlerLlmRequest({ scope, ctx, vctx, blockSeq, req, resChat, promptId, preAssembled, }) {
689
- const modelId = await scope
690
- .evalResult(async () => {
691
- const r = await getModelDefaults(vctx, { appSlug: resChat.appSlug, ownerHandle: resChat.ownerHandle });
692
- if (r.isErr()) {
693
- return Result.Err(r);
694
- }
695
- switch (req.mode) {
696
- case "codegen":
697
- return Result.Ok(req.prompt.model ?? r.Ok().codegen.model.id);
698
- case "runtime":
699
- return Result.Ok(req.prompt.model ?? r.Ok().runtime.model.id);
700
- case "img":
701
- return Result.Ok(req.prompt.model ?? r.Ok().img.model.id);
702
- default:
703
- return Result.Err(`Unknown prompt mode: ${req.mode}`);
704
- }
705
- })
706
- .do();
707
- const withSystemPrompt = await scope
708
- .evalResult(async () => {
709
- let withSystemPrompt = Result.Ok({
710
- model: modelId,
711
- messages: [],
712
- });
713
- if (preAssembled !== undefined) {
714
- withSystemPrompt = Result.Ok(preAssembled);
715
- }
716
- else if (req.mode === "runtime" || req.mode === "img") {
717
- let messages = stripVibesFileBlocks(req.prompt.messages);
718
- if (req.mode === "img") {
719
- const imgReq = req;
720
- if (imgReq.inputImageBase64) {
721
- const lastUserIdx = (() => {
722
- for (let i = messages.length - 1; i >= 0; i--) {
723
- if (messages[i].role === "user")
724
- return i;
725
- }
726
- return -1;
727
- })();
728
- if (lastUserIdx >= 0) {
729
- const target = messages[lastUserIdx];
730
- messages = [
731
- ...messages.slice(0, lastUserIdx),
732
- {
733
- ...target,
734
- content: [...target.content, { type: "image_url", image_url: { url: imgReq.inputImageBase64 } }],
735
- },
736
- ...messages.slice(lastUserIdx + 1),
737
- ];
738
- }
739
- }
740
- }
741
- withSystemPrompt = Result.Ok({
742
- model: req.prompt.model ?? modelId,
743
- messages,
744
- });
745
- }
746
- return withSystemPrompt;
747
- })
748
- .do();
749
- const isInitialTurn = withSystemPrompt.messages.filter((m) => m.role === "user").length <= 1;
750
- const llmReq = {
751
- ...{
752
- ...req.prompt,
753
- messages: withSystemPrompt.messages,
754
- },
755
- ...vctx.params.llm.enforced,
756
- model: withSystemPrompt.model,
757
- headers: vctx.params.llm.headers,
758
- stream: true,
759
- ...(isInitialTurn && req.mode === "codegen" ? { verbosity: "low" } : {}),
760
- ...(req.mode === "img" ? { modalities: ["text", "image"] } : {}),
761
- };
762
- await scope
763
- .evalResult(async () => {
764
- const r = await appendBlockEvent({
765
- ctx,
766
- vctx,
767
- req,
768
- promptId,
769
- blockSeq: blockSeq,
770
- evt: {
771
- type: "prompt.req",
772
- streamId: promptId,
773
- chatId: req.chatId,
774
- seq: blockSeq,
775
- request: { ...req.prompt, model: modelId },
776
- timestamp: new Date(),
777
- },
778
- });
779
- blockSeq++;
780
- return r;
781
- })
782
- .do();
783
- if (vctx.billing.enforceEnabled) {
784
- await scope
785
- .evalResult(async () => {
786
- const rCheck = await vctx.billing.checkCredits({ userId: req._auth.verifiedAuth.claims.userId });
787
- if (rCheck.isErr()) {
788
- vctx.logger.Warn().Err(rCheck).Str("chatId", req.chatId).Msg("billing-check-failed-open");
789
- return Result.Ok(undefined);
790
- }
791
- if (rCheck.Ok().allowed === false) {
792
- return Result.Err("credits-exhausted: you have used your included AI credits — upgrade to continue");
793
- }
794
- return Result.Ok(undefined);
795
- })
796
- .do();
797
- }
798
- const dispatchStartedAt = Date.now();
799
- vctx.logger.Info().Any("event", { chatId: req.chatId, promptId, mode: req.mode, model: modelId }).Msg("llm-dispatch-start");
800
- const dispatch = await dispatchLlmRequestWithFallback({
801
- vctx,
802
- mode: req.mode,
803
- llmReq,
804
- chatId: req.chatId,
805
- promptId,
806
- phase: "initial",
807
- });
808
- vctx.logger
809
- .Info()
810
- .Any("event", {
811
- chatId: req.chatId,
812
- promptId,
813
- mode: req.mode,
814
- ok: dispatch.ok,
815
- model: dispatch.llmReq.model,
816
- attempts: dispatch.attempts.length,
817
- elapsedMs: Date.now() - dispatchStartedAt,
818
- })
819
- .Msg("llm-dispatch-settled");
820
- const res = await scope
821
- .evalResult(async () => {
822
- return dispatch.ok ? Result.Ok(dispatch.res) : Result.Err(dispatch.error);
823
- })
824
- .do();
825
- const finalModel = dispatch.llmReq.model;
826
- if (dispatch.ok && finalModel !== undefined && finalModel !== modelId) {
827
- await scope
828
- .evalResult(async () => {
829
- const r = await appendBlockEvent({
830
- ctx,
831
- vctx,
832
- req,
833
- promptId,
834
- blockSeq,
835
- evt: {
836
- type: "prompt.model-resolved",
837
- streamId: promptId,
838
- chatId: req.chatId,
839
- seq: blockSeq,
840
- model: finalModel,
841
- timestamp: new Date(),
842
- },
843
- });
844
- blockSeq++;
845
- return r;
846
- })
847
- .do();
848
- }
849
- return { res, blockSeq, llmReq: dispatch.llmReq, abort: dispatch.abort };
850
- }
851
- async function handleProdiaImageRequest({ scope, ctx, vctx, req, promptId, blockSeq, resolvedModel, resChat, }) {
852
- const prodiaToken = vctx.prodiaToken;
853
- if (!prodiaToken) {
854
- return Result.Err("PRODIA_TOKEN not configured");
855
- }
856
- const prodiaStem = resolvedModel.startsWith("prodia/") ? resolvedModel.slice("prodia/".length) : "";
857
- if (!prodiaStem) {
858
- return Result.Err(`Invalid Prodia model id: ${resolvedModel}`);
859
- }
860
- const userMessages = req.prompt.messages.filter((m) => m.role === "user");
861
- const lastUserMsg = userMessages[userMessages.length - 1];
862
- const firstTextPart = lastUserMsg?.content?.find((c) => c.type === "text");
863
- const promptText = firstTextPart?.type === "text" ? firstTextPart.text : "";
864
- if (!promptText) {
865
- return Result.Err("No prompt text found in user messages");
866
- }
867
- const inputImageDataUrl = req.inputImageBase64;
868
- await scope
869
- .evalResult(async () => {
870
- const r = await appendBlockEvent({
871
- ctx,
872
- vctx,
873
- req,
874
- promptId,
875
- blockSeq,
876
- evt: {
877
- type: "prompt.req",
878
- streamId: promptId,
879
- chatId: req.chatId,
880
- seq: blockSeq,
881
- request: req.prompt,
882
- timestamp: new Date(),
883
- },
884
- });
885
- blockSeq++;
886
- return r;
887
- })
888
- .do();
889
- let prodiaRes;
890
- if (inputImageDataUrl) {
891
- const base64Data = inputImageDataUrl.split(",")[1];
892
- const binaryString = atob(base64Data);
893
- const bytes = new Uint8Array(binaryString.length);
894
- for (let i = 0; i < binaryString.length; i++) {
895
- bytes[i] = binaryString.charCodeAt(i);
896
- }
897
- const formData = new FormData();
898
- formData.append("job", new Blob([JSON.stringify({ type: `inference.${prodiaStem}.img2img.v1`, config: { prompt: promptText } })], {
899
- type: "application/json",
900
- }), "job.json");
901
- formData.append("input", new Blob([bytes], { type: "image/jpeg" }), "input.jpg");
902
- prodiaRes = await vctx.inferenceFetch("https://inference.prodia.com/v2/job", {
903
- method: "POST",
904
- headers: {
905
- Authorization: `Bearer ${prodiaToken}`,
906
- Accept: "image/png",
907
- },
908
- body: formData,
909
- });
910
- }
911
- else {
912
- prodiaRes = await vctx.inferenceFetch("https://inference.prodia.com/v2/job", {
913
- method: "POST",
914
- headers: {
915
- Authorization: `Bearer ${prodiaToken}`,
916
- Accept: "image/png",
917
- "Content-Type": "application/json",
918
- },
919
- body: JSON.stringify({
920
- type: `inference.${prodiaStem}.txt2img.v1`,
921
- config: { prompt: promptText },
922
- }),
923
- });
924
- }
925
- if (!prodiaRes.ok) {
926
- const errorText = await prodiaRes.text().catch(() => "");
927
- return Result.Err(`Prodia request failed: ${prodiaRes.status} ${prodiaRes.statusText} ${errorText}`);
928
- }
929
- if (!prodiaRes.body) {
930
- return Result.Err("Prodia response has no body");
931
- }
932
- const rStored = await storeAndAuditAsset(vctx, {
933
- bytes: prodiaRes.body,
934
- userId: req._auth.verifiedAuth.claims.userId,
935
- ownerHandle: resChat.ownerHandle,
936
- appSlug: resChat.appSlug,
937
- mimeType: "image/png",
938
- });
939
- if (rStored.isErr()) {
940
- return Result.Err(`Failed to store Prodia image: ${rStored.Err().message}`);
941
- }
942
- const imageCostCents = prodiaCostCents(prodiaStem);
943
- const zeroTokens = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
944
- const rPromptCtx = await exception2Result(() => vctx.sql.db.insert(vctx.sql.tables.promptContexts).values({
945
- userId: req._auth.verifiedAuth.claims.userId,
946
- chatId: req.chatId,
947
- promptId,
948
- nethash: vctx.netHash(),
949
- promptTokens: 0,
950
- completionTokens: 0,
951
- totalTokens: 0,
952
- ref: {
953
- type: "prompt.usage.sql",
954
- usage: {
955
- given: [{ ...zeroTokens, cost: imageCostCents / 100 }],
956
- calculated: zeroTokens,
957
- },
958
- model: resolvedModel,
959
- },
960
- created: new Date().toISOString(),
961
- }));
962
- if (rPromptCtx.isErr()) {
963
- vctx.logger.Warn().Err(rPromptCtx).Str("promptId", promptId).Msg("image-prompt-context-persist-failed");
964
- }
965
- if (imageCostCents > 0) {
966
- const rTrack = await vctx.billing.trackUsage({
967
- userId: req._auth.verifiedAuth.claims.userId,
968
- costCents: imageCostCents,
969
- promptId,
970
- model: resolvedModel,
971
- mode: "image",
972
- });
973
- if (rTrack.isErr()) {
974
- vctx.logger.Warn().Err(rTrack).Str("promptId", promptId).Msg("billing-track-failed");
975
- }
976
- }
977
- const stored = rStored.Ok();
978
- const fileMimeType = stored.mimeType ?? "image/png";
979
- const blockId = vctx.sthis.nextId(12).str;
980
- const now = new Date();
981
- await appendBlockEvent({
982
- ctx,
983
- vctx,
984
- req,
985
- promptId,
986
- blockSeq: blockSeq++,
987
- evt: {
988
- type: "block.begin",
989
- blockId,
990
- streamId: promptId,
991
- seq: blockSeq,
992
- blockNr: 0,
993
- timestamp: now,
994
- },
995
- });
996
- await appendBlockEvent({
997
- ctx,
998
- vctx,
999
- req,
1000
- promptId,
1001
- blockSeq: blockSeq++,
1002
- evt: {
1003
- type: "block.image",
1004
- sectionId: vctx.sthis.nextId(12).str,
1005
- uploadId: stored.uploadId,
1006
- cid: stored.cid,
1007
- mimeType: fileMimeType,
1008
- size: stored.size,
1009
- blockId,
1010
- streamId: promptId,
1011
- seq: blockSeq,
1012
- blockNr: 1,
1013
- timestamp: now,
1014
- stats: { lines: 0, bytes: stored.size, cnt: 1 },
1015
- },
1016
- });
1017
- const zeroStats = { lines: 0, bytes: 0 };
1018
- const imageStats = { lines: 0, bytes: stored.size, cnt: 1 };
1019
- await appendBlockEvent({
1020
- ctx,
1021
- vctx,
1022
- req,
1023
- promptId,
1024
- blockSeq: blockSeq++,
1025
- evt: {
1026
- type: "block.end",
1027
- blockId,
1028
- streamId: promptId,
1029
- seq: blockSeq,
1030
- blockNr: 2,
1031
- timestamp: now,
1032
- stats: {
1033
- toplevel: zeroStats,
1034
- code: zeroStats,
1035
- image: imageStats,
1036
- total: imageStats,
1037
- },
1038
- usage: {
1039
- given: [],
1040
- calculated: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
1041
- },
1042
- },
1043
- emitMode: "emit-only",
1044
- });
1045
- const promptContextSql = {
1046
- type: "prompt.usage.sql",
1047
- usage: {
1048
- given: [],
1049
- calculated: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
1050
- },
1051
- };
1052
- await exception2Result(() => vctx.sql.db.insert(vctx.sql.tables.chatSections).values({
1053
- chatId: req.chatId,
1054
- promptId,
1055
- blockSeq: blockSeq++,
1056
- blocks: [promptContextSql],
1057
- created: now.toISOString(),
1058
- }));
1059
- for (const conn of vctx.connections) {
1060
- const chatCtx = conn.chatIds.get(req.chatId);
1061
- const promptIdCtx = chatCtx?.promptIds.get(promptId);
1062
- if (promptIdCtx && chatCtx) {
1063
- chatCtx.promptIds.delete(promptId);
1064
- }
1065
- }
1066
- return Result.Ok(blockSeq);
1067
- }
1068
- async function handleEndMsg({ collectedMsgs, vctx, req, ctx, resChat, promptId, value, blockSeq, fileSystem, terminal, model, codegenTheme, }) {
1069
- terminal.releasePersistGate ??= acquireChatPersistGate(vctx.connections, req.chatId);
1070
- const endSeq = blockSeq++;
1071
- const rEnd = await appendBlockEvent({
1072
- ctx,
1073
- vctx,
1074
- req,
1075
- promptId,
1076
- blockSeq: endSeq,
1077
- evt: { type: "prompt.block-end", streamId: promptId, chatId: req.chatId, seq: endSeq, timestamp: new Date() },
1078
- emitMode: "emit-only",
1079
- });
1080
- if (rEnd.isErr()) {
1081
- return Result.Err(rEnd);
1082
- }
1083
- terminal.promptBlockEndEmitted = true;
1084
- const revealSeq = blockSeq++;
1085
- const r = await handlePromptContext({
1086
- vctx,
1087
- req,
1088
- promptId,
1089
- resChat,
1090
- value,
1091
- blockSeq,
1092
- collectedMsgs,
1093
- fileSystem,
1094
- model,
1095
- codegenTheme,
1096
- onFsRef: async (fsRefVal) => {
1097
- const rReveal = await appendBlockEvent({
1098
- ctx,
1099
- vctx,
1100
- req,
1101
- promptId,
1102
- blockSeq: revealSeq,
1103
- evt: {
1104
- type: "prompt.fs-persisted",
1105
- streamId: promptId,
1106
- chatId: req.chatId,
1107
- seq: revealSeq,
1108
- timestamp: new Date(),
1109
- fsRef: fsRefVal,
1110
- },
1111
- emitMode: "emit-only",
1112
- });
1113
- if (rReveal.isErr()) {
1114
- vctx.logger.Warn().Err(rReveal).Str("promptId", promptId).Msg("reveal-emit-failed");
1115
- }
1116
- },
1117
- });
1118
- if (r.isErr()) {
1119
- return Result.Err(r);
1120
- }
1121
- blockSeq = r.Ok().blockSeq;
1122
- const rEvt = await appendBlockEvent({
1123
- ctx,
1124
- vctx,
1125
- req,
1126
- promptId,
1127
- blockSeq: blockSeq++,
1128
- evt: { ...value, fsRef: r.Ok().fsRef.toValue() },
1129
- emitMode: "emit-only",
1130
- });
1131
- for (const conn of vctx.connections) {
1132
- const chatCtx = conn.chatIds.get(req.chatId);
1133
- const promptIdCtx = chatCtx?.promptIds.get(promptId);
1134
- if (promptIdCtx && chatCtx) {
1135
- chatCtx.promptIds.delete(promptId);
1136
- }
1137
- }
1138
- if (rEvt.isErr()) {
1139
- return Result.Err(rEvt);
1140
- }
1141
- return Result.Ok(blockSeq);
1142
- }
1143
- async function handleLlmResponse({ scope, vctx, req, ctx, res, resChat, promptId, blockSeq, llmReq, abort, terminal, codegenTheme, }) {
1144
- await scope
1145
- .evalResult(async () => {
1146
- let collectedMsgs;
1147
- for (const conn of vctx.connections) {
1148
- const tChatCtx = conn.chatIds.get(req.chatId);
1149
- if (tChatCtx) {
1150
- const promptIdCtx = tChatCtx.promptIds.get(promptId);
1151
- if (!promptIdCtx) {
1152
- collectedMsgs = [];
1153
- tChatCtx.promptIds.set(promptId, {
1154
- blocks: collectedMsgs,
1155
- promptId,
1156
- type: "vibes.diy.section-event",
1157
- chatId: req.chatId,
1158
- blockSeq: 0,
1159
- timestamp: new Date(),
1160
- });
1161
- }
1162
- else {
1163
- collectedMsgs = promptIdCtx.blocks;
1164
- }
1165
- }
1166
- }
1167
- if (!collectedMsgs) {
1168
- collectedMsgs = [];
1169
- }
1170
- const seedTimelineResult = await loadVersionTimeline(vctx, req.chatId);
1171
- if (seedTimelineResult.isErr())
1172
- return Result.Err(seedTimelineResult);
1173
- const seedTimeline = seedTimelineResult.Ok();
1174
- const seedForResolver = seedTimeline.length > 0 ? seedTimeline[seedTimeline.length - 1].vfs : new Map();
1175
- const resolverLogger = ensureLogger(vctx.sthis, "streamingResolver");
1176
- const recoveryLogger = ensureLogger(vctx.sthis, "applyRecovery");
1177
- const streamingResolver = createStreamingResolver({
1178
- chatId: req.chatId,
1179
- promptId,
1180
- seed: seedForResolver,
1181
- onApplyError: (evt) => logApplyError(resolverLogger, evt),
1182
- });
1183
- const blockAcc = createBlockAccumulator();
1184
- let recoveryCounter = { consecutiveFruitless: 0 };
1185
- let isRecoveryStream = false;
1186
- let turnRawText = "";
1187
- let rawEmitted = false;
1188
- const emitTurnRaw = async () => {
1189
- if (rawEmitted || turnRawText === "")
1190
- return Result.Ok();
1191
- rawEmitted = true;
1192
- const r = await appendBlockEvent({
1193
- ctx,
1194
- vctx,
1195
- req,
1196
- promptId,
1197
- blockSeq,
1198
- evt: {
1199
- type: "prompt.raw",
1200
- streamId: promptId,
1201
- chatId: req.chatId,
1202
- seq: blockSeq,
1203
- text: turnRawText,
1204
- timestamp: new Date(),
1205
- },
1206
- resChat,
1207
- });
1208
- blockSeq++;
1209
- return r;
1210
- };
1211
- let currentRes = res;
1212
- let currentAbort = abort;
1213
- const HEARTBEAT_MS = 20_000;
1214
- while (true) {
1215
- let streamMadeProgress = false;
1216
- const partialBuffer = { text: "", safeCut: 0, lastReplaceFileLines: undefined };
1217
- const captureDeltas = new TransformStream({
1218
- transform(msg, controller) {
1219
- if (isDeltaLine(msg))
1220
- partialBuffer.text += msg.content;
1221
- controller.enqueue(msg);
1222
- },
1223
- });
1224
- const pipeline = currentRes
1225
- .body.pipeThrough(createStatsCollector(promptId, 1000))
1226
- .pipeThrough(createLineStream(promptId))
1227
- .pipeThrough(createDataStream(promptId))
1228
- .pipeThrough(createSseStream(promptId))
1229
- .pipeThrough(createDeltaStream(promptId, () => vctx.sthis.nextId(12).str))
1230
- .pipeThrough(captureDeltas)
1231
- .pipeThrough(createSectionsStream(promptId, () => vctx.sthis.nextId(12).str));
1232
- const reader = pipeline.getReader();
1233
- let recoverHint = null;
1234
- let drainOnly = false;
1235
- readLoop: while (true) {
1236
- const rRead = await exception2Result(() => withHeartbeat(() => reader.read(), {
1237
- intervalMs: HEARTBEAT_MS,
1238
- beat: () => {
1239
- ctx.send
1240
- .send(ctx, { type: "vibes.diy.res-progress", stage: "generating" })
1241
- .catch((e) => {
1242
- recoveryLogger
1243
- .Warn()
1244
- .Any("event", { chatId: req.chatId, promptId, err: String(e) })
1245
- .Msg("heartbeat-emit-failed");
1246
- });
1247
- },
1248
- }));
1249
- if (rRead.isErr()) {
1250
- const intentional = drainOnly || currentAbort.signal.aborted;
1251
- if (!intentional) {
1252
- recoveryLogger
1253
- .Info()
1254
- .Any("event", {
1255
- chatId: req.chatId,
1256
- promptId,
1257
- err: String(rRead.Err()),
1258
- })
1259
- .Msg("upstream-read-failed");
1260
- }
1261
- break readLoop;
1262
- }
1263
- const { done, value } = rRead.Ok();
1264
- if (done)
1265
- break readLoop;
1266
- if (drainOnly)
1267
- continue;
1268
- if (!isBlockEnd(value)) {
1269
- if (!isBlockStreamMsg(value))
1270
- continue;
1271
- const closed = blockAcc.ingest(value);
1272
- const applyResult = closed ? streamingResolver.observeBlock(closed) : undefined;
1273
- const isFailedCodeEnd = closed !== undefined && applyResult !== undefined && applyResult.errors.length > 0;
1274
- if (isFailedCodeEnd) {
1275
- const first = applyResult.errors[0];
1276
- const truncateEvt = buildTruncatedEvent({
1277
- closed,
1278
- firstError: first,
1279
- errorCount: applyResult.errors.length,
1280
- promptId,
1281
- blockSeq,
1282
- now: new Date(),
1283
- });
1284
- collectedMsgs.push(truncateEvt);
1285
- const r = await appendBlockEvent({
1286
- ctx,
1287
- vctx,
1288
- req,
1289
- promptId,
1290
- blockSeq: blockSeq++,
1291
- evt: truncateEvt,
1292
- emitMode: "emit-only",
1293
- });
1294
- if (r.isErr()) {
1295
- return Result.Err(r);
1296
- }
1297
- recoverHint = {
1298
- partial: partialBuffer.text.slice(0, partialBuffer.safeCut),
1299
- focusPath: applyResult.path,
1300
- blockId: closed.end.blockId,
1301
- sectionId: closed.end.sectionId,
1302
- reason: first.reason,
1303
- kind: first.kind,
1304
- errorCount: applyResult.errors.length,
1305
- lastReplaceFileLines: partialBuffer.lastReplaceFileLines,
1306
- };
1307
- currentAbort.abort();
1308
- drainOnly = true;
1309
- continue;
1310
- }
1311
- collectedMsgs.push(value);
1312
- const r = await appendBlockEvent({
1313
- ctx,
1314
- vctx,
1315
- req,
1316
- promptId,
1317
- blockSeq: blockSeq++,
1318
- evt: value,
1319
- emitMode: "emit-only",
1320
- resChat,
1321
- });
1322
- if (r.isErr()) {
1323
- return Result.Err(r);
1324
- }
1325
- if (closed !== undefined && applyResult !== undefined && applyResult.errors.length === 0) {
1326
- partialBuffer.safeCut = partialBuffer.text.length;
1327
- streamMadeProgress = true;
1328
- const wasReplace = closed.lines.some((l) => l.line.startsWith("<<<<<<< SEARCH"));
1329
- if (wasReplace) {
1330
- const resolvedText = streamingResolver.getVfs().get(applyResult.path);
1331
- partialBuffer.lastReplaceFileLines = resolvedText ? resolvedText.split("\n").length : undefined;
1332
- }
1333
- else {
1334
- partialBuffer.lastReplaceFileLines = undefined;
1335
- }
1336
- }
1337
- }
1338
- else {
1339
- collectedMsgs.push(value);
1340
- const x = await handleEndMsg({
1341
- collectedMsgs,
1342
- vctx,
1343
- req,
1344
- ctx,
1345
- resChat,
1346
- promptId,
1347
- value,
1348
- blockSeq,
1349
- terminal,
1350
- model: llmReq.model,
1351
- codegenTheme,
1352
- });
1353
- if (x.isErr())
1354
- return Result.Err(x);
1355
- blockSeq = x.Ok();
1356
- collectedMsgs.splice(0, collectedMsgs.length);
1357
- }
1358
- }
1359
- turnRawText += recoverHint === null ? partialBuffer.text : recoverHint.partial;
1360
- if (isRecoveryStream) {
1361
- recoveryCounter = updateRecoveryCounter(recoveryCounter, { madeProgress: streamMadeProgress });
1362
- recoveryLogger
1363
- .Debug()
1364
- .Any("event", {
1365
- chatId: req.chatId,
1366
- promptId,
1367
- madeProgress: streamMadeProgress,
1368
- consecutiveFruitless: recoveryCounter.consecutiveFruitless,
1369
- })
1370
- .Msg("recovery-stream-end");
1371
- }
1372
- if (recoverHint === null) {
1373
- const rRaw = await emitTurnRaw();
1374
- if (rRaw.isErr())
1375
- return Result.Err(rRaw);
1376
- return Result.Ok();
1377
- }
1378
- if (!shouldAttemptRecovery(recoveryCounter)) {
1379
- recoveryLogger
1380
- .Info()
1381
- .Any("event", {
1382
- chatId: req.chatId,
1383
- promptId,
1384
- blockId: recoverHint.blockId,
1385
- consecutiveFruitless: recoveryCounter.consecutiveFruitless,
1386
- })
1387
- .Msg("recovery-exhausted");
1388
- const rRaw = await emitTurnRaw();
1389
- if (rRaw.isErr())
1390
- return Result.Err(rRaw);
1391
- const exhaustedEnd = {
1392
- type: "block.end",
1393
- blockId: recoverHint.blockId,
1394
- streamId: promptId,
1395
- seq: blockSeq,
1396
- blockNr: 0,
1397
- timestamp: new Date(),
1398
- stats: {
1399
- toplevel: { lines: 0, bytes: 0 },
1400
- code: { lines: 0, bytes: 0 },
1401
- image: { lines: 0, bytes: 0 },
1402
- total: { lines: 0, bytes: 0 },
1403
- },
1404
- usage: {
1405
- given: [],
1406
- calculated: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
1407
- },
1408
- };
1409
- collectedMsgs.push(exhaustedEnd);
1410
- const xEnd = await handleEndMsg({
1411
- collectedMsgs,
1412
- vctx,
1413
- req,
1414
- ctx,
1415
- resChat,
1416
- promptId,
1417
- value: exhaustedEnd,
1418
- blockSeq,
1419
- terminal,
1420
- model: llmReq.model,
1421
- codegenTheme,
1422
- });
1423
- if (xEnd.isErr())
1424
- return Result.Err(xEnd);
1425
- return Result.Ok();
1426
- }
1427
- const stitchMode = recoveryCounter.consecutiveFruitless === 2;
1428
- recoveryLogger
1429
- .Debug()
1430
- .Any("event", {
1431
- chatId: req.chatId,
1432
- promptId,
1433
- blockId: recoverHint.blockId,
1434
- sectionId: recoverHint.sectionId,
1435
- path: recoverHint.focusPath,
1436
- reason: recoverHint.reason,
1437
- kind: recoverHint.kind,
1438
- errorCount: recoverHint.errorCount,
1439
- consecutiveFruitless: recoveryCounter.consecutiveFruitless,
1440
- mode: stitchMode ? "stitch" : "continue",
1441
- })
1442
- .Msg("recovery-start");
1443
- const pkgBaseUrl = promptsPkgBaseUrl(vctx.params.pkgRepos.workspace);
1444
- const fetchOverride = createPromptAssetFetch({ fetchAsset: vctx.fetchAsset });
1445
- const addendum = await exception2Result(() => stitchMode ? getRecoveryStitchAddendum(pkgBaseUrl, fetchOverride) : getRecoveryAddendum(pkgBaseUrl, fetchOverride));
1446
- if (addendum.isErr()) {
1447
- recoveryLogger
1448
- .Info()
1449
- .Any("event", { chatId: req.chatId, promptId, err: String(addendum.Err()) })
1450
- .Msg("recovery-addendum-failed");
1451
- return Result.Ok();
1452
- }
1453
- const recReq = buildRecoveryRequest({
1454
- originalRequest: llmReq,
1455
- recoveryAddendum: addendum.Ok(),
1456
- vfs: streamingResolver.getVfs(),
1457
- focusPath: recoverHint.focusPath,
1458
- assistantPartial: stitchMode ? undefined : recoverHint.partial,
1459
- lastReplaceFileLines: stitchMode ? undefined : recoverHint.lastReplaceFileLines,
1460
- });
1461
- if (recReq.isErr()) {
1462
- recoveryLogger
1463
- .Info()
1464
- .Any("event", {
1465
- chatId: req.chatId,
1466
- promptId,
1467
- err: String(recReq.Err()),
1468
- originalMessageCount: llmReq.messages.length,
1469
- originalRoles: llmReq.messages.map((m) => m.role),
1470
- })
1471
- .Msg("recovery-build-failed");
1472
- return Result.Ok();
1473
- }
1474
- const recPayload = recReq.Ok();
1475
- const recMessageCount = recPayload.messages.length;
1476
- const recRoles = recPayload.messages.map((m) => m.role);
1477
- const recLlmReq = {
1478
- ...recPayload,
1479
- headers: llmReq.headers,
1480
- model: llmReq.model,
1481
- };
1482
- const recDispatch = await dispatchLlmRequestWithFallback({
1483
- vctx,
1484
- mode: req.mode,
1485
- llmReq: recLlmReq,
1486
- chatId: req.chatId,
1487
- promptId,
1488
- phase: "recovery",
1489
- });
1490
- const recModel = recDispatch.llmReq.model;
1491
- if (recDispatch.ok === false) {
1492
- recoveryLogger
1493
- .Info()
1494
- .Any("event", {
1495
- chatId: req.chatId,
1496
- promptId,
1497
- err: recDispatch.error,
1498
- model: recModel,
1499
- messageCount: recMessageCount,
1500
- roles: recRoles,
1501
- })
1502
- .Msg("recovery-call-failed");
1503
- return Result.Ok();
1504
- }
1505
- const nextRes = recDispatch.res;
1506
- recoveryLogger
1507
- .Debug()
1508
- .Any("event", {
1509
- chatId: req.chatId,
1510
- promptId,
1511
- partialBytes: recoverHint.partial.length,
1512
- focusPath: recoverHint.focusPath,
1513
- model: recModel,
1514
- messageCount: recMessageCount,
1515
- roles: recRoles,
1516
- })
1517
- .Msg("recovery-call-started");
1518
- currentRes = nextRes;
1519
- currentAbort = recDispatch.abort;
1520
- isRecoveryStream = true;
1521
- }
1522
- })
1523
- .do();
1524
- return blockSeq;
1525
- }
1526
- export async function handleFSPrompt({ scope, vctx, resChat, req, ctx, promptId, terminal, }) {
1527
- let fileSystem;
1528
- if (isReqPromptFSSetChatSection(req)) {
1529
- fileSystem = req.fsSet;
1530
- }
1531
- else if (isReqPromptFSUpdateChatSection(req)) {
1532
- let baseItems;
1533
- if (req.refFsId) {
1534
- const refApp = await vctx.sql.db
1535
- .select({ fileSystem: vctx.sql.tables.apps.fileSystem })
1536
- .from(vctx.sql.tables.apps)
1537
- .where(and(eq(vctx.sql.tables.apps.fsId, req.refFsId), eq(vctx.sql.tables.apps.ownerHandle, resChat.ownerHandle), eq(vctx.sql.tables.apps.appSlug, resChat.appSlug)))
1538
- .orderBy(desc(vctx.sql.tables.apps.releaseSeq))
1539
- .limit(1)
1540
- .then((r) => r[0]);
1541
- if (!refApp) {
1542
- throw new Error(`fs-update refFsId ${req.refFsId} is not a release of ${resChat.ownerHandle}/${resChat.appSlug}`);
1543
- }
1544
- baseItems = refApp.fileSystem;
1545
- }
1546
- else {
1547
- const lastestPrompt = await vctx.sql.db
1548
- .select()
1549
- .from(vctx.sql.tables.promptContexts)
1550
- .innerJoin(vctx.sql.tables.apps, eq(vctx.sql.tables.apps.fsId, vctx.sql.tables.promptContexts.fsId))
1551
- .where(eq(vctx.sql.tables.promptContexts.chatId, req.chatId))
1552
- .orderBy(desc(vctx.sql.tables.promptContexts.created))
1553
- .limit(1)
1554
- .then((r) => r[0]);
1555
- baseItems = lastestPrompt?.Apps.fileSystem;
1556
- }
1557
- const refFS = [];
1558
- if (baseItems !== undefined) {
1559
- const vfs = await resolveVfsFromFileSystem(vctx, parseArray(baseItems, fileSystemItem));
1560
- for (const [filename, content] of vfs) {
1561
- if (filename.startsWith("/~~calculated~~/"))
1562
- continue;
1563
- refFS.push({ type: "code-block", filename, lang: langForFilename(filename), content });
1564
- }
1565
- }
1566
- const mapFS = refFS.reduce((acc, file) => {
1567
- acc.set(file.filename, file);
1568
- return acc;
1569
- }, new Map());
1570
- req.fsUpdate.update.forEach((update) => {
1571
- mapFS.set(update.filename, update);
1572
- });
1573
- req.fsUpdate.remove?.forEach((item) => {
1574
- mapFS.delete(item.filename);
1575
- });
1576
- fileSystem = Array.from(mapFS.values());
1577
- }
1578
- const rEndMsg = await scope
1579
- .evalResult(async () => {
1580
- const sectionId = vctx.sthis.nextId(12).str;
1581
- let blockNr = 0;
1582
- const blockId = vctx.sthis.nextId(12).str;
1583
- let blockSeq = 0;
1584
- let bytes = 0;
1585
- const collectedMsgs = [
1586
- {
1587
- type: "block.begin",
1588
- blockId,
1589
- streamId: promptId,
1590
- seq: blockSeq++,
1591
- blockNr: blockNr++,
1592
- timestamp: new Date(),
1593
- },
1594
- ...fileSystem.flatMap((file) => {
1595
- if (!isVibeCodeBlock(file)) {
1596
- return [];
1597
- }
1598
- return [
1599
- {
1600
- type: "block.code.begin",
1601
- sectionId,
1602
- lang: file.lang,
1603
- blockId: blockId,
1604
- streamId: promptId,
1605
- seq: blockSeq++,
1606
- blockNr,
1607
- timestamp: new Date(),
1608
- },
1609
- ...file.content.split("\n").map((line, lineNr) => {
1610
- bytes += line.length + 1;
1611
- return {
1612
- type: "block.code.line",
1613
- sectionId,
1614
- blockId,
1615
- line,
1616
- streamId: promptId,
1617
- seq: blockSeq++,
1618
- timestamp: new Date(),
1619
- lang: file.lang,
1620
- blockNr,
1621
- lineNr,
1622
- };
1623
- }),
1624
- {
1625
- type: "block.code.end",
1626
- sectionId,
1627
- blockId,
1628
- streamId: promptId,
1629
- seq: blockSeq++,
1630
- timestamp: new Date(),
1631
- lang: file.lang,
1632
- blockNr,
1633
- stats: { lines: file.content.split("\n").length, bytes },
1634
- },
1635
- ];
1636
- }),
1637
- ];
1638
- const value = {
1639
- type: "block.end",
1640
- stats: {
1641
- toplevel: { lines: 0, bytes: 0 },
1642
- code: { lines: blockSeq, bytes },
1643
- image: { lines: 0, bytes: 0 },
1644
- total: { lines: blockSeq, bytes },
1645
- },
1646
- usage: {
1647
- given: [],
1648
- calculated: {
1649
- prompt_tokens: 0,
1650
- completion_tokens: 0,
1651
- total_tokens: 0,
1652
- },
1653
- },
1654
- blockId: blockId,
1655
- streamId: promptId,
1656
- seq: blockSeq++,
1657
- blockNr: blockNr,
1658
- timestamp: new Date(),
1659
- };
1660
- collectedMsgs.push(value);
1661
- for (const msg of collectedMsgs) {
1662
- if (!isBlockEnd(msg)) {
1663
- await appendBlockEvent({
1664
- ctx,
1665
- vctx,
1666
- req,
1667
- promptId,
1668
- blockSeq: blockSeq++,
1669
- evt: msg,
1670
- emitMode: "emit-only",
1671
- });
1672
- }
1673
- }
1674
- return handleEndMsg({
1675
- collectedMsgs,
1676
- vctx,
1677
- req,
1678
- ctx,
1679
- resChat,
1680
- promptId,
1681
- value,
1682
- blockSeq,
1683
- fileSystem,
1684
- terminal,
1685
- });
1686
- })
1687
- .do();
1688
- return Result.Ok(rEndMsg);
1689
- }
1690
92
  export const promptChatSection = {
1691
93
  hash: "prompt-chat-section-handler",
1692
94
  validate: unwrapMsgBase(async (msg) => {
@@ -1756,6 +158,7 @@ export const promptChatSection = {
1756
158
  }),
1757
159
  focusPath: orig.focusPath,
1758
160
  attachScreenshot: orig.dryRun !== true,
161
+ degradeAttachments: orig.dryRun === true,
1759
162
  ...(activeSettingsOverride ? { activeSettingsOverride } : {}),
1760
163
  });
1761
164
  if (rAssembled.isErr())