@tangle-network/agent-app 0.34.0 → 0.36.0

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.
@@ -0,0 +1,293 @@
1
+ // src/studio/generation.ts
2
+ var GENERATION_TYPES = ["image", "video", "avatar", "speech", "transcription"];
3
+ function isGenerationType(value) {
4
+ return GENERATION_TYPES.includes(value);
5
+ }
6
+ var DESTINATIONS = [
7
+ { id: "instagram", label: "Instagram", providerIds: ["instagram"], fields: "Caption, hashtags, crop" },
8
+ { id: "tiktok", label: "TikTok", providerIds: ["tiktok"], fields: "Caption, audio note, vertical video" },
9
+ { id: "youtube-shorts", label: "YouTube Shorts", providerIds: ["youtube"], fields: "Title, description, vertical video" },
10
+ { id: "linkedin", label: "LinkedIn", providerIds: ["linkedin"], fields: "Caption, link, creator/company page" },
11
+ { id: "x", label: "X", providerIds: ["twitter"], fields: "Short copy, mentions, thread option" }
12
+ ];
13
+ var CADENCES = ["Manual approval", "Publish now", "Daily creative drop", "Weekly series"];
14
+ var MIN_IMAGE_COUNT = 1;
15
+ var MAX_IMAGE_COUNT = 4;
16
+ function relativeTime(date) {
17
+ if (!date) return "";
18
+ const now = Date.now();
19
+ const diff = now - new Date(date).getTime();
20
+ const minutes = Math.floor(diff / 6e4);
21
+ if (minutes < 1) return "just now";
22
+ if (minutes < 60) return `${minutes}m ago`;
23
+ const hours = Math.floor(minutes / 60);
24
+ if (hours < 24) return `${hours}h ago`;
25
+ const days = Math.floor(hours / 24);
26
+ if (days < 7) return `${days}d ago`;
27
+ return new Date(date).toLocaleDateString("en-US", { month: "short", day: "numeric" });
28
+ }
29
+ function outputPathFor(type) {
30
+ if (type === "image") return "generated/images";
31
+ if (type === "video") return "generated/videos";
32
+ if (type === "avatar") return "generated/avatars";
33
+ if (type === "speech") return "generated/audio";
34
+ return "generated/transcripts";
35
+ }
36
+ function generationVaultPath(generation) {
37
+ const value = generation.metadata?.vaultPath;
38
+ return typeof value === "string" && value.trim() ? value.trim() : null;
39
+ }
40
+ function buildPublishPackage({
41
+ caption,
42
+ postDescription,
43
+ mentions,
44
+ cadence,
45
+ destinations
46
+ }) {
47
+ const trimmedCaption = caption.trim();
48
+ const trimmedDescription = postDescription.trim();
49
+ const parsedMentions = mentions.split(",").map((item) => item.trim()).filter(Boolean);
50
+ const selectedDestinations = destinations.filter(Boolean);
51
+ const trimmedCadence = cadence.trim() || "Manual approval";
52
+ const hasPublishContent = Boolean(
53
+ trimmedCaption || trimmedDescription || parsedMentions.length > 0 || selectedDestinations.length > 0 || trimmedCadence !== "Manual approval"
54
+ );
55
+ if (!hasPublishContent) return null;
56
+ return {
57
+ caption: trimmedCaption,
58
+ description: trimmedDescription,
59
+ mentions: parsedMentions,
60
+ destinations: selectedDestinations,
61
+ cadence: trimmedCadence,
62
+ workflowDraft: trimmedCadence !== "Manual approval",
63
+ evalContract: {
64
+ artifactType: "publish_package",
65
+ deterministicChecks: ["has_asset", "has_destination", "has_caption_or_description", "has_cadence"]
66
+ }
67
+ };
68
+ }
69
+ function isPublishPackage(value) {
70
+ if (!value || typeof value !== "object") return false;
71
+ const publishPackage = value;
72
+ const destinations = Array.isArray(publishPackage.destinations) ? publishPackage.destinations.filter(Boolean) : [];
73
+ const mentions = Array.isArray(publishPackage.mentions) ? publishPackage.mentions.filter(Boolean) : [];
74
+ return Boolean(
75
+ destinations.length > 0 || mentions.length > 0 || typeof publishPackage.caption === "string" && publishPackage.caption.trim() || typeof publishPackage.description === "string" && publishPackage.description.trim() || typeof publishPackage.cadence === "string" && publishPackage.cadence.trim() && publishPackage.cadence.trim() !== "Manual approval"
76
+ );
77
+ }
78
+ function isDestinationConnected(destination, connections) {
79
+ return connections.some((connection) => connection.status === "connected" && (destination.providerIds.includes(connection.providerId) || destination.providerIds.includes(connection.connectorId)));
80
+ }
81
+ function selectedModelsWithDefaults(current, catalog) {
82
+ const next = { ...current };
83
+ for (const key of GENERATION_TYPES) {
84
+ const models = catalog.models[key] ?? [];
85
+ const currentOption = models.find((model) => model.id === next[key]);
86
+ if (!next[key] || !currentOption || currentOption.status === "unavailable") {
87
+ next[key] = preferredModelId(key, catalog) ?? "";
88
+ }
89
+ }
90
+ return next;
91
+ }
92
+ function preferredModelId(type, catalog) {
93
+ if (!catalog) return void 0;
94
+ const models = catalog.models[type] ?? [];
95
+ const preferred = catalog.defaults[type];
96
+ return models.find((model) => model.id === preferred)?.id ?? models.find((model) => model.status !== "unavailable")?.id ?? models[0]?.id;
97
+ }
98
+ function modelMessage(model, loading, count) {
99
+ if (loading) return "Loading media models...";
100
+ if (count === 0) return "No models are available for this media type.";
101
+ if (!model) return "Select a model.";
102
+ if (model.status === "unavailable") return model.reason ?? "This model is not configured.";
103
+ if (model.status === "limited") return model.reason ? `Limited: ${model.reason}` : "Limited availability.";
104
+ return null;
105
+ }
106
+ function buildGenerationRequestBody(fields) {
107
+ const body = {
108
+ workspaceId: fields.workspaceId,
109
+ clientRequestId: fields.clientRequestId,
110
+ type: fields.type,
111
+ model: fields.model,
112
+ prompt: fields.prompt.trim(),
113
+ negativePrompt: fields.negativePrompt.trim() || void 0,
114
+ outputPath: fields.outputPath.trim() || void 0
115
+ };
116
+ if (fields.publishPackage) body.publishPackage = fields.publishPackage;
117
+ if (fields.type === "image") Object.assign(body, {
118
+ size: fields.image.size,
119
+ quality: fields.image.quality,
120
+ n: fields.image.count
121
+ });
122
+ if (fields.type === "video") {
123
+ const duration = Number(fields.video.duration);
124
+ Object.assign(body, {
125
+ // omit (let the API default) rather than serialize NaN → null on bad input
126
+ duration: Number.isFinite(duration) ? duration : void 0,
127
+ resolution: fields.video.resolution,
128
+ aspectRatio: fields.video.aspectRatio.trim() || void 0,
129
+ referenceImageUrl: fields.video.referenceImageUrl.trim() || void 0
130
+ });
131
+ }
132
+ if (fields.type === "speech") Object.assign(body, { voice: fields.speech.voice });
133
+ if (fields.type === "avatar") Object.assign(body, {
134
+ audioUrl: fields.avatar.audioUrl.trim(),
135
+ imageUrl: fields.avatar.imageUrl.trim() || void 0,
136
+ avatarId: fields.avatar.avatarId.trim() || void 0
137
+ });
138
+ if (fields.type === "transcription") {
139
+ const temperature = Number(fields.transcription.temperature);
140
+ Object.assign(body, {
141
+ audioUrl: fields.transcription.audioUrl.trim(),
142
+ language: fields.transcription.language.trim() || void 0,
143
+ responseFormat: fields.transcription.responseFormat,
144
+ // omit (let the API default) rather than serialize NaN → null on bad input
145
+ temperature: Number.isFinite(temperature) ? temperature : void 0
146
+ });
147
+ }
148
+ return body;
149
+ }
150
+ function generationStatus(generation) {
151
+ const metadata = generation.metadata ?? {};
152
+ const status = typeof metadata.generationStatus === "string" ? metadata.generationStatus : "";
153
+ if (status === "pending" || status === "running" || status === "failed" || status === "succeeded") return status;
154
+ return generation.result ? "succeeded" : "pending";
155
+ }
156
+ function generationError(generation) {
157
+ const metadata = generation.metadata ?? {};
158
+ if (typeof metadata.providerError === "string" && metadata.providerError.trim()) {
159
+ return userSafeGenerationMessage(metadata.providerError);
160
+ }
161
+ if (typeof metadata.storageError === "string" && metadata.storageError.trim()) {
162
+ return metadata.storageError;
163
+ }
164
+ return null;
165
+ }
166
+ function generationClientRequestId(generation) {
167
+ const metadata = generation.metadata ?? {};
168
+ return typeof metadata.clientRequestId === "string" && metadata.clientRequestId.trim() ? metadata.clientRequestId : null;
169
+ }
170
+ function generationBatchSlotKey(generation) {
171
+ const metadata = generation.metadata ?? {};
172
+ const batchId = typeof metadata.batchId === "string" && metadata.batchId.trim() ? metadata.batchId : null;
173
+ return batchId && typeof metadata.outputIndex === "number" ? `${batchId}:${metadata.outputIndex}` : null;
174
+ }
175
+ function generationMergeKey(generation) {
176
+ return generationBatchSlotKey(generation) ?? generationClientRequestId(generation);
177
+ }
178
+ function mergeLiveGeneration(current, generation) {
179
+ const mergeKey = generationMergeKey(generation);
180
+ const existingIndex = current.findIndex((item) => item.id === generation.id || mergeKey && generationMergeKey(item) === mergeKey);
181
+ if (existingIndex === -1) return [generation, ...current];
182
+ const next = [...current];
183
+ next[existingIndex] = generation;
184
+ return next;
185
+ }
186
+ function mergeLoaderAndLive(loader, live) {
187
+ if (live.length === 0) return loader;
188
+ const leading = live.map((generation) => {
189
+ const mergeKey = generationMergeKey(generation);
190
+ return mergeKey ? loader.find((gen) => generationMergeKey(gen) === mergeKey) ?? generation : loader.find((gen) => gen.id === generation.id) ?? generation;
191
+ });
192
+ const leadingIds = new Set(leading.map((gen) => gen.id));
193
+ const leadingMergeKeys = new Set(leading.map((gen) => generationMergeKey(gen)).filter((id) => Boolean(id)));
194
+ return [
195
+ ...leading,
196
+ ...loader.filter((gen) => !leadingIds.has(gen.id) && !leadingMergeKeys.has(generationMergeKey(gen) ?? ""))
197
+ ];
198
+ }
199
+ function isLocalGeneration(generation) {
200
+ return generation.id.startsWith("local-");
201
+ }
202
+ function generationOutputIndex(generation) {
203
+ const value = generation.metadata?.outputIndex;
204
+ return typeof value === "number" ? value : 0;
205
+ }
206
+ function latestBatchOf(generations) {
207
+ const first = generations[0];
208
+ if (!first) return [];
209
+ const key = generationClientRequestId(first);
210
+ const batch = key ? generations.filter((generation) => generationClientRequestId(generation) === key) : [first];
211
+ return [...batch].sort((a, b) => generationOutputIndex(a) - generationOutputIndex(b));
212
+ }
213
+ function userSafeGenerationMessage(message) {
214
+ if (!message) return "Generation failed";
215
+ if (/Tangle API key is invalid or expired/i.test(message)) return message;
216
+ if (/(api[_ -]?key|secret|token|credential|env|configured|configuration)/i.test(message)) {
217
+ return "Generation failed";
218
+ }
219
+ return message;
220
+ }
221
+ function optimisticGeneration({
222
+ type,
223
+ prompt,
224
+ model,
225
+ clientRequestId,
226
+ outputIndex,
227
+ outputCount
228
+ }) {
229
+ const batchId = outputIndex == null ? void 0 : clientRequestId;
230
+ return {
231
+ id: outputIndex == null ? `local-${clientRequestId}` : `local-${clientRequestId}-${outputIndex}`,
232
+ type,
233
+ prompt,
234
+ result: null,
235
+ model: model ?? null,
236
+ cost: null,
237
+ createdAt: /* @__PURE__ */ new Date(),
238
+ metadata: {
239
+ generationStatus: "pending",
240
+ provider: type,
241
+ clientRequestId,
242
+ batchId,
243
+ outputIndex,
244
+ outputCount
245
+ }
246
+ };
247
+ }
248
+ function failedOptimisticGeneration(generation) {
249
+ return {
250
+ ...generation,
251
+ metadata: {
252
+ ...generation.metadata ?? {},
253
+ generationStatus: "failed",
254
+ providerError: "Generation failed"
255
+ }
256
+ };
257
+ }
258
+ function normalizeImageCount(value) {
259
+ const numeric = typeof value === "number" ? value : Number(value);
260
+ if (!Number.isFinite(numeric)) return MIN_IMAGE_COUNT;
261
+ return Math.min(Math.max(Math.trunc(numeric), MIN_IMAGE_COUNT), MAX_IMAGE_COUNT);
262
+ }
263
+
264
+ export {
265
+ GENERATION_TYPES,
266
+ isGenerationType,
267
+ DESTINATIONS,
268
+ CADENCES,
269
+ MIN_IMAGE_COUNT,
270
+ MAX_IMAGE_COUNT,
271
+ relativeTime,
272
+ outputPathFor,
273
+ generationVaultPath,
274
+ buildPublishPackage,
275
+ isPublishPackage,
276
+ isDestinationConnected,
277
+ selectedModelsWithDefaults,
278
+ preferredModelId,
279
+ modelMessage,
280
+ buildGenerationRequestBody,
281
+ generationStatus,
282
+ generationError,
283
+ generationMergeKey,
284
+ mergeLiveGeneration,
285
+ mergeLoaderAndLive,
286
+ isLocalGeneration,
287
+ latestBatchOf,
288
+ userSafeGenerationMessage,
289
+ optimisticGeneration,
290
+ failedOptimisticGeneration,
291
+ normalizeImageCount
292
+ };
293
+ //# sourceMappingURL=chunk-DTS5TZRN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/studio/generation.ts"],"sourcesContent":["// Minimal structural view of a connected integration — `isDestinationConnected`\n// only reads the connection status and the provider/connector identifiers, so\n// the logic layer declares its own shape instead of depending on the\n// design-system package. A sandbox-ui `IntegrationConnection` is assignable to\n// this (it carries all three fields).\nexport interface StudioIntegrationConnection {\n status: string\n providerId: string\n connectorId: string\n}\n\nexport type GenerationType = 'image' | 'video' | 'speech' | 'avatar' | 'transcription'\n\nexport type GenerationStatus = 'pending' | 'running' | 'succeeded' | 'failed'\n\nexport type MediaModelStatus = 'available' | 'limited' | 'unavailable'\n\nexport interface Generation {\n id: string\n type: string\n prompt: string\n result: string | null\n model: string | null\n cost: number | null\n createdAt: Date | null\n metadata: Record<string, unknown> | null\n}\n\nexport interface MediaModelOption {\n id: string\n name: string\n provider?: string\n type: GenerationType\n status: MediaModelStatus\n reason?: string\n}\n\nexport interface MediaModelCatalogResponse {\n defaults: Record<GenerationType, string>\n models: Record<GenerationType, MediaModelOption[]>\n error?: string\n}\n\nexport interface PublishPackage {\n caption: string\n description: string\n mentions: string[]\n destinations: string[]\n cadence: string\n workflowDraft: boolean\n evalContract: {\n artifactType: string\n deterministicChecks: string[]\n }\n}\n\nexport interface PublishDestination {\n id: string\n label: string\n providerIds: string[]\n fields: string\n}\n\n// Order drives the type filter tabs and the composer segmented control\nexport const GENERATION_TYPES: readonly GenerationType[] = ['image', 'video', 'avatar', 'speech', 'transcription']\n\nexport function isGenerationType(value: string): value is GenerationType {\n return (GENERATION_TYPES as readonly string[]).includes(value)\n}\n\nexport const DESTINATIONS: PublishDestination[] = [\n { id: 'instagram', label: 'Instagram', providerIds: ['instagram'], fields: 'Caption, hashtags, crop' },\n { id: 'tiktok', label: 'TikTok', providerIds: ['tiktok'], fields: 'Caption, audio note, vertical video' },\n { id: 'youtube-shorts', label: 'YouTube Shorts', providerIds: ['youtube'], fields: 'Title, description, vertical video' },\n { id: 'linkedin', label: 'LinkedIn', providerIds: ['linkedin'], fields: 'Caption, link, creator/company page' },\n { id: 'x', label: 'X', providerIds: ['twitter'], fields: 'Short copy, mentions, thread option' },\n]\n\nexport const CADENCES = ['Manual approval', 'Publish now', 'Daily creative drop', 'Weekly series']\n\nexport const MIN_IMAGE_COUNT = 1\nexport const MAX_IMAGE_COUNT = 4\n\nexport function relativeTime(date: Date | null): string {\n if (!date) return ''\n const now = Date.now()\n const diff = now - new Date(date).getTime()\n const minutes = Math.floor(diff / 60000)\n if (minutes < 1) return 'just now'\n if (minutes < 60) return `${minutes}m ago`\n const hours = Math.floor(minutes / 60)\n if (hours < 24) return `${hours}h ago`\n const days = Math.floor(hours / 24)\n if (days < 7) return `${days}d ago`\n return new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n}\n\nexport function outputPathFor(type: GenerationType): string {\n if (type === 'image') return 'generated/images'\n if (type === 'video') return 'generated/videos'\n if (type === 'avatar') return 'generated/avatars'\n if (type === 'speech') return 'generated/audio'\n return 'generated/transcripts'\n}\n\nexport function generationVaultPath(generation: Generation): string | null {\n const value = generation.metadata?.vaultPath\n return typeof value === 'string' && value.trim() ? value.trim() : null\n}\n\nexport function buildPublishPackage({\n caption,\n postDescription,\n mentions,\n cadence,\n destinations,\n}: {\n caption: string\n postDescription: string\n mentions: string\n cadence: string\n destinations: string[]\n}): PublishPackage | null {\n const trimmedCaption = caption.trim()\n const trimmedDescription = postDescription.trim()\n const parsedMentions = mentions.split(',').map((item) => item.trim()).filter(Boolean)\n const selectedDestinations = destinations.filter(Boolean)\n const trimmedCadence = cadence.trim() || 'Manual approval'\n const hasPublishContent = Boolean(\n trimmedCaption\n || trimmedDescription\n || parsedMentions.length > 0\n || selectedDestinations.length > 0\n || trimmedCadence !== 'Manual approval',\n )\n if (!hasPublishContent) return null\n\n return {\n caption: trimmedCaption,\n description: trimmedDescription,\n mentions: parsedMentions,\n destinations: selectedDestinations,\n cadence: trimmedCadence,\n workflowDraft: trimmedCadence !== 'Manual approval',\n evalContract: {\n artifactType: 'publish_package',\n deterministicChecks: ['has_asset', 'has_destination', 'has_caption_or_description', 'has_cadence'],\n },\n }\n}\n\nexport function isPublishPackage(value: unknown): value is { caption?: string; description?: string; mentions?: string[]; destinations?: string[]; cadence?: string } {\n if (!value || typeof value !== 'object') return false\n const publishPackage = value as {\n caption?: unknown\n description?: unknown\n mentions?: unknown\n destinations?: unknown\n cadence?: unknown\n }\n const destinations = Array.isArray(publishPackage.destinations) ? publishPackage.destinations.filter(Boolean) : []\n const mentions = Array.isArray(publishPackage.mentions) ? publishPackage.mentions.filter(Boolean) : []\n return Boolean(\n destinations.length > 0\n || mentions.length > 0\n || (typeof publishPackage.caption === 'string' && publishPackage.caption.trim())\n || (typeof publishPackage.description === 'string' && publishPackage.description.trim())\n || (typeof publishPackage.cadence === 'string' && publishPackage.cadence.trim() && publishPackage.cadence.trim() !== 'Manual approval'),\n )\n}\n\nexport function isDestinationConnected(\n destination: PublishDestination,\n connections: StudioIntegrationConnection[],\n): boolean {\n return connections.some((connection) => connection.status === 'connected'\n && (destination.providerIds.includes(connection.providerId) || destination.providerIds.includes(connection.connectorId)))\n}\n\nexport function selectedModelsWithDefaults(\n current: Partial<Record<GenerationType, string>>,\n catalog: MediaModelCatalogResponse,\n): Partial<Record<GenerationType, string>> {\n const next = { ...current }\n for (const key of GENERATION_TYPES) {\n const models = catalog.models[key] ?? []\n const currentOption = models.find((model) => model.id === next[key])\n // Reset when: no selection, selection not in catalog, or selection is unavailable.\n // This ensures the Generate button is never stuck disabled when routeable\n // models exist but the stored default isn't one of them.\n if (!next[key] || !currentOption || currentOption.status === 'unavailable') {\n next[key] = preferredModelId(key, catalog) ?? ''\n }\n }\n return next\n}\n\nexport function preferredModelId(type: GenerationType, catalog: MediaModelCatalogResponse | null): string | undefined {\n if (!catalog) return undefined\n const models = catalog.models[type] ?? []\n const preferred = catalog.defaults[type]\n return models.find((model) => model.id === preferred)?.id\n ?? models.find((model) => model.status !== 'unavailable')?.id\n ?? models[0]?.id\n}\n\nexport function modelMessage(model: MediaModelOption | undefined, loading: boolean, count: number): string | null {\n if (loading) return 'Loading media models...'\n if (count === 0) return 'No models are available for this media type.'\n if (!model) return 'Select a model.'\n if (model.status === 'unavailable') return model.reason ?? 'This model is not configured.'\n if (model.status === 'limited') return model.reason ? `Limited: ${model.reason}` : 'Limited availability.'\n return null\n}\n\nexport interface GenerationRequestFields {\n workspaceId: string\n clientRequestId: string\n type: GenerationType\n model: string\n prompt: string\n negativePrompt: string\n outputPath: string\n publishPackage: PublishPackage | null\n image: { size: string; quality: string; count: number }\n video: { duration: string; resolution: string; aspectRatio: string; referenceImageUrl: string }\n speech: { voice: string }\n avatar: { audioUrl: string; imageUrl: string; avatarId: string }\n transcription: { audioUrl: string; language: string; responseFormat: string; temperature: string }\n}\n\n// image.count must already be normalized — it is also the optimistic-card count on the caller side\nexport function buildGenerationRequestBody(fields: GenerationRequestFields): Record<string, unknown> {\n const body: Record<string, unknown> = {\n workspaceId: fields.workspaceId,\n clientRequestId: fields.clientRequestId,\n type: fields.type,\n model: fields.model,\n prompt: fields.prompt.trim(),\n negativePrompt: fields.negativePrompt.trim() || undefined,\n outputPath: fields.outputPath.trim() || undefined,\n }\n if (fields.publishPackage) body.publishPackage = fields.publishPackage\n if (fields.type === 'image') Object.assign(body, {\n size: fields.image.size,\n quality: fields.image.quality,\n n: fields.image.count,\n })\n if (fields.type === 'video') {\n const duration = Number(fields.video.duration)\n Object.assign(body, {\n // omit (let the API default) rather than serialize NaN → null on bad input\n duration: Number.isFinite(duration) ? duration : undefined,\n resolution: fields.video.resolution,\n aspectRatio: fields.video.aspectRatio.trim() || undefined,\n referenceImageUrl: fields.video.referenceImageUrl.trim() || undefined,\n })\n }\n if (fields.type === 'speech') Object.assign(body, { voice: fields.speech.voice })\n if (fields.type === 'avatar') Object.assign(body, {\n audioUrl: fields.avatar.audioUrl.trim(),\n imageUrl: fields.avatar.imageUrl.trim() || undefined,\n avatarId: fields.avatar.avatarId.trim() || undefined,\n })\n if (fields.type === 'transcription') {\n const temperature = Number(fields.transcription.temperature)\n Object.assign(body, {\n audioUrl: fields.transcription.audioUrl.trim(),\n language: fields.transcription.language.trim() || undefined,\n responseFormat: fields.transcription.responseFormat,\n // omit (let the API default) rather than serialize NaN → null on bad input\n temperature: Number.isFinite(temperature) ? temperature : undefined,\n })\n }\n return body\n}\n\nexport function generationStatus(generation: Generation): GenerationStatus {\n const metadata = generation.metadata ?? {}\n const status = typeof metadata.generationStatus === 'string' ? metadata.generationStatus : ''\n if (status === 'pending' || status === 'running' || status === 'failed' || status === 'succeeded') return status\n return generation.result ? 'succeeded' : 'pending'\n}\n\nexport function generationError(generation: Generation): string | null {\n const metadata = generation.metadata ?? {}\n if (typeof metadata.providerError === 'string' && metadata.providerError.trim()) {\n return userSafeGenerationMessage(metadata.providerError)\n }\n if (typeof metadata.storageError === 'string' && metadata.storageError.trim()) {\n return metadata.storageError\n }\n return null\n}\n\nfunction generationClientRequestId(generation: Generation): string | null {\n const metadata = generation.metadata ?? {}\n return typeof metadata.clientRequestId === 'string' && metadata.clientRequestId.trim()\n ? metadata.clientRequestId\n : null\n}\n\nfunction generationBatchSlotKey(generation: Generation): string | null {\n const metadata = generation.metadata ?? {}\n const batchId = typeof metadata.batchId === 'string' && metadata.batchId.trim() ? metadata.batchId : null\n return batchId && typeof metadata.outputIndex === 'number'\n ? `${batchId}:${metadata.outputIndex}`\n : null\n}\n\nexport function generationMergeKey(generation: Generation): string | null {\n return generationBatchSlotKey(generation) ?? generationClientRequestId(generation)\n}\n\nexport function mergeLiveGeneration(current: Generation[], generation: Generation): Generation[] {\n const mergeKey = generationMergeKey(generation)\n const existingIndex = current.findIndex((item) => (\n item.id === generation.id\n || (mergeKey && generationMergeKey(item) === mergeKey)\n ))\n if (existingIndex === -1) return [generation, ...current]\n\n const next = [...current]\n next[existingIndex] = generation\n return next\n}\n\n// Overlay in-flight `live` generations on the loader's rows: each live row leads\n// (prefer the matching loader row by merge key / id so it carries the freshest\n// server state), then the remaining loader rows that no live row already\n// represents — deduped by BOTH id and merge key so a server row and its\n// optimistic twin never both appear. Returns `loader` unchanged when nothing is\n// live. Drives the canvas, library, and polling off one list.\nexport function mergeLoaderAndLive(loader: Generation[], live: Generation[]): Generation[] {\n if (live.length === 0) return loader\n const leading = live.map((generation) => {\n const mergeKey = generationMergeKey(generation)\n return mergeKey\n ? loader.find((gen) => generationMergeKey(gen) === mergeKey) ?? generation\n : loader.find((gen) => gen.id === generation.id) ?? generation\n })\n const leadingIds = new Set(leading.map((gen) => gen.id))\n const leadingMergeKeys = new Set(leading\n .map((gen) => generationMergeKey(gen))\n .filter((id): id is string => Boolean(id)))\n return [\n ...leading,\n ...loader.filter((gen) => (\n !leadingIds.has(gen.id)\n && !leadingMergeKeys.has(generationMergeKey(gen) ?? '')\n )),\n ]\n}\n\nexport function isLocalGeneration(generation: Generation): boolean {\n return generation.id.startsWith('local-')\n}\n\nfunction generationOutputIndex(generation: Generation): number {\n const value = generation.metadata?.outputIndex\n return typeof value === 'number' ? value : 0\n}\n\n// The most-recent run: all generations sharing the leading item's clientRequestId\n// (a multi-image batch), ordered by output slot. Falls back to the single leading\n// item when no request id is present. Drives the result canvas.\nexport function latestBatchOf(generations: Generation[]): Generation[] {\n const first = generations[0]\n if (!first) return []\n const key = generationClientRequestId(first)\n const batch = key\n ? generations.filter((generation) => generationClientRequestId(generation) === key)\n : [first]\n return [...batch].sort((a, b) => generationOutputIndex(a) - generationOutputIndex(b))\n}\n\nexport function userSafeGenerationMessage(message?: string): string {\n if (!message) return 'Generation failed'\n if (/Tangle API key is invalid or expired/i.test(message)) return message\n if (/(api[_ -]?key|secret|token|credential|env|configured|configuration)/i.test(message)) {\n return 'Generation failed'\n }\n return message\n}\n\nexport function optimisticGeneration({\n type,\n prompt,\n model,\n clientRequestId,\n outputIndex,\n outputCount,\n}: {\n type: GenerationType\n prompt: string\n model?: string\n clientRequestId: string\n outputIndex?: number\n outputCount?: number\n}): Generation {\n const batchId = outputIndex == null ? undefined : clientRequestId\n return {\n id: outputIndex == null ? `local-${clientRequestId}` : `local-${clientRequestId}-${outputIndex}`,\n type,\n prompt,\n result: null,\n model: model ?? null,\n cost: null,\n createdAt: new Date(),\n metadata: {\n generationStatus: 'pending',\n provider: type,\n clientRequestId,\n batchId,\n outputIndex,\n outputCount,\n },\n }\n}\n\nexport function failedOptimisticGeneration(generation: Generation): Generation {\n return {\n ...generation,\n metadata: {\n ...(generation.metadata ?? {}),\n generationStatus: 'failed',\n providerError: 'Generation failed',\n },\n }\n}\n\nexport function normalizeImageCount(value: unknown): number {\n const numeric = typeof value === 'number' ? value : Number(value)\n if (!Number.isFinite(numeric)) return MIN_IMAGE_COUNT\n return Math.min(Math.max(Math.trunc(numeric), MIN_IMAGE_COUNT), MAX_IMAGE_COUNT)\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,41 @@
1
+ import { e as IntakeAnswers, b as IntakeGraph, I as IntakeAnswerValue } from './model-Bzi5i63l.js';
2
+
3
+ /**
4
+ * Pure completion-state helpers for an intake payload — the small algebra over
5
+ * the JSON blob the store persists. No I/O: the store reads/writes the row;
6
+ * these functions only build and judge the payload shape, so the same logic
7
+ * runs in the UI, the handlers, and the DB layer without divergence.
8
+ *
9
+ * A payload is `{ graphId, answers, completedAt? }`. It carries the answers
10
+ * and the id of the graph they were collected against, so a later graph
11
+ * revision can detect a stale payload (`graphId` mismatch) rather than
12
+ * silently mixing answers from two question sets.
13
+ */
14
+
15
+ /** The persisted JSON for one intake (the `payload` column). */
16
+ interface IntakePayload {
17
+ /** The graph id the answers were collected against — guards stale schemas. */
18
+ graphId: string;
19
+ answers: IntakeAnswers;
20
+ /** ISO-8601 instant the intake was completed, or undefined while in progress. */
21
+ completedAt?: string;
22
+ }
23
+ /** An empty payload for a fresh intake against `graph`. */
24
+ declare function emptyPayload(graph: IntakeGraph): IntakePayload;
25
+ /** A copy of `payload` with one answer set (does not mutate the input). */
26
+ declare function withAnswer(payload: IntakePayload, questionId: string, value: IntakeAnswerValue): IntakePayload;
27
+ /**
28
+ * True when the payload's answers complete the graph AND the payload was
29
+ * collected against THIS graph. A `graphId` mismatch is never "complete" —
30
+ * the answers belong to a different question set and must be re-collected.
31
+ */
32
+ declare function payloadComplete(graph: IntakeGraph, payload: IntakePayload): boolean;
33
+ /** True when the payload was collected against a DIFFERENT graph revision. */
34
+ declare function payloadIsStale(graph: IntakeGraph, payload: IntakePayload): boolean;
35
+ /**
36
+ * Stamp the payload complete at `at` (default now). Returns a copy; pure. The
37
+ * caller has already checked `payloadComplete` — this only records the instant.
38
+ */
39
+ declare function markComplete(payload: IntakePayload, at?: Date): IntakePayload;
40
+
41
+ export { type IntakePayload as I, payloadIsStale as a, emptyPayload as e, markComplete as m, payloadComplete as p, withAnswer as w };
@@ -1,7 +1,7 @@
1
- import { I as IntakeAnswerValue, a as IntakeQuestion, b as IntakeGraph } from '../model-Cuj7d-xT.js';
1
+ import { I as IntakeAnswerValue, a as IntakeQuestion, b as IntakeGraph } from '../model-Bzi5i63l.js';
2
2
  import { IntakeStore } from './drizzle.js';
3
3
  import 'drizzle-orm/sqlite-core';
4
- import './index.js';
4
+ import '../completion-Kaqp_tWk.js';
5
5
 
6
6
  /**
7
7
  * Framework-neutral intake API: the get-current / save-answer / complete logic,
@@ -1,7 +1,7 @@
1
1
  import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
2
2
  import { AnySQLiteTable, AnySQLiteColumn, BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core';
3
- import { I as IntakeAnswerValue, b as IntakeGraph } from '../model-Cuj7d-xT.js';
4
- import { IntakePayload } from './index.js';
3
+ import { I as IntakeAnswerValue, b as IntakeGraph } from '../model-Bzi5i63l.js';
4
+ import { I as IntakePayload } from '../completion-Kaqp_tWk.js';
5
5
 
6
6
  /** A product table referenced by FK — only the `id` column is touched. */
7
7
  type IntakeParentTable = AnySQLiteTable & {
@@ -1,42 +1,118 @@
1
- import { c as IntakeAnswers, b as IntakeGraph, I as IntakeAnswerValue } from '../model-Cuj7d-xT.js';
2
- export { A as AnswerRejectionReason, d as AnswerValidationResult, e as IntakeAnswerType, f as IntakeOption, a as IntakeQuestion, g as getQuestion, h as hasAnswer, i as intakeProgress, j as isComplete, n as nextQuestion, r as reachableQuestions, v as validateAnswer } from '../model-Cuj7d-xT.js';
1
+ export { A as AnswerRejectionReason, c as AnswerValidationResult, d as IntakeAnswerType, I as IntakeAnswerValue, e as IntakeAnswers, b as IntakeGraph, f as IntakeOption, a as IntakeQuestion, g as getQuestion, h as hasAnswer, i as intakeProgress, j as isComplete, n as nextQuestion, r as reachableQuestions, v as validateAnswer } from '../model-Bzi5i63l.js';
2
+ export { I as IntakePayload, e as emptyPayload, m as markComplete, p as payloadComplete, a as payloadIsStale, w as withAnswer } from '../completion-Kaqp_tWk.js';
3
3
 
4
4
  /**
5
- * Pure completion-state helpers for an intake payload — the small algebra over
6
- * the JSON blob the store persists. No I/O: the store reads/writes the row;
7
- * these functions only build and judge the payload shape, so the same logic
8
- * runs in the UI, the handlers, and the DB layer without divergence.
5
+ * Pure context-sufficiency floor + conversational-gather scaffold — the
6
+ * product-agnostic core of a CONVERSATIONAL intake. Where the question-graph
7
+ * model (`./model`) runs a one-question-at-a-time interview, this leaf judges
8
+ * whether an agent has gathered ENOUGH context to act, and renders the prompt
9
+ * directive that tells the agent to fold the remaining gaps into its work
10
+ * instead of running a form.
9
11
  *
10
- * A payload is `{ graphId, answers, completedAt? }`. It carries the answers
11
- * and the id of the graph they were collected against, so a later graph
12
- * revision can detect a stale payload (`graphId` mismatch) rather than
13
- * silently mixing answers from two question sets.
12
+ * Zero dependencies: no drizzle, no env, no react, no I/O, no product reads.
13
+ * A product declares WHICH facts matter (`ContextFactSpec`), and its own
14
+ * adapter resolves those facts + named substrate flags from whatever substrate
15
+ * it owns (`ResolvedContextSignals`); this leaf only does the deterministic
16
+ * combine and the wording. That separation is what makes the framework opt-in
17
+ * and tree-shakeable, and what lets gtm/tax/legal/insurance share one core.
18
+ *
19
+ * Readiness is a two-part floor: SCOPE (every required fact has a value) AND
20
+ * SUBSTRATE (at least one named substrate flag is true). Scope alone is not
21
+ * enough — knowing the goal without any durable thing to act on is still
22
+ * not-ready; substrate alone is not enough either. The product's adapter
23
+ * decides what counts as a fact and what counts as substrate.
14
24
  */
15
-
16
- /** The persisted JSON for one intake (the `payload` column). */
17
- interface IntakePayload {
18
- /** The graph id the answers were collected against — guards stale schemas. */
19
- graphId: string;
20
- answers: IntakeAnswers;
21
- /** ISO-8601 instant the intake was completed, or undefined while in progress. */
22
- completedAt?: string;
25
+ /** One fact the product treats as known context once it has a value. */
26
+ interface ContextFact {
27
+ /** Stable key the resolved-signals map is keyed on. */
28
+ key: string;
29
+ /** Human label shown in the prompt's known/missing lists. */
30
+ label: string;
31
+ /** When true, this fact must have a value for SCOPE to be met. */
32
+ required?: boolean;
33
+ /** How the agent should gather this fact conversationally, if missing. */
34
+ gatherHint?: string;
35
+ }
36
+ /**
37
+ * The product's declaration of what context matters: the facts that make up
38
+ * scope, plus optional tool hints appended verbatim to the gather prompt (e.g.
39
+ * a product passes the command that extracts a brand from a URL). Pure data —
40
+ * the product supplies labels, hints, and tool lines; the framework never
41
+ * names a product-specific concept itself.
42
+ */
43
+ interface ContextFactSpec {
44
+ facts: ContextFact[];
45
+ /** Lines appended after the gather directive — e.g. product tool pointers. */
46
+ toolHints?: string[];
47
+ }
48
+ /**
49
+ * What a product's adapter resolves from its own substrate, ready to combine.
50
+ * `facts` carries the resolved VALUE per fact key (undefined/empty = not
51
+ * known); `substrate` carries named boolean flags (e.g.
52
+ * `{ brandConfirmed, configHasContext, coreKnowledgePresent }`) — any one true
53
+ * satisfies the substrate half of the floor.
54
+ */
55
+ interface ResolvedContextSignals {
56
+ facts: Record<string, string | undefined>;
57
+ substrate: Record<string, boolean>;
58
+ }
59
+ /** A resolved fact that has a value — surfaced to the prompt as known context. */
60
+ interface KnownFact {
61
+ key: string;
62
+ label: string;
63
+ value: string;
64
+ }
65
+ /** A required fact with no value — surfaced to the prompt as a gap to close. */
66
+ interface MissingFact {
67
+ key: string;
68
+ label: string;
69
+ gatherHint?: string;
70
+ }
71
+ /** The deterministic verdict over the resolved signals. */
72
+ interface ContextSufficiency {
73
+ /** True when the floor is met: `hasScope && hasSubstrate`. */
74
+ ready: boolean;
75
+ /** Every REQUIRED fact has a non-empty value. */
76
+ hasScope: boolean;
77
+ /** At least one named substrate flag is true. */
78
+ hasSubstrate: boolean;
79
+ /** Facts (required or not) that have a value. */
80
+ knownFacts: KnownFact[];
81
+ /** Required facts that have no value yet. */
82
+ missingFacts: MissingFact[];
83
+ /** The substrate flags as resolved, passed through for the prompt/caller. */
84
+ substrate: Record<string, boolean>;
23
85
  }
24
- /** An empty payload for a fresh intake against `graph`. */
25
- declare function emptyPayload(graph: IntakeGraph): IntakePayload;
26
- /** A copy of `payload` with one answer set (does not mutate the input). */
27
- declare function withAnswer(payload: IntakePayload, questionId: string, value: IntakeAnswerValue): IntakePayload;
28
86
  /**
29
- * True when the payload's answers complete the graph AND the payload was
30
- * collected against THIS graph. A `graphId` mismatch is never "complete"
31
- * the answers belong to a different question set and must be re-collected.
87
+ * Combine a fact spec with resolved signals into the readiness verdict. Pure,
88
+ * deterministic, never throws a missing fact key or an empty substrate map
89
+ * reads as not-ready, not an error, so a fresh scope is honestly not-ready
90
+ * rather than crashing the caller.
91
+ *
92
+ * SCOPE = every `required` fact resolves to a non-empty value.
93
+ * SUBSTRATE = any value in `signals.substrate` is true.
94
+ * READY = SCOPE && SUBSTRATE.
95
+ *
96
+ * `knownFacts` lists every fact (required or optional) that resolved to a
97
+ * value, in spec declaration order; `missingFacts` lists every REQUIRED fact
98
+ * that did not — optional facts never appear as missing because they never gate
99
+ * scope.
32
100
  */
33
- declare function payloadComplete(graph: IntakeGraph, payload: IntakePayload): boolean;
34
- /** True when the payload was collected against a DIFFERENT graph revision. */
35
- declare function payloadIsStale(graph: IntakeGraph, payload: IntakePayload): boolean;
101
+ declare function computeContextSufficiency(spec: ContextFactSpec, signals: ResolvedContextSignals): ContextSufficiency;
36
102
  /**
37
- * Stamp the payload complete at `at` (default now). Returns a copy; pure. The
38
- * caller has already checked `payloadComplete` this only records the instant.
103
+ * Render the prompt section that mirrors a conversational-gather flow:
104
+ * - "### Context you already have"the known facts, as `label: value`.
105
+ * - "### Context still missing — gather it while you work, not as a form" —
106
+ * the missing facts (with their gather hints), the act-first directive, and
107
+ * any product tool hints appended verbatim.
108
+ *
109
+ * When scope is met but the floor still is not (no substrate flag), it emits
110
+ * the substrate directive instead of a missing-facts list. Returns '' when
111
+ * there is nothing to say (no known facts, no gaps, and ready) so a caller can
112
+ * concatenate it unconditionally. Pure: wording is product-neutral; every
113
+ * product-specific phrase comes from the spec's labels, gather hints, and tool
114
+ * hints.
39
115
  */
40
- declare function markComplete(payload: IntakePayload, at?: Date): IntakePayload;
116
+ declare function buildContextGatherPrompt(spec: ContextFactSpec, sufficiency: ContextSufficiency): string;
41
117
 
42
- export { IntakeAnswerValue, IntakeAnswers, IntakeGraph, type IntakePayload, emptyPayload, markComplete, payloadComplete, payloadIsStale, withAnswer };
118
+ export { type ContextFact, type ContextFactSpec, type ContextSufficiency, type KnownFact, type MissingFact, type ResolvedContextSignals, buildContextGatherPrompt, computeContextSufficiency };
@@ -14,7 +14,73 @@ import {
14
14
  reachableQuestions,
15
15
  validateAnswer
16
16
  } from "../chunk-ND3XEGBE.js";
17
+
18
+ // src/intakes/context-sufficiency.ts
19
+ function presentValue(value) {
20
+ if (typeof value !== "string") return void 0;
21
+ const trimmed = value.trim();
22
+ return trimmed.length > 0 ? trimmed : void 0;
23
+ }
24
+ function computeContextSufficiency(spec, signals) {
25
+ const facts = spec.facts ?? [];
26
+ const resolvedFacts = signals.facts ?? {};
27
+ const substrate = signals.substrate ?? {};
28
+ const knownFacts = [];
29
+ const missingFacts = [];
30
+ let hasScope = true;
31
+ for (const fact of facts) {
32
+ const value = presentValue(resolvedFacts[fact.key]);
33
+ if (value !== void 0) {
34
+ knownFacts.push({ key: fact.key, label: fact.label, value });
35
+ } else if (fact.required) {
36
+ hasScope = false;
37
+ missingFacts.push({ key: fact.key, label: fact.label, gatherHint: fact.gatherHint });
38
+ }
39
+ }
40
+ const hasSubstrate = Object.values(substrate).some(Boolean);
41
+ return {
42
+ ready: hasScope && hasSubstrate,
43
+ hasScope,
44
+ hasSubstrate,
45
+ knownFacts,
46
+ missingFacts,
47
+ substrate
48
+ };
49
+ }
50
+ var GATHER_DIRECTIVE = "Do not run an interview. Act on the message first, then fold AT MOST one or two pointed questions into the same turn to close the highest-leverage gap. Never present a form.";
51
+ var SUBSTRATE_DIRECTIVE = "You have the scope but no durable substrate to act on yet. As you work, persist what you learn \u2014 that is what makes this context-ready.";
52
+ function buildContextGatherPrompt(spec, sufficiency) {
53
+ const lines = [];
54
+ if (sufficiency.knownFacts.length > 0) {
55
+ lines.push("### Context you already have");
56
+ lines.push(sufficiency.knownFacts.map((f) => `- ${f.label}: ${f.value}`).join("\n"));
57
+ }
58
+ if (sufficiency.missingFacts.length > 0) {
59
+ if (lines.length > 0) lines.push("");
60
+ lines.push("### Context still missing \u2014 gather it while you work, not as a form");
61
+ const gaps = sufficiency.missingFacts.map((f) => f.gatherHint ? `- ${f.label} \u2014 ${f.gatherHint}` : `- ${f.label}`).join("\n");
62
+ lines.push(`You do NOT have:
63
+ ${gaps}`);
64
+ lines.push(GATHER_DIRECTIVE);
65
+ for (const hint of spec.toolHints ?? []) {
66
+ const trimmed = hint.trim();
67
+ if (trimmed) lines.push(trimmed);
68
+ }
69
+ } else if (!sufficiency.ready) {
70
+ if (lines.length > 0) lines.push("");
71
+ lines.push(SUBSTRATE_DIRECTIVE);
72
+ for (const hint of spec.toolHints ?? []) {
73
+ const trimmed = hint.trim();
74
+ if (trimmed) lines.push(trimmed);
75
+ }
76
+ }
77
+ if (lines.length === 0) return "";
78
+ return `## Project Context & Sufficiency
79
+ ${lines.join("\n")}`;
80
+ }
17
81
  export {
82
+ buildContextGatherPrompt,
83
+ computeContextSufficiency,
18
84
  emptyPayload,
19
85
  getQuestion,
20
86
  hasAnswer,