whale-code 6.5.4 → 6.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/services/agent-config.d.ts +25 -0
- package/dist/cli/services/agent-config.js +61 -0
- package/dist/cli/services/agent-loop.js +30 -9
- package/dist/cli/services/error-logger.d.ts +2 -3
- package/dist/cli/services/error-logger.js +43 -52
- package/dist/cli/services/subagent.js +11 -7
- package/dist/cli/services/teammate.js +28 -14
- package/dist/server/handlers/api-docs.d.ts +6 -0
- package/dist/server/handlers/api-docs.js +1478 -0
- package/dist/server/handlers/api-keys.js +16 -2
- package/dist/server/handlers/comms.d.ts +0 -53
- package/dist/server/handlers/comms.js +45 -27
- package/dist/server/handlers/voice.js +22 -0
- package/dist/server/index.js +57 -26
- package/dist/server/lib/clickhouse-client.js +2 -2
- package/dist/server/lib/pdf-renderer.d.ts +1 -1
- package/dist/server/lib/pdf-renderer.js +18 -4
- package/dist/server/lib/server-agent-loop.d.ts +6 -0
- package/dist/server/lib/server-agent-loop.js +20 -10
- package/dist/server/lib/server-subagent.d.ts +2 -0
- package/dist/server/lib/server-subagent.js +4 -2
- package/dist/server/providers/anthropic.js +4 -4
- package/dist/server/providers/bedrock.js +4 -4
- package/dist/server/tool-router.d.ts +13 -0
- package/dist/server/tool-router.js +3 -1
- package/dist/shared/agent-core.d.ts +86 -8
- package/dist/shared/agent-core.js +94 -19
- package/dist/shared/api-client.d.ts +1 -0
- package/dist/shared/api-client.js +2 -2
- package/dist/shared/tool-dispatch.d.ts +0 -2
- package/package.json +1 -1
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
// Supports linking keys to creations (TV menus, displays, landing pages)
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
const KEY_COLS = "id, name, key_prefix, key_type, scope, is_active, rate_limit_per_minute, rate_limit_per_day, last_used_at, request_count, expires_at, revoked_at, revoked_reason, creation_id, client_store_id, created_at, updated_at";
|
|
5
|
+
// Canonical format: action:resource (e.g. "read:products", "write:cart")
|
|
6
|
+
const SCOPE_ACTIONS = new Set(["read", "write"]);
|
|
7
|
+
function normalizeScopes(scopes) {
|
|
8
|
+
return scopes.map(s => {
|
|
9
|
+
if (s === "*" || !s.includes(":"))
|
|
10
|
+
return s;
|
|
11
|
+
const [left, right] = s.split(":", 2);
|
|
12
|
+
if (SCOPE_ACTIONS.has(left))
|
|
13
|
+
return s; // already "read:products"
|
|
14
|
+
if (SCOPE_ACTIONS.has(right))
|
|
15
|
+
return `${right}:${left}`; // "products:read" → "read:products"
|
|
16
|
+
return s;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
5
19
|
export async function handleAPIKeys(sb, args, storeId) {
|
|
6
20
|
const sid = storeId;
|
|
7
21
|
const action = args.action;
|
|
@@ -15,7 +29,7 @@ export async function handleAPIKeys(sb, args, storeId) {
|
|
|
15
29
|
if (keyType !== "live" && keyType !== "test") {
|
|
16
30
|
return { success: false, error: "key_type must be 'live' or 'test'" };
|
|
17
31
|
}
|
|
18
|
-
const scopes = args.scopes || ["*"];
|
|
32
|
+
const scopes = normalizeScopes(args.scopes || ["*"]);
|
|
19
33
|
const rateLimitPerMinute = args.rate_limit_per_minute || 60;
|
|
20
34
|
const rateLimitPerDay = args.rate_limit_per_day || 10000;
|
|
21
35
|
const expiresAt = args.expires_at;
|
|
@@ -167,7 +181,7 @@ export async function handleAPIKeys(sb, args, storeId) {
|
|
|
167
181
|
if (args.name !== undefined)
|
|
168
182
|
updates.name = args.name;
|
|
169
183
|
if (args.scopes !== undefined)
|
|
170
|
-
updates.scope = args.scopes;
|
|
184
|
+
updates.scope = normalizeScopes(args.scopes);
|
|
171
185
|
if (args.rate_limit_per_minute !== undefined)
|
|
172
186
|
updates.rate_limit_per_minute = args.rate_limit_per_minute;
|
|
173
187
|
if (args.rate_limit_per_day !== undefined)
|
|
@@ -35,7 +35,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
35
35
|
results?: undefined;
|
|
36
36
|
errors?: undefined;
|
|
37
37
|
warnings?: undefined;
|
|
38
|
-
customers?: undefined;
|
|
39
38
|
document_ids?: undefined;
|
|
40
39
|
customer_id?: undefined;
|
|
41
40
|
customer_email?: undefined;
|
|
@@ -73,7 +72,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
73
72
|
results?: undefined;
|
|
74
73
|
errors?: undefined;
|
|
75
74
|
warnings?: undefined;
|
|
76
|
-
customers?: undefined;
|
|
77
75
|
document_ids?: undefined;
|
|
78
76
|
customer_id?: undefined;
|
|
79
77
|
customer_email?: undefined;
|
|
@@ -103,7 +101,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
103
101
|
results?: undefined;
|
|
104
102
|
errors?: undefined;
|
|
105
103
|
warnings?: undefined;
|
|
106
|
-
customers?: undefined;
|
|
107
104
|
document_ids?: undefined;
|
|
108
105
|
customer_id?: undefined;
|
|
109
106
|
customer_email?: undefined;
|
|
@@ -133,7 +130,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
133
130
|
results?: undefined;
|
|
134
131
|
errors?: undefined;
|
|
135
132
|
warnings?: undefined;
|
|
136
|
-
customers?: undefined;
|
|
137
133
|
document_ids?: undefined;
|
|
138
134
|
customer_id?: undefined;
|
|
139
135
|
customer_email?: undefined;
|
|
@@ -170,7 +166,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
170
166
|
results?: undefined;
|
|
171
167
|
errors?: undefined;
|
|
172
168
|
warnings?: undefined;
|
|
173
|
-
customers?: undefined;
|
|
174
169
|
document_ids?: undefined;
|
|
175
170
|
customer_id?: undefined;
|
|
176
171
|
customer_email?: undefined;
|
|
@@ -200,7 +195,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
200
195
|
results?: undefined;
|
|
201
196
|
errors?: undefined;
|
|
202
197
|
warnings?: undefined;
|
|
203
|
-
customers?: undefined;
|
|
204
198
|
document_ids?: undefined;
|
|
205
199
|
customer_id?: undefined;
|
|
206
200
|
customer_email?: undefined;
|
|
@@ -236,7 +230,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
236
230
|
results?: undefined;
|
|
237
231
|
errors?: undefined;
|
|
238
232
|
warnings?: undefined;
|
|
239
|
-
customers?: undefined;
|
|
240
233
|
document_ids?: undefined;
|
|
241
234
|
customer_id?: undefined;
|
|
242
235
|
customer_email?: undefined;
|
|
@@ -273,7 +266,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
273
266
|
results?: undefined;
|
|
274
267
|
errors?: undefined;
|
|
275
268
|
warnings?: undefined;
|
|
276
|
-
customers?: undefined;
|
|
277
269
|
document_ids?: undefined;
|
|
278
270
|
customer_id?: undefined;
|
|
279
271
|
customer_email?: undefined;
|
|
@@ -307,7 +299,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
307
299
|
profiles?: undefined;
|
|
308
300
|
errors?: undefined;
|
|
309
301
|
warnings?: undefined;
|
|
310
|
-
customers?: undefined;
|
|
311
302
|
document_ids?: undefined;
|
|
312
303
|
customer_id?: undefined;
|
|
313
304
|
customer_email?: undefined;
|
|
@@ -338,7 +329,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
338
329
|
succeeded?: undefined;
|
|
339
330
|
failed?: undefined;
|
|
340
331
|
results?: undefined;
|
|
341
|
-
customers?: undefined;
|
|
342
332
|
document_ids?: undefined;
|
|
343
333
|
customer_id?: undefined;
|
|
344
334
|
customer_email?: undefined;
|
|
@@ -367,7 +357,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
367
357
|
failed?: undefined;
|
|
368
358
|
results?: undefined;
|
|
369
359
|
errors?: undefined;
|
|
370
|
-
customers?: undefined;
|
|
371
360
|
document_ids?: undefined;
|
|
372
361
|
customer_id?: undefined;
|
|
373
362
|
customer_email?: undefined;
|
|
@@ -404,7 +393,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
404
393
|
results?: undefined;
|
|
405
394
|
errors?: undefined;
|
|
406
395
|
warnings?: undefined;
|
|
407
|
-
customers?: undefined;
|
|
408
396
|
document_ids?: undefined;
|
|
409
397
|
customer_id?: undefined;
|
|
410
398
|
customer_email?: undefined;
|
|
@@ -441,45 +429,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
441
429
|
results?: undefined;
|
|
442
430
|
errors?: undefined;
|
|
443
431
|
warnings?: undefined;
|
|
444
|
-
customers?: undefined;
|
|
445
|
-
document_ids?: undefined;
|
|
446
|
-
customer_id?: undefined;
|
|
447
|
-
customer_email?: undefined;
|
|
448
|
-
email_sent?: undefined;
|
|
449
|
-
};
|
|
450
|
-
error?: undefined;
|
|
451
|
-
} | {
|
|
452
|
-
success: boolean;
|
|
453
|
-
data: {
|
|
454
|
-
count: number;
|
|
455
|
-
customers: {
|
|
456
|
-
id: any;
|
|
457
|
-
name: string;
|
|
458
|
-
email: any;
|
|
459
|
-
phone: any;
|
|
460
|
-
loyalty_tier: any;
|
|
461
|
-
total_orders: any;
|
|
462
|
-
is_active: any;
|
|
463
|
-
}[];
|
|
464
|
-
id?: undefined;
|
|
465
|
-
name?: undefined;
|
|
466
|
-
type?: undefined;
|
|
467
|
-
url?: undefined;
|
|
468
|
-
file_name?: undefined;
|
|
469
|
-
size?: undefined;
|
|
470
|
-
documents?: undefined;
|
|
471
|
-
deleted?: undefined;
|
|
472
|
-
template_id?: undefined;
|
|
473
|
-
templates?: undefined;
|
|
474
|
-
template?: undefined;
|
|
475
|
-
stores?: undefined;
|
|
476
|
-
profiles?: undefined;
|
|
477
|
-
total?: undefined;
|
|
478
|
-
succeeded?: undefined;
|
|
479
|
-
failed?: undefined;
|
|
480
|
-
results?: undefined;
|
|
481
|
-
errors?: undefined;
|
|
482
|
-
warnings?: undefined;
|
|
483
432
|
document_ids?: undefined;
|
|
484
433
|
customer_id?: undefined;
|
|
485
434
|
customer_email?: undefined;
|
|
@@ -513,7 +462,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
513
462
|
results?: undefined;
|
|
514
463
|
errors?: undefined;
|
|
515
464
|
warnings?: undefined;
|
|
516
|
-
customers?: undefined;
|
|
517
465
|
};
|
|
518
466
|
error?: undefined;
|
|
519
467
|
} | {
|
|
@@ -548,7 +496,6 @@ export declare function handleDocuments(sb: SupabaseClient, args: Record<string,
|
|
|
548
496
|
results?: undefined;
|
|
549
497
|
errors?: undefined;
|
|
550
498
|
warnings?: undefined;
|
|
551
|
-
customers?: undefined;
|
|
552
499
|
document_ids?: undefined;
|
|
553
500
|
customer_id?: undefined;
|
|
554
501
|
customer_email?: undefined;
|
|
@@ -4,9 +4,26 @@ import { applyGenerationRules, generateCannabinoidData, applyCalculations, runVa
|
|
|
4
4
|
import { renderLayoutToPdf, renderHtmlToPdf, renderLabelToPdf } from "../lib/react-pdf-layout.js";
|
|
5
5
|
import { renderCOAToPdf } from "../lib/coa-renderer.js";
|
|
6
6
|
import QRCode from "qrcode";
|
|
7
|
+
import { handleBrowser } from "./browser.js";
|
|
7
8
|
const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; // 10MB per attachment
|
|
8
9
|
const MAX_TOTAL_ATTACHMENT_BYTES = 25 * 1024 * 1024; // 25MB total
|
|
9
10
|
const MAX_ATTACHMENT_COUNT = 10;
|
|
11
|
+
/** Generate a PNG thumbnail for a PDF document via Playwright screenshot and store it in Supabase. */
|
|
12
|
+
async function generateThumbnail(sb, docId, pdfUrl) {
|
|
13
|
+
// Wrap in Google Docs viewer — headless Chromium downloads raw PDFs instead of rendering them
|
|
14
|
+
const viewerUrl = `https://docs.google.com/gview?url=${encodeURIComponent(pdfUrl)}&embedded=true`;
|
|
15
|
+
const result = await handleBrowser(sb, { action: "screenshot", url: viewerUrl });
|
|
16
|
+
const b64 = result.data?.screenshot_base64;
|
|
17
|
+
if (!b64)
|
|
18
|
+
return;
|
|
19
|
+
const thumbBuffer = Buffer.from(b64, 'base64');
|
|
20
|
+
const thumbPath = `thumbs/${docId}.png`;
|
|
21
|
+
await sb.storage.from('screenshots').upload(thumbPath, thumbBuffer, {
|
|
22
|
+
contentType: 'image/png', upsert: true,
|
|
23
|
+
});
|
|
24
|
+
const { data: thumbUrlData } = sb.storage.from('screenshots').getPublicUrl(thumbPath);
|
|
25
|
+
await sb.from('store_documents').update({ thumbnail_url: thumbUrlData.publicUrl }).eq('id', docId);
|
|
26
|
+
}
|
|
10
27
|
export async function handleEmail(sb, args, storeId) {
|
|
11
28
|
if (!storeId)
|
|
12
29
|
return { success: false, error: "store_id required" };
|
|
@@ -369,6 +386,8 @@ export async function handleDocuments(sb, args, storeId) {
|
|
|
369
386
|
}).select("id, document_name, file_url, created_at").single();
|
|
370
387
|
if (insertErr)
|
|
371
388
|
return { success: false, error: insertErr.message };
|
|
389
|
+
if (fileUrl.endsWith('.pdf'))
|
|
390
|
+
await generateThumbnail(sb, record.id, fileUrl).catch(() => { });
|
|
372
391
|
return { success: true, data: { id: record.id, name: record.document_name, type: docType, url: record.file_url, file_name: fileName, size: sizeBytes } };
|
|
373
392
|
}
|
|
374
393
|
case "find": {
|
|
@@ -521,6 +540,8 @@ export async function handleDocuments(sb, args, storeId) {
|
|
|
521
540
|
}).select("id, document_name, file_url, created_at").single();
|
|
522
541
|
if (insertErr)
|
|
523
542
|
return { success: false, error: insertErr.message };
|
|
543
|
+
if (urlData.publicUrl.endsWith('.pdf'))
|
|
544
|
+
await generateThumbnail(sb, record.id, urlData.publicUrl).catch(() => { });
|
|
524
545
|
return { success: true, data: { id: record.id, name: record.document_name, type: docType, template: template.name, url: record.file_url, size: sizeBytes } };
|
|
525
546
|
}
|
|
526
547
|
case "list_stores": {
|
|
@@ -684,6 +705,16 @@ export async function handleDocuments(sb, args, storeId) {
|
|
|
684
705
|
if (cannabinoidCfg && !mergedData.cannabinoids) {
|
|
685
706
|
mergedData.cannabinoids = generateCannabinoidData(cannabinoidCfg, mergedConstants);
|
|
686
707
|
}
|
|
708
|
+
// 6b. Flatten cannabinoid rows into top-level keys for calculation formulas
|
|
709
|
+
// e.g. { name: "D9-THC", percentWeight: 0.22 } → D9_THC: 0.22
|
|
710
|
+
if (Array.isArray(mergedData.cannabinoids)) {
|
|
711
|
+
for (const row of mergedData.cannabinoids) {
|
|
712
|
+
const key = row.name.replace(/-/g, "_").replace(/[^a-zA-Z0-9_]/g, "");
|
|
713
|
+
if (key && row.percentWeight !== undefined) {
|
|
714
|
+
mergedData[key] = row.percentWeight;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
687
718
|
// 7. Apply calculations
|
|
688
719
|
mergedData = applyCalculations(tpl.calculations, mergedConstants, mergedData);
|
|
689
720
|
// Also run per-row calculations if cannabinoids exist
|
|
@@ -715,6 +746,16 @@ export async function handleDocuments(sb, args, storeId) {
|
|
|
715
746
|
residualSolvents: { status: "PASS" },
|
|
716
747
|
};
|
|
717
748
|
}
|
|
749
|
+
// 7b2. Auto-set test status flags for summary section
|
|
750
|
+
if (mergedData.cannabinoids && mergedData.cannabinoids.length > 0) {
|
|
751
|
+
mergedData.testsCannabinoids = true;
|
|
752
|
+
}
|
|
753
|
+
if (mergedData.moisture !== undefined && mergedData.moisture !== null) {
|
|
754
|
+
mergedData.testsMoisture = true;
|
|
755
|
+
}
|
|
756
|
+
if (mergedData.dateTested) {
|
|
757
|
+
mergedData.testsBatch = true;
|
|
758
|
+
}
|
|
718
759
|
// 7c. Generate QR code for COA templates — links to Quantix COA landing page
|
|
719
760
|
if (tpl.slug?.startsWith("cannabis-coa") && !mergedData.qrCodeDataUrl) {
|
|
720
761
|
const productSlug = (mergedData.sampleName || "certificate")
|
|
@@ -843,6 +884,7 @@ export async function handleDocuments(sb, args, storeId) {
|
|
|
843
884
|
}).select("id, document_name, file_url, created_at").single();
|
|
844
885
|
if (insertErr)
|
|
845
886
|
return { success: false, error: insertErr.message };
|
|
887
|
+
await generateThumbnail(sb, record.id, urlData.publicUrl).catch(() => { });
|
|
846
888
|
return {
|
|
847
889
|
success: true,
|
|
848
890
|
data: {
|
|
@@ -907,32 +949,8 @@ export async function handleDocuments(sb, args, storeId) {
|
|
|
907
949
|
};
|
|
908
950
|
}
|
|
909
951
|
case "list_clients":
|
|
910
|
-
case "list_customers":
|
|
911
|
-
|
|
912
|
-
.select("id, first_name, last_name, email, phone, loyalty_tier, total_orders, is_active, created_at")
|
|
913
|
-
.eq("store_id", sid)
|
|
914
|
-
.order("created_at", { ascending: false });
|
|
915
|
-
if (args.query) {
|
|
916
|
-
const term = `%${sanitizeFilterValue(String(args.query).trim())}%`;
|
|
917
|
-
query = query.or(`first_name.ilike.${term},last_name.ilike.${term},email.ilike.${term}`);
|
|
918
|
-
}
|
|
919
|
-
if (args.limit)
|
|
920
|
-
query = query.limit(args.limit);
|
|
921
|
-
const { data, error } = await query;
|
|
922
|
-
if (error)
|
|
923
|
-
return { success: false, error: error.message };
|
|
924
|
-
return {
|
|
925
|
-
success: true,
|
|
926
|
-
data: {
|
|
927
|
-
count: data?.length || 0,
|
|
928
|
-
customers: (data || []).map(c => ({
|
|
929
|
-
id: c.id, name: [c.first_name, c.last_name].filter(Boolean).join(" "),
|
|
930
|
-
email: c.email, phone: c.phone, loyalty_tier: c.loyalty_tier,
|
|
931
|
-
total_orders: c.total_orders, is_active: c.is_active,
|
|
932
|
-
})),
|
|
933
|
-
},
|
|
934
|
-
};
|
|
935
|
-
}
|
|
952
|
+
case "list_customers":
|
|
953
|
+
return { success: false, error: "Use the 'customers' tool instead. Clients are customers." };
|
|
936
954
|
case "deliver_documents": {
|
|
937
955
|
// Batch-assign documents to a customer and optionally send notification email
|
|
938
956
|
const docIds = args.document_ids || (args.document_id ? [args.document_id] : []);
|
|
@@ -1016,6 +1034,6 @@ export async function handleDocuments(sb, args, storeId) {
|
|
|
1016
1034
|
};
|
|
1017
1035
|
}
|
|
1018
1036
|
default:
|
|
1019
|
-
return { success: false, error: `Unknown documents action: ${action}. Valid: create, find, delete, create_template, list_templates, from_template, list_stores, list_profiles, generate, bulk_generate, generate_pdf, list_pdf_templates, list_document_profiles,
|
|
1037
|
+
return { success: false, error: `Unknown documents action: ${action}. Valid: create, find, delete, create_template, list_templates, from_template, list_stores, list_profiles, generate, bulk_generate, generate_pdf, list_pdf_templates, list_document_profiles, deliver_documents, list_customer_documents` };
|
|
1020
1038
|
}
|
|
1021
1039
|
}
|
|
@@ -1084,6 +1084,28 @@ export async function handleVoice(sb, args, storeId) {
|
|
|
1084
1084
|
headers: { "xi-api-key": apiKey },
|
|
1085
1085
|
});
|
|
1086
1086
|
if (!resp.ok) {
|
|
1087
|
+
// If key lacks user_read scope, fall back to a partial status check
|
|
1088
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
1089
|
+
const voicesResp = await fetchWithRetry(`${ELEVENLABS_BASE}/voices`, {
|
|
1090
|
+
headers: { "xi-api-key": apiKey },
|
|
1091
|
+
});
|
|
1092
|
+
const keyWorks = voicesResp.ok;
|
|
1093
|
+
const voiceCount = keyWorks ? ((await voicesResp.json()).voices?.length ?? 0) : 0;
|
|
1094
|
+
return {
|
|
1095
|
+
success: true,
|
|
1096
|
+
data: {
|
|
1097
|
+
tier: "unknown (API key missing user_read scope)",
|
|
1098
|
+
character_count: null,
|
|
1099
|
+
character_limit: null,
|
|
1100
|
+
characters_remaining: null,
|
|
1101
|
+
usage_percent: null,
|
|
1102
|
+
next_reset: null,
|
|
1103
|
+
key_valid: keyWorks,
|
|
1104
|
+
voice_count: voiceCount,
|
|
1105
|
+
note: "ElevenLabs API key is missing the 'user_read' permission. Regenerate the key at elevenlabs.io with full permissions to see usage stats. All generation features (speak, music, SFX) still work.",
|
|
1106
|
+
},
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1087
1109
|
const errText = await resp.text();
|
|
1088
1110
|
return { success: false, error: `Usage error ${resp.status}: ${errText}` };
|
|
1089
1111
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { randomUUID, timingSafeEqual, createHash } from "node:crypto";
|
|
|
8
8
|
import Anthropic from "@anthropic-ai/sdk";
|
|
9
9
|
import { createLogger } from "./lib/logger.js";
|
|
10
10
|
const log = createLogger("server");
|
|
11
|
-
import {
|
|
11
|
+
import { sanitizeError, resolveAgentLoopConfig, } from "../shared/agent-core.js";
|
|
12
12
|
import { MODELS } from "../shared/constants.js";
|
|
13
13
|
import { handleProxy } from "./proxy-handlers.js";
|
|
14
14
|
import { handleNodeRoutes, setNodeAgentInvoker } from "./handlers/nodes.js";
|
|
@@ -939,8 +939,12 @@ async function handleAgentChat(req, res, supabase, body, user, isServiceRole, to
|
|
|
939
939
|
clientContext: context, userId, userEmail, extendedTools: getExtendedToolsIndex(),
|
|
940
940
|
});
|
|
941
941
|
const anthropic = getAnthropicClient(agent);
|
|
942
|
-
|
|
943
|
-
const
|
|
942
|
+
// Resolve all behavioral config from DB — single source of truth
|
|
943
|
+
const resolved = resolveAgentLoopConfig(agent, "sse");
|
|
944
|
+
if (resolved.defaultsUsed.length > 0) {
|
|
945
|
+
log.info({ agentId, defaults: resolved.defaultsUsed }, "agent config defaults applied");
|
|
946
|
+
}
|
|
947
|
+
const MAX_HISTORY_CHARS = resolved.maxHistoryChars;
|
|
944
948
|
// Build user message — multi-modal if image attachments present
|
|
945
949
|
let userContent;
|
|
946
950
|
if (attachments?.length) {
|
|
@@ -978,7 +982,6 @@ async function handleAgentChat(req, res, supabase, body, user, isServiceRole, to
|
|
|
978
982
|
// Client disconnect detection
|
|
979
983
|
let clientDisconnected = false;
|
|
980
984
|
req.on("close", () => { clientDisconnected = true; });
|
|
981
|
-
const maxDurationMs = 15 * 60 * 1000;
|
|
982
985
|
const startedAt = Date.now();
|
|
983
986
|
const chatStartTime = Date.now();
|
|
984
987
|
try {
|
|
@@ -990,9 +993,17 @@ async function handleAgentChat(req, res, supabase, body, user, isServiceRole, to
|
|
|
990
993
|
messages,
|
|
991
994
|
tools,
|
|
992
995
|
extendedTools,
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
+
// All behavioral knobs from resolved config — DB is single source of truth
|
|
997
|
+
maxTurns: resolved.maxTurns,
|
|
998
|
+
temperature: resolved.temperature,
|
|
999
|
+
maxTokens: resolved.maxTokens,
|
|
1000
|
+
maxConcurrentTools: resolved.maxConcurrentTools,
|
|
1001
|
+
enableDelegation: resolved.enableDelegation,
|
|
1002
|
+
enableModelRouting: resolved.enableModelRouting,
|
|
1003
|
+
contextOverrides: resolved.contextOverrides,
|
|
1004
|
+
subagentMaxTokens: resolved.subagentMaxTokens,
|
|
1005
|
+
subagentMaxTurns: resolved.subagentMaxTurns,
|
|
1006
|
+
subagentTemperature: resolved.subagentTemperature,
|
|
996
1007
|
storeId,
|
|
997
1008
|
traceId,
|
|
998
1009
|
userId,
|
|
@@ -1022,7 +1033,7 @@ async function handleAgentChat(req, res, supabase, body, user, isServiceRole, to
|
|
|
1022
1033
|
},
|
|
1023
1034
|
clientDisconnected: { get value() { return clientDisconnected; } },
|
|
1024
1035
|
startedAt,
|
|
1025
|
-
maxDurationMs,
|
|
1036
|
+
maxDurationMs: resolved.maxDurationMs,
|
|
1026
1037
|
});
|
|
1027
1038
|
// Send usage SSE
|
|
1028
1039
|
sendSSE(res, {
|
|
@@ -2344,16 +2355,14 @@ setAgentExecutor(async (supabase, agentId, prompt, storeId, maxTurns = 5, onToke
|
|
|
2344
2355
|
const { rows: userToolRows, defs: userToolDefs } = await loadUserTools(supabase, storeId);
|
|
2345
2356
|
const tools = getToolsForAgent(agent, allTools, userToolDefs);
|
|
2346
2357
|
const agentModel = agent.model || MODELS.SONNET;
|
|
2347
|
-
//
|
|
2348
|
-
const
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
if (agent.verbosity === "concise")
|
|
2356
|
-
systemPrompt += "\n\nBe concise.";
|
|
2358
|
+
// Resolve all behavioral config from DB — workflow maxTurns caps agent config
|
|
2359
|
+
const resolved = resolveAgentLoopConfig(agent, "workflow", maxTurns);
|
|
2360
|
+
if (resolved.defaultsUsed.length > 0) {
|
|
2361
|
+
log.info({ agentId, callPath: "workflow", defaults: resolved.defaultsUsed }, "workflow agent config defaults applied");
|
|
2362
|
+
}
|
|
2363
|
+
// Build full system prompt — shared helper ensures workflow gets same context as SSE/channel
|
|
2364
|
+
// (previously hand-built, missing location/customer context)
|
|
2365
|
+
const { systemPrompt } = await buildAgentSystemPrompt(supabase, agent, storeId, prompt, tools);
|
|
2357
2366
|
try {
|
|
2358
2367
|
const result = await runServerAgentLoop({
|
|
2359
2368
|
anthropic: getAnthropicClient(agent),
|
|
@@ -2362,8 +2371,17 @@ setAgentExecutor(async (supabase, agentId, prompt, storeId, maxTurns = 5, onToke
|
|
|
2362
2371
|
systemPrompt,
|
|
2363
2372
|
messages: [{ role: "user", content: prompt }],
|
|
2364
2373
|
tools,
|
|
2365
|
-
|
|
2366
|
-
|
|
2374
|
+
// All behavioral knobs from resolved config — DB is single source of truth
|
|
2375
|
+
maxTurns: resolved.maxTurns,
|
|
2376
|
+
temperature: resolved.temperature,
|
|
2377
|
+
maxTokens: resolved.maxTokens,
|
|
2378
|
+
maxConcurrentTools: resolved.maxConcurrentTools,
|
|
2379
|
+
enableDelegation: resolved.enableDelegation,
|
|
2380
|
+
enableModelRouting: resolved.enableModelRouting,
|
|
2381
|
+
contextOverrides: resolved.contextOverrides,
|
|
2382
|
+
subagentMaxTokens: resolved.subagentMaxTokens,
|
|
2383
|
+
subagentMaxTurns: resolved.subagentMaxTurns,
|
|
2384
|
+
subagentTemperature: resolved.subagentTemperature,
|
|
2367
2385
|
storeId,
|
|
2368
2386
|
source: "workflow_agent",
|
|
2369
2387
|
agentId,
|
|
@@ -2376,7 +2394,7 @@ setAgentExecutor(async (supabase, agentId, prompt, storeId, maxTurns = 5, onToke
|
|
|
2376
2394
|
},
|
|
2377
2395
|
enableStreaming: !!onToken,
|
|
2378
2396
|
onText: onToken || undefined,
|
|
2379
|
-
maxDurationMs:
|
|
2397
|
+
maxDurationMs: resolved.maxDurationMs,
|
|
2380
2398
|
});
|
|
2381
2399
|
return { success: true, response: result.finalText || "(no response)" };
|
|
2382
2400
|
}
|
|
@@ -2403,6 +2421,11 @@ async function invokeAgentForChannel(supabase, agentId, message, storeId, conver
|
|
|
2403
2421
|
const { rows: userToolRows, defs: userToolDefs } = await loadUserTools(supabase, storeId);
|
|
2404
2422
|
const tools = getToolsForAgent(agent, coreTools, userToolDefs);
|
|
2405
2423
|
const agentModel = agent.model || MODELS.SONNET;
|
|
2424
|
+
// Resolve all behavioral config from DB — channel has 15-turn safety cap built in
|
|
2425
|
+
const resolved = resolveAgentLoopConfig(agent, "channel");
|
|
2426
|
+
if (resolved.defaultsUsed.length > 0) {
|
|
2427
|
+
log.info({ agentId, callPath: "channel", defaults: resolved.defaultsUsed }, "channel agent config defaults applied");
|
|
2428
|
+
}
|
|
2406
2429
|
// Build system prompt — shared helper, identical to SSE chat
|
|
2407
2430
|
const { systemPrompt, dynamicContext } = await buildAgentSystemPrompt(supabase, agent, storeId, message, tools, { senderContext, extendedTools: getExtendedToolsIndex() });
|
|
2408
2431
|
// Update ai_conversations with agent_id (fire-and-forget)
|
|
@@ -2410,8 +2433,7 @@ async function invokeAgentForChannel(supabase, agentId, message, storeId, conver
|
|
|
2410
2433
|
Promise.resolve(supabase.from("ai_conversations").update({ agent_id: agentId }).eq("id", conversationId).is("agent_id", null)).catch(() => { });
|
|
2411
2434
|
}
|
|
2412
2435
|
// Load conversation history with size-based compaction (same as SSE chat)
|
|
2413
|
-
const
|
|
2414
|
-
const MAX_HISTORY_CHARS = ctxCfg?.max_history_chars || 400_000;
|
|
2436
|
+
const MAX_HISTORY_CHARS = resolved.maxHistoryChars;
|
|
2415
2437
|
let loadedHistory = [];
|
|
2416
2438
|
if (conversationId) {
|
|
2417
2439
|
try {
|
|
@@ -2448,8 +2470,17 @@ async function invokeAgentForChannel(supabase, agentId, message, storeId, conver
|
|
|
2448
2470
|
messages,
|
|
2449
2471
|
tools,
|
|
2450
2472
|
extendedTools,
|
|
2451
|
-
|
|
2452
|
-
|
|
2473
|
+
// All behavioral knobs from resolved config — DB is single source of truth
|
|
2474
|
+
maxTurns: resolved.maxTurns,
|
|
2475
|
+
temperature: resolved.temperature,
|
|
2476
|
+
maxTokens: resolved.maxTokens,
|
|
2477
|
+
maxConcurrentTools: resolved.maxConcurrentTools,
|
|
2478
|
+
enableDelegation: resolved.enableDelegation,
|
|
2479
|
+
enableModelRouting: resolved.enableModelRouting,
|
|
2480
|
+
contextOverrides: resolved.contextOverrides,
|
|
2481
|
+
subagentMaxTokens: resolved.subagentMaxTokens,
|
|
2482
|
+
subagentMaxTurns: resolved.subagentMaxTurns,
|
|
2483
|
+
subagentTemperature: resolved.subagentTemperature,
|
|
2453
2484
|
storeId,
|
|
2454
2485
|
source: "channel_agent",
|
|
2455
2486
|
agentId,
|
|
@@ -2466,7 +2497,7 @@ async function invokeAgentForChannel(supabase, agentId, message, storeId, conver
|
|
|
2466
2497
|
return executeTool(supabase, toolName, toolArgs, storeId, traceId, senderUserId, senderUserLabel, "channel_agent", conversationId, userToolRows, agentId, undefined, true);
|
|
2467
2498
|
},
|
|
2468
2499
|
enableStreaming: false,
|
|
2469
|
-
maxDurationMs:
|
|
2500
|
+
maxDurationMs: resolved.maxDurationMs,
|
|
2470
2501
|
});
|
|
2471
2502
|
// Persist everything — shared helper, identical to SSE chat
|
|
2472
2503
|
await persistAgentTurn(supabase, agent, {
|
|
@@ -34,9 +34,9 @@ class ClickHouseClient {
|
|
|
34
34
|
return `https://${this.host}:8443`;
|
|
35
35
|
}
|
|
36
36
|
get authHeaders() {
|
|
37
|
+
const credentials = Buffer.from(`${this.user}:${this.password}`).toString("base64");
|
|
37
38
|
return {
|
|
38
|
-
|
|
39
|
-
"X-ClickHouse-Key": this.password,
|
|
39
|
+
Authorization: `Basic ${credentials}`,
|
|
40
40
|
"X-ClickHouse-Database": this.database,
|
|
41
41
|
};
|
|
42
42
|
}
|
|
@@ -75,7 +75,7 @@ interface Calculation {
|
|
|
75
75
|
formula: string;
|
|
76
76
|
decimals?: number;
|
|
77
77
|
}
|
|
78
|
-
export declare function applyCalculations(calculations: Calculation[] | undefined | null, constants: Record<string, unknown>, data: Record<string, unknown>): Record<string, unknown>;
|
|
78
|
+
export declare function applyCalculations(calculations: Calculation[] | Record<string, string> | undefined | null, constants: Record<string, unknown>, data: Record<string, unknown>): Record<string, unknown>;
|
|
79
79
|
interface ValidationRule {
|
|
80
80
|
name: string;
|
|
81
81
|
formula: string;
|
|
@@ -225,7 +225,7 @@ function tokenize(expr) {
|
|
|
225
225
|
i++;
|
|
226
226
|
continue;
|
|
227
227
|
}
|
|
228
|
-
if (/[0-9
|
|
228
|
+
if (/[0-9]/.test(ch) || (ch === "." && i + 1 < expr.length && /[0-9]/.test(expr[i + 1]))) {
|
|
229
229
|
let num = "";
|
|
230
230
|
while (i < expr.length && /[0-9.]/.test(expr[i])) {
|
|
231
231
|
num += expr[i];
|
|
@@ -391,8 +391,22 @@ function safeEval(expr, ctx) {
|
|
|
391
391
|
const parser = new ExprParser(tokens, ctx);
|
|
392
392
|
return parser.parse();
|
|
393
393
|
}
|
|
394
|
+
function normalizeCalculations(raw) {
|
|
395
|
+
if (!raw)
|
|
396
|
+
return [];
|
|
397
|
+
if (Array.isArray(raw))
|
|
398
|
+
return raw;
|
|
399
|
+
if (typeof raw === "object") {
|
|
400
|
+
return Object.entries(raw).map(([field, formula]) => ({
|
|
401
|
+
field,
|
|
402
|
+
formula: String(formula),
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
return [];
|
|
406
|
+
}
|
|
394
407
|
export function applyCalculations(calculations, constants, data) {
|
|
395
|
-
|
|
408
|
+
const normalized = normalizeCalculations(calculations);
|
|
409
|
+
if (!normalized.length)
|
|
396
410
|
return data;
|
|
397
411
|
const result = { ...data };
|
|
398
412
|
// Flatten constants into context
|
|
@@ -405,7 +419,7 @@ export function applyCalculations(calculations, constants, data) {
|
|
|
405
419
|
// Multi-pass (max 3) for inter-formula dependencies
|
|
406
420
|
for (let pass = 0; pass < 3; pass++) {
|
|
407
421
|
let changed = false;
|
|
408
|
-
for (const calc of
|
|
422
|
+
for (const calc of normalized) {
|
|
409
423
|
try {
|
|
410
424
|
const val = safeEval(calc.formula, ctx);
|
|
411
425
|
const rounded = parseFloat(val.toFixed(calc.decimals ?? 3));
|
|
@@ -433,7 +447,7 @@ function tokenizeBool(expr) {
|
|
|
433
447
|
i++;
|
|
434
448
|
continue;
|
|
435
449
|
}
|
|
436
|
-
if (/[0-9
|
|
450
|
+
if (/[0-9]/.test(ch) || (ch === "." && i + 1 < expr.length && /[0-9]/.test(expr[i + 1]))) {
|
|
437
451
|
let num = "";
|
|
438
452
|
while (i < expr.length && /[0-9.]/.test(expr[i])) {
|
|
439
453
|
num += expr[i];
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type Anthropic from "@anthropic-ai/sdk";
|
|
12
12
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
13
|
+
import { type ContextManagementOverrides } from "../../shared/agent-core.js";
|
|
13
14
|
import type { ToolChoice } from "../../shared/agent-core.js";
|
|
14
15
|
import type { CitationBlock } from "../../shared/types.js";
|
|
15
16
|
import type { ToolProgressCallback } from "../tool-router.js";
|
|
@@ -51,6 +52,7 @@ export interface ServerAgentLoopOptions {
|
|
|
51
52
|
enableDelegation?: boolean;
|
|
52
53
|
enablePromptCaching?: boolean;
|
|
53
54
|
enableStreaming?: boolean;
|
|
55
|
+
enableModelRouting?: boolean;
|
|
54
56
|
maxConcurrentTools?: number;
|
|
55
57
|
documents?: any[];
|
|
56
58
|
onText?: (text: string) => void;
|
|
@@ -58,6 +60,9 @@ export interface ServerAgentLoopOptions {
|
|
|
58
60
|
onToolResult?: (name: string, success: boolean, result: unknown) => void;
|
|
59
61
|
onCitation?: (citation: CitationBlock) => void;
|
|
60
62
|
onSubagentProgress?: SubagentProgressCallback;
|
|
63
|
+
subagentMaxTokens?: number;
|
|
64
|
+
subagentMaxTurns?: number;
|
|
65
|
+
subagentTemperature?: number;
|
|
61
66
|
maxCostUsd?: number;
|
|
62
67
|
clientDisconnected?: {
|
|
63
68
|
value: boolean;
|
|
@@ -65,6 +70,7 @@ export interface ServerAgentLoopOptions {
|
|
|
65
70
|
startedAt?: number;
|
|
66
71
|
maxDurationMs?: number;
|
|
67
72
|
requiredCapabilities?: RequestCapabilityRequirements;
|
|
73
|
+
contextOverrides?: ContextManagementOverrides;
|
|
68
74
|
}
|
|
69
75
|
export interface TurnMetrics {
|
|
70
76
|
turn: number;
|