@superatomai/sdk-node 0.0.50-dsp → 0.0.51-dsp
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/index.d.mts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +155 -34
- package/dist/index.mjs +155 -34
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -291,6 +291,14 @@ type MatchTier = 'high' | 'near' | 'edit' | 'none';
|
|
|
291
291
|
*/
|
|
292
292
|
interface ScriptBinding {
|
|
293
293
|
recipeId: string;
|
|
294
|
+
/**
|
|
295
|
+
* The recipe's own version number at the moment THIS turn ran — not
|
|
296
|
+
* necessarily the recipe's current version. A later edit bumps the live
|
|
297
|
+
* version, so without this a past turn's binding would silently point at
|
|
298
|
+
* whatever the recipe looks like today instead of what actually produced
|
|
299
|
+
* that answer. Absent on bindings created before this field existed.
|
|
300
|
+
*/
|
|
301
|
+
version?: number;
|
|
294
302
|
/** Params the script ran with — the edit's starting point. */
|
|
295
303
|
params: Record<string, any>;
|
|
296
304
|
/** Recipe name at bind time (matcher catalog + user-facing confirmation). */
|
|
@@ -2548,6 +2556,18 @@ declare class ScriptStore {
|
|
|
2548
2556
|
search(prompt: string, limit?: number): Promise<ScriptRecipe[]>;
|
|
2549
2557
|
/** Fetch one recipe by id with its body loaded from disk. */
|
|
2550
2558
|
get(id: string): Promise<ScriptRecipe | null>;
|
|
2559
|
+
/**
|
|
2560
|
+
* Fetch one recipe by id, pinned to a SPECIFIC past version rather than
|
|
2561
|
+
* whatever is live today. Needed to render a past turn's conversation
|
|
2562
|
+
* history accurately after the script has since been edited — get() alone
|
|
2563
|
+
* always returns the CURRENT body, which may no longer be what that turn
|
|
2564
|
+
* actually ran.
|
|
2565
|
+
*
|
|
2566
|
+
* Returns null (never a silently-wrong version) when the requested version
|
|
2567
|
+
* isn't the current one and its archived body can't be found — e.g. the
|
|
2568
|
+
* archive was cleaned up after the whole recipe was later deleted.
|
|
2569
|
+
*/
|
|
2570
|
+
getVersion(id: string, version: number): Promise<ScriptRecipe | null>;
|
|
2551
2571
|
/** Create or update a recipe (metadata upsert + body write when changed). */
|
|
2552
2572
|
save(recipe: ScriptRecipe): Promise<void>;
|
|
2553
2573
|
/**
|
|
@@ -3470,7 +3490,14 @@ declare class Thread {
|
|
|
3470
3490
|
* See backend/docs/SCRIPT-EDIT-DESIGN.md § Component 1.
|
|
3471
3491
|
*/
|
|
3472
3492
|
getScriptBindings(limit?: number, currentUIBlockId?: string): Record<string, any>[];
|
|
3473
|
-
|
|
3493
|
+
/**
|
|
3494
|
+
* @param getScriptBody Resolves a script-backed turn's EXACT script, pinned
|
|
3495
|
+
* to the version that turn actually ran — never today's (possibly
|
|
3496
|
+
* since-edited) live body. Owned by the caller (needs a ScriptStore, which
|
|
3497
|
+
* Thread doesn't hold) — omit it and every turn falls back to the
|
|
3498
|
+
* no-script summary below.
|
|
3499
|
+
*/
|
|
3500
|
+
getConversationContext(limit?: number, currentUIBlockId?: string, getScriptBody?: (recipeId: string, version: number) => Promise<string | null>): Promise<string>;
|
|
3474
3501
|
/**
|
|
3475
3502
|
* Convert Thread to JSON-serializable object
|
|
3476
3503
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -291,6 +291,14 @@ type MatchTier = 'high' | 'near' | 'edit' | 'none';
|
|
|
291
291
|
*/
|
|
292
292
|
interface ScriptBinding {
|
|
293
293
|
recipeId: string;
|
|
294
|
+
/**
|
|
295
|
+
* The recipe's own version number at the moment THIS turn ran — not
|
|
296
|
+
* necessarily the recipe's current version. A later edit bumps the live
|
|
297
|
+
* version, so without this a past turn's binding would silently point at
|
|
298
|
+
* whatever the recipe looks like today instead of what actually produced
|
|
299
|
+
* that answer. Absent on bindings created before this field existed.
|
|
300
|
+
*/
|
|
301
|
+
version?: number;
|
|
294
302
|
/** Params the script ran with — the edit's starting point. */
|
|
295
303
|
params: Record<string, any>;
|
|
296
304
|
/** Recipe name at bind time (matcher catalog + user-facing confirmation). */
|
|
@@ -2548,6 +2556,18 @@ declare class ScriptStore {
|
|
|
2548
2556
|
search(prompt: string, limit?: number): Promise<ScriptRecipe[]>;
|
|
2549
2557
|
/** Fetch one recipe by id with its body loaded from disk. */
|
|
2550
2558
|
get(id: string): Promise<ScriptRecipe | null>;
|
|
2559
|
+
/**
|
|
2560
|
+
* Fetch one recipe by id, pinned to a SPECIFIC past version rather than
|
|
2561
|
+
* whatever is live today. Needed to render a past turn's conversation
|
|
2562
|
+
* history accurately after the script has since been edited — get() alone
|
|
2563
|
+
* always returns the CURRENT body, which may no longer be what that turn
|
|
2564
|
+
* actually ran.
|
|
2565
|
+
*
|
|
2566
|
+
* Returns null (never a silently-wrong version) when the requested version
|
|
2567
|
+
* isn't the current one and its archived body can't be found — e.g. the
|
|
2568
|
+
* archive was cleaned up after the whole recipe was later deleted.
|
|
2569
|
+
*/
|
|
2570
|
+
getVersion(id: string, version: number): Promise<ScriptRecipe | null>;
|
|
2551
2571
|
/** Create or update a recipe (metadata upsert + body write when changed). */
|
|
2552
2572
|
save(recipe: ScriptRecipe): Promise<void>;
|
|
2553
2573
|
/**
|
|
@@ -3470,7 +3490,14 @@ declare class Thread {
|
|
|
3470
3490
|
* See backend/docs/SCRIPT-EDIT-DESIGN.md § Component 1.
|
|
3471
3491
|
*/
|
|
3472
3492
|
getScriptBindings(limit?: number, currentUIBlockId?: string): Record<string, any>[];
|
|
3473
|
-
|
|
3493
|
+
/**
|
|
3494
|
+
* @param getScriptBody Resolves a script-backed turn's EXACT script, pinned
|
|
3495
|
+
* to the version that turn actually ran — never today's (possibly
|
|
3496
|
+
* since-edited) live body. Owned by the caller (needs a ScriptStore, which
|
|
3497
|
+
* Thread doesn't hold) — omit it and every turn falls back to the
|
|
3498
|
+
* no-script summary below.
|
|
3499
|
+
*/
|
|
3500
|
+
getConversationContext(limit?: number, currentUIBlockId?: string, getScriptBody?: (recipeId: string, version: number) => Promise<string | null>): Promise<string>;
|
|
3474
3501
|
/**
|
|
3475
3502
|
* Convert Thread to JSON-serializable object
|
|
3476
3503
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1502,7 +1502,14 @@ var Thread = class {
|
|
|
1502
1502
|
}
|
|
1503
1503
|
return out;
|
|
1504
1504
|
}
|
|
1505
|
-
|
|
1505
|
+
/**
|
|
1506
|
+
* @param getScriptBody Resolves a script-backed turn's EXACT script, pinned
|
|
1507
|
+
* to the version that turn actually ran — never today's (possibly
|
|
1508
|
+
* since-edited) live body. Owned by the caller (needs a ScriptStore, which
|
|
1509
|
+
* Thread doesn't hold) — omit it and every turn falls back to the
|
|
1510
|
+
* no-script summary below.
|
|
1511
|
+
*/
|
|
1512
|
+
async getConversationContext(limit = 5, currentUIBlockId, getScriptBody) {
|
|
1506
1513
|
if (limit === 0) {
|
|
1507
1514
|
return "";
|
|
1508
1515
|
}
|
|
@@ -1512,53 +1519,62 @@ var Thread = class {
|
|
|
1512
1519
|
}
|
|
1513
1520
|
const recentBlocks = allBlocks.slice(-limit);
|
|
1514
1521
|
const contextLines = [];
|
|
1515
|
-
|
|
1516
|
-
const questionNum = index + 1;
|
|
1522
|
+
for (const block of recentBlocks) {
|
|
1517
1523
|
const question = block.getUserQuestion();
|
|
1518
1524
|
const metadata = block.getComponentMetadata();
|
|
1519
1525
|
const programAnswer = block.getProgramAnswer();
|
|
1520
1526
|
const textResponse = block.getTextResponse();
|
|
1521
|
-
|
|
1527
|
+
const scriptBinding = block.getScriptBinding();
|
|
1522
1528
|
const responseParts = [];
|
|
1529
|
+
let analysisText = "";
|
|
1523
1530
|
const hasProgramAnswer = programAnswer && programAnswer.trim().length > 0;
|
|
1524
1531
|
if (hasProgramAnswer) {
|
|
1525
1532
|
const trimmed = programAnswer.trim();
|
|
1526
|
-
|
|
1533
|
+
analysisText = truncateKeepingMethodLine(trimmed, 2e3);
|
|
1527
1534
|
} else {
|
|
1528
1535
|
const hasTextResponse = textResponse && textResponse.trim().length > 0;
|
|
1529
1536
|
if (hasTextResponse) {
|
|
1530
1537
|
const cleaned = cleanAssistantText(textResponse);
|
|
1531
1538
|
if (cleaned) {
|
|
1532
|
-
|
|
1533
|
-
responseParts.push(trimmedText);
|
|
1539
|
+
analysisText = cleaned.length > 800 ? cleaned.substring(0, 800) + "...[truncated]" : cleaned;
|
|
1534
1540
|
}
|
|
1535
1541
|
}
|
|
1536
1542
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
if (summary) responseParts.push(` Components: ${summary}`);
|
|
1543
|
+
if (analysisText) responseParts.push(analysisText);
|
|
1544
|
+
let scriptAttached = false;
|
|
1545
|
+
if (scriptBinding?.recipeId && scriptBinding.version !== void 0 && getScriptBody) {
|
|
1546
|
+
try {
|
|
1547
|
+
const body = await getScriptBody(scriptBinding.recipeId, scriptBinding.version);
|
|
1548
|
+
if (body) {
|
|
1549
|
+
responseParts.push(`Script used:
|
|
1550
|
+
${body}`);
|
|
1551
|
+
scriptAttached = true;
|
|
1552
|
+
}
|
|
1553
|
+
} catch {
|
|
1549
1554
|
}
|
|
1550
1555
|
}
|
|
1551
|
-
if (
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1556
|
+
if (!scriptAttached) {
|
|
1557
|
+
if (scriptBinding?.params && Object.keys(scriptBinding.params).length > 0) {
|
|
1558
|
+
responseParts.push(`Parameters used: ${JSON.stringify(scriptBinding.params)}`);
|
|
1559
|
+
}
|
|
1560
|
+
const hasComponent = metadata && Object.keys(metadata).length > 0 && metadata.type;
|
|
1561
|
+
if (hasComponent) {
|
|
1562
|
+
const label = metadata.name ? `${metadata.type} \u2014 ${metadata.name}` : metadata.type;
|
|
1563
|
+
responseParts.push(`[Rendered: ${label}]`);
|
|
1564
|
+
const innerComponents = metadata.props?.config?.components;
|
|
1565
|
+
if (Array.isArray(innerComponents) && innerComponents.length > 0) {
|
|
1566
|
+
const summary = innerComponents.map((c) => c?.props?.title ? `${c.name} ("${c.props.title}")` : c?.name).filter(Boolean).join(", ");
|
|
1567
|
+
if (summary) responseParts.push(` Components: ${summary}`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1555
1570
|
}
|
|
1571
|
+
const assistantResponse = responseParts.length > 0 ? responseParts.join("\n") : "No response generated";
|
|
1556
1572
|
contextLines.push(`User:
|
|
1557
1573
|
${question}`);
|
|
1558
1574
|
contextLines.push(`Assistant:
|
|
1559
1575
|
${assistantResponse}`);
|
|
1560
1576
|
contextLines.push("---");
|
|
1561
|
-
}
|
|
1577
|
+
}
|
|
1562
1578
|
return contextLines.join("\n").trim();
|
|
1563
1579
|
}
|
|
1564
1580
|
/**
|
|
@@ -10232,16 +10248,20 @@ ${formatted}`;
|
|
|
10232
10248
|
} catch (execError) {
|
|
10233
10249
|
const errorMsg = execError instanceof Error ? execError.message : typeof execError === "object" && execError !== null ? execError.message || execError.error || JSON.stringify(execError) : String(execError);
|
|
10234
10250
|
logger.warn(`[SourceAgent:${this.tool.name}] Tool execution failed (attempt ${this.attempts}/${this.config.maxRetries}): ${errorMsg}`);
|
|
10251
|
+
let hint = "";
|
|
10252
|
+
if (this.extractSourceType() === "netsuite" && /"o:errorCode"\s*:\s*"UNEXPECTED_ERROR"/.test(errorMsg)) {
|
|
10253
|
+
hint = ` This gives no column/syntax detail \u2014 it usually means a column that doesn't exist on that table (wrong field name), or a column referenced outside the CTE that selects it (a CTE only exposes columns it projects). Recheck every column name against the schema.`;
|
|
10254
|
+
}
|
|
10235
10255
|
if (this.streamBuffer.hasCallback()) {
|
|
10236
10256
|
const querySeconds = ((Date.now() - queryStartTime) / 1e3).toFixed(1);
|
|
10237
10257
|
this.streamBuffer.write(`__QUERY_TIMER_DONE_${querySeconds}__
|
|
10238
10258
|
|
|
10239
10259
|
`);
|
|
10240
|
-
this.streamBuffer.write(`\u274C **Query failed:** ${errorMsg}
|
|
10260
|
+
this.streamBuffer.write(`\u274C **Query failed:** ${errorMsg}${hint}
|
|
10241
10261
|
|
|
10242
10262
|
`);
|
|
10243
10263
|
}
|
|
10244
|
-
return `\u274C ERROR: ${errorMsg}
|
|
10264
|
+
return `\u274C ERROR: ${errorMsg}${hint}
|
|
10245
10265
|
|
|
10246
10266
|
Analyze the error and try again with a corrected query.`;
|
|
10247
10267
|
}
|
|
@@ -11254,13 +11274,34 @@ function findHardcodedComponentText(scriptBody) {
|
|
|
11254
11274
|
}
|
|
11255
11275
|
return violations;
|
|
11256
11276
|
}
|
|
11277
|
+
function findHardcodedEntityNames(source, entityDisplayNames) {
|
|
11278
|
+
const names = entityDisplayNames.filter((n) => n && n.trim().length >= 2);
|
|
11279
|
+
if (!names.length) return [];
|
|
11280
|
+
const violations = [];
|
|
11281
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11282
|
+
for (const chunk of literalChunks(source)) {
|
|
11283
|
+
const trimmed = chunk.trim();
|
|
11284
|
+
if (!trimmed) continue;
|
|
11285
|
+
for (const name of names) {
|
|
11286
|
+
if (seen.has(name)) continue;
|
|
11287
|
+
if (trimmed.toLowerCase().includes(name.toLowerCase())) {
|
|
11288
|
+
seen.add(name);
|
|
11289
|
+
violations.push({
|
|
11290
|
+
text: trimmed.slice(0, 120),
|
|
11291
|
+
reason: `"${name}" \u2014 a name resolved via resolve_entities this turn \u2014 is written as fixed text. Read it from \`data\`/\`datasets\` instead (e.g. \`data[0]?.<name_column>\`), or filter/join on the resolved id (\`params.<name>Id\`) rather than the name string \u2014 a name typed as a literal is exactly as wrong on replay as a number typed as a literal: the id a future run resolves will differ, but the printed/filtered name will not.`
|
|
11292
|
+
});
|
|
11293
|
+
}
|
|
11294
|
+
}
|
|
11295
|
+
}
|
|
11296
|
+
return violations;
|
|
11297
|
+
}
|
|
11257
11298
|
function formatLintError(violations) {
|
|
11258
11299
|
if (!violations.length) return null;
|
|
11259
11300
|
const lines = violations.map((v) => ` \u2022 "${v.text}" \u2014 ${v.reason}`).join("\n");
|
|
11260
|
-
return `The analysis body hardcodes ${violations.length}
|
|
11301
|
+
return `The analysis body hardcodes ${violations.length} value${violations.length > 1 ? "s" : ""} as text:
|
|
11261
11302
|
${lines}
|
|
11262
11303
|
|
|
11263
|
-
FIX: every figure must come from the data via a fmt helper \u2014 fmt.money, fmt.num, fmt.pct, fmt.delta, fmt.count, fmt.date, fmt.range. A
|
|
11304
|
+
FIX: every figure must come from the data via a fmt helper \u2014 fmt.money, fmt.num, fmt.pct, fmt.delta, fmt.count, fmt.date, fmt.range \u2014 and every resolved name must come from \`data\`/\`datasets\`, not be typed in. A value typed into a string is frozen at authoring time and will be wrong on replay when the data or params change. Rewording will not clear this \u2014 only moving the value out of the string will. A constant the code itself uses is fine once interpolated from a variable; a figure or name read off the data is not.`;
|
|
11264
11305
|
}
|
|
11265
11306
|
|
|
11266
11307
|
// src/userResponse/scripts/component-validator.ts
|
|
@@ -12031,7 +12072,8 @@ ${catalog}`,
|
|
|
12031
12072
|
this.renderComponentAttempts++;
|
|
12032
12073
|
const hardcodedAggregates = findHardcodedAggregates(body);
|
|
12033
12074
|
const hardcodedComponentText = findHardcodedComponentText(body);
|
|
12034
|
-
const
|
|
12075
|
+
const hardcodedEntityNames = findHardcodedEntityNames(body, this.resolvedEntities.map((e) => e.displayName));
|
|
12076
|
+
const componentViolations = [...hardcodedAggregates, ...hardcodedComponentText, ...hardcodedEntityNames];
|
|
12035
12077
|
if (componentViolations.length > 0) {
|
|
12036
12078
|
const named = componentViolations.map((v) => `"${v.text}"`).join(", ");
|
|
12037
12079
|
const repeatedTexts = componentViolations.map((v) => v.text).filter((t) => this.lastComponentsViolationTexts.includes(t));
|
|
@@ -12114,7 +12156,10 @@ Now write your answer for the user. Do not restate the component list.`;
|
|
|
12114
12156
|
return `Reached the max of ${MAX_ANALYSIS_ATTEMPTS} write_analysis attempts. Write your final analysis as prose instead, using the values from the script result exactly as shown.`;
|
|
12115
12157
|
}
|
|
12116
12158
|
this.scriptState.analysisAttempts++;
|
|
12117
|
-
const violations =
|
|
12159
|
+
const violations = [
|
|
12160
|
+
...lintAnalysisBody(body),
|
|
12161
|
+
...findHardcodedEntityNames(body, this.resolvedEntities.map((e) => e.displayName))
|
|
12162
|
+
];
|
|
12118
12163
|
const repeatedTexts = violations.map((v) => v.text).filter((t) => this.scriptState.lastAnalysisViolationTexts.includes(t));
|
|
12119
12164
|
this.scriptState.lastAnalysisViolationTexts = violations.map((v) => v.text);
|
|
12120
12165
|
const lintError = formatLintError(violations);
|
|
@@ -12192,6 +12237,14 @@ FIX: derive every such value from the data itself \u2014 \`data.length\`, a real
|
|
|
12192
12237
|
${lines}
|
|
12193
12238
|
|
|
12194
12239
|
FIX: build the title/description with a template literal that reads the value from \`data\`/\`datasets\` at render time (e.g. \` \\\`Total: \\\${fmt.money(datasets.summary[0].total)}\\\` \`), never a number typed directly into the string.`;
|
|
12240
|
+
}
|
|
12241
|
+
const hardcodedEntityNames = findHardcodedEntityNames(scriptBody, this.resolvedEntities.map((e) => e.displayName));
|
|
12242
|
+
if (hardcodedEntityNames.length > 0) {
|
|
12243
|
+
const lines = hardcodedEntityNames.map((v) => ` \u2022 "${v.text}" \u2014 ${v.reason}`).join("\n");
|
|
12244
|
+
return `write_script rejected: the script hardcodes ${hardcodedEntityNames.length} resolved entity name(s) as fixed text:
|
|
12245
|
+
${lines}
|
|
12246
|
+
|
|
12247
|
+
FIX: read the name from \`data\`/\`datasets\` at render time, or filter/join on the resolved id (\`params.<name>Id\`) instead of the name string.`;
|
|
12195
12248
|
}
|
|
12196
12249
|
const draft = await this.scriptStore.saveDraft({
|
|
12197
12250
|
recipeId: this.scriptState.recipeId ?? void 0,
|
|
@@ -13397,6 +13450,22 @@ function stripNamedExport(body, fnName) {
|
|
|
13397
13450
|
}
|
|
13398
13451
|
return body.slice(0, start);
|
|
13399
13452
|
}
|
|
13453
|
+
function extractNamedExport(body, fnName) {
|
|
13454
|
+
const start = body.search(new RegExp(`export\\s+(?:async\\s+)?function\\s+${fnName}\\b`));
|
|
13455
|
+
if (start === -1) return null;
|
|
13456
|
+
const open = body.indexOf("{", start);
|
|
13457
|
+
if (open === -1) return null;
|
|
13458
|
+
let depth = 0;
|
|
13459
|
+
for (let i = open; i < body.length; i++) {
|
|
13460
|
+
const c = body[i];
|
|
13461
|
+
if (c === "{") depth++;
|
|
13462
|
+
else if (c === "}") {
|
|
13463
|
+
depth--;
|
|
13464
|
+
if (depth === 0) return body.slice(start, i + 1);
|
|
13465
|
+
}
|
|
13466
|
+
}
|
|
13467
|
+
return null;
|
|
13468
|
+
}
|
|
13400
13469
|
function normalizeScriptBody(scriptBody) {
|
|
13401
13470
|
let i = 0;
|
|
13402
13471
|
const n = scriptBody.length;
|
|
@@ -13496,6 +13565,37 @@ var ScriptStore = class {
|
|
|
13496
13565
|
return null;
|
|
13497
13566
|
}
|
|
13498
13567
|
}
|
|
13568
|
+
/**
|
|
13569
|
+
* Fetch one recipe by id, pinned to a SPECIFIC past version rather than
|
|
13570
|
+
* whatever is live today. Needed to render a past turn's conversation
|
|
13571
|
+
* history accurately after the script has since been edited — get() alone
|
|
13572
|
+
* always returns the CURRENT body, which may no longer be what that turn
|
|
13573
|
+
* actually ran.
|
|
13574
|
+
*
|
|
13575
|
+
* Returns null (never a silently-wrong version) when the requested version
|
|
13576
|
+
* isn't the current one and its archived body can't be found — e.g. the
|
|
13577
|
+
* archive was cleaned up after the whole recipe was later deleted.
|
|
13578
|
+
*/
|
|
13579
|
+
async getVersion(id, version) {
|
|
13580
|
+
if (!this.store) return null;
|
|
13581
|
+
try {
|
|
13582
|
+
const row = await this.store.getById(id);
|
|
13583
|
+
if (!row) return null;
|
|
13584
|
+
if (version === row.version) {
|
|
13585
|
+
return this.rowToRecipe(row, this.readBody(row.fileBase));
|
|
13586
|
+
}
|
|
13587
|
+
const archivePath = path6.join(this.archiveDir, `${row.fileBase}.v${version}.ts`);
|
|
13588
|
+
if (!fs6.existsSync(archivePath)) {
|
|
13589
|
+
logger.warn(`[ScriptStore] getVersion(${id}, v${version}) \u2014 archived body not found, recipe is now at v${row.version}`);
|
|
13590
|
+
return null;
|
|
13591
|
+
}
|
|
13592
|
+
const body = fs6.readFileSync(archivePath, "utf-8");
|
|
13593
|
+
return this.rowToRecipe(row, body);
|
|
13594
|
+
} catch (err) {
|
|
13595
|
+
logger.warn(`[ScriptStore] getVersion(${id}, v${version}) failed: ${err}`);
|
|
13596
|
+
return null;
|
|
13597
|
+
}
|
|
13598
|
+
}
|
|
13499
13599
|
// ============================================
|
|
13500
13600
|
// Write
|
|
13501
13601
|
// ============================================
|
|
@@ -13535,7 +13635,20 @@ var ScriptStore = class {
|
|
|
13535
13635
|
const reuse = existing && existing.status === "draft" && existing.turnId === input.turnId;
|
|
13536
13636
|
const id = reuse ? existing.id : `script_${Date.now()}_${(0, import_crypto5.randomBytes)(3).toString("hex")}`;
|
|
13537
13637
|
const fileBase = reuse ? existing.fileBase : input.parentId ? `${this.toSlug(input.name)}-${id.slice(-6)}` : await this.computeFileBase(input.name, id);
|
|
13538
|
-
|
|
13638
|
+
let normalized = normalizeScriptBody(input.scriptBody);
|
|
13639
|
+
if (reuse) {
|
|
13640
|
+
const existingBody = this.readBody(fileBase);
|
|
13641
|
+
if (existingBody) {
|
|
13642
|
+
for (const fnName of ["getComponents", "getAnalysis"]) {
|
|
13643
|
+
if (new RegExp(`export\\s+(?:async\\s+)?function\\s+${fnName}\\b`).test(normalized)) continue;
|
|
13644
|
+
const carried = extractNamedExport(existingBody, fnName);
|
|
13645
|
+
if (carried) normalized = `${normalized.trimEnd()}
|
|
13646
|
+
|
|
13647
|
+
${carried.trim()}
|
|
13648
|
+
`;
|
|
13649
|
+
}
|
|
13650
|
+
}
|
|
13651
|
+
}
|
|
13539
13652
|
const bodyHash = this.hash(normalized);
|
|
13540
13653
|
this.writeBody(fileBase, normalized);
|
|
13541
13654
|
const recipe = {
|
|
@@ -17785,6 +17898,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${scriptMatch
|
|
|
17785
17898
|
// ("use mode instead") can edit THIS script.
|
|
17786
17899
|
scriptBinding: {
|
|
17787
17900
|
recipeId: scriptMatch.recipe.id,
|
|
17901
|
+
version: scriptMatch.recipe.version,
|
|
17788
17902
|
params: scriptMatch.extractedParams || {},
|
|
17789
17903
|
name: scriptMatch.recipe.name,
|
|
17790
17904
|
columns: scriptResult.executedQueries?.[0]?.data?.[0] ? Object.keys(scriptResult.executedQueries[0].data[0]) : void 0
|
|
@@ -18212,6 +18326,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${carriedRequ
|
|
|
18212
18326
|
if (committed) {
|
|
18213
18327
|
scriptBinding = {
|
|
18214
18328
|
recipeId: committed.id,
|
|
18329
|
+
version: committed.version,
|
|
18215
18330
|
params: {},
|
|
18216
18331
|
name: committed.name,
|
|
18217
18332
|
columns: authored.executedQueries?.[0]?.data?.[0] ? Object.keys(authored.executedQueries[0].data[0]) : void 0
|
|
@@ -18269,6 +18384,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${carriedRequ
|
|
|
18269
18384
|
}
|
|
18270
18385
|
scriptBinding = {
|
|
18271
18386
|
recipeId: authored.recipeId,
|
|
18387
|
+
version: promoted?.version,
|
|
18272
18388
|
params: {},
|
|
18273
18389
|
name: authored.name,
|
|
18274
18390
|
columns: authored.executedQueries?.[0]?.data?.[0] ? Object.keys(authored.executedQueries[0].data[0]) : void 0
|
|
@@ -18587,7 +18703,7 @@ var CONTEXT_CONFIG = {
|
|
|
18587
18703
|
* Set to 0 to disable conversation history
|
|
18588
18704
|
* Higher values provide more context but may increase token usage
|
|
18589
18705
|
*/
|
|
18590
|
-
MAX_CONVERSATION_CONTEXT_BLOCKS:
|
|
18706
|
+
MAX_CONVERSATION_CONTEXT_BLOCKS: 2
|
|
18591
18707
|
};
|
|
18592
18708
|
|
|
18593
18709
|
// src/handlers/user-prompt-request.ts
|
|
@@ -18685,7 +18801,12 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
|
|
|
18685
18801
|
}
|
|
18686
18802
|
const requestStartTime = Date.now();
|
|
18687
18803
|
logger.info(`Starting user prompt request with ${components.length} components`);
|
|
18688
|
-
const
|
|
18804
|
+
const conversationScriptStore = new ScriptStore({ collections, projectId: process.env.SUPERATOM_PROJECT_ID || "" });
|
|
18805
|
+
const conversationHistory = await thread.getConversationContext(
|
|
18806
|
+
CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS,
|
|
18807
|
+
existingUiBlockId,
|
|
18808
|
+
async (recipeId, version) => (await conversationScriptStore.getVersion(recipeId, version))?.scriptBody ?? null
|
|
18809
|
+
);
|
|
18689
18810
|
const MAX_EDIT_CANDIDATES = 5;
|
|
18690
18811
|
let activeScriptBindings = thread.getScriptBindings(MAX_EDIT_CANDIDATES, existingUiBlockId);
|
|
18691
18812
|
if (activeScriptBindings.length === 0 && collections?.["conversation-history"]?.["threadScriptBindings"]) {
|
|
@@ -19333,7 +19454,7 @@ async function handleActionsRequest(data, sendMessage, anthropicApiKey, groqApiK
|
|
|
19333
19454
|
logger.debug(`[ACTIONS_REQ ${id}] Component: ${component?.name || "unknown"} (${component?.type || "unknown"})`);
|
|
19334
19455
|
logger.debug(`[ACTIONS_REQ ${id}] Component data available: ${componentData ? "yes" : "no"}`);
|
|
19335
19456
|
logger.debug(`[ACTIONS_REQ ${id}] Extracting conversation history (max ${CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS} blocks)`);
|
|
19336
|
-
const conversationHistory = thread.getConversationContext(CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS, uiBlockId);
|
|
19457
|
+
const conversationHistory = await thread.getConversationContext(CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS, uiBlockId);
|
|
19337
19458
|
const historyLineCount = conversationHistory.split("\n").filter((l) => l.trim()).length;
|
|
19338
19459
|
logger.info(`[ACTIONS_REQ ${id}] Conversation history extracted: ${historyLineCount} lines`);
|
|
19339
19460
|
logger.debug(`[ACTIONS_REQ ${id}] Conversation history preview:
|
package/dist/index.mjs
CHANGED
|
@@ -1443,7 +1443,14 @@ var Thread = class {
|
|
|
1443
1443
|
}
|
|
1444
1444
|
return out;
|
|
1445
1445
|
}
|
|
1446
|
-
|
|
1446
|
+
/**
|
|
1447
|
+
* @param getScriptBody Resolves a script-backed turn's EXACT script, pinned
|
|
1448
|
+
* to the version that turn actually ran — never today's (possibly
|
|
1449
|
+
* since-edited) live body. Owned by the caller (needs a ScriptStore, which
|
|
1450
|
+
* Thread doesn't hold) — omit it and every turn falls back to the
|
|
1451
|
+
* no-script summary below.
|
|
1452
|
+
*/
|
|
1453
|
+
async getConversationContext(limit = 5, currentUIBlockId, getScriptBody) {
|
|
1447
1454
|
if (limit === 0) {
|
|
1448
1455
|
return "";
|
|
1449
1456
|
}
|
|
@@ -1453,53 +1460,62 @@ var Thread = class {
|
|
|
1453
1460
|
}
|
|
1454
1461
|
const recentBlocks = allBlocks.slice(-limit);
|
|
1455
1462
|
const contextLines = [];
|
|
1456
|
-
|
|
1457
|
-
const questionNum = index + 1;
|
|
1463
|
+
for (const block of recentBlocks) {
|
|
1458
1464
|
const question = block.getUserQuestion();
|
|
1459
1465
|
const metadata = block.getComponentMetadata();
|
|
1460
1466
|
const programAnswer = block.getProgramAnswer();
|
|
1461
1467
|
const textResponse = block.getTextResponse();
|
|
1462
|
-
|
|
1468
|
+
const scriptBinding = block.getScriptBinding();
|
|
1463
1469
|
const responseParts = [];
|
|
1470
|
+
let analysisText = "";
|
|
1464
1471
|
const hasProgramAnswer = programAnswer && programAnswer.trim().length > 0;
|
|
1465
1472
|
if (hasProgramAnswer) {
|
|
1466
1473
|
const trimmed = programAnswer.trim();
|
|
1467
|
-
|
|
1474
|
+
analysisText = truncateKeepingMethodLine(trimmed, 2e3);
|
|
1468
1475
|
} else {
|
|
1469
1476
|
const hasTextResponse = textResponse && textResponse.trim().length > 0;
|
|
1470
1477
|
if (hasTextResponse) {
|
|
1471
1478
|
const cleaned = cleanAssistantText(textResponse);
|
|
1472
1479
|
if (cleaned) {
|
|
1473
|
-
|
|
1474
|
-
responseParts.push(trimmedText);
|
|
1480
|
+
analysisText = cleaned.length > 800 ? cleaned.substring(0, 800) + "...[truncated]" : cleaned;
|
|
1475
1481
|
}
|
|
1476
1482
|
}
|
|
1477
1483
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
if (summary) responseParts.push(` Components: ${summary}`);
|
|
1484
|
+
if (analysisText) responseParts.push(analysisText);
|
|
1485
|
+
let scriptAttached = false;
|
|
1486
|
+
if (scriptBinding?.recipeId && scriptBinding.version !== void 0 && getScriptBody) {
|
|
1487
|
+
try {
|
|
1488
|
+
const body = await getScriptBody(scriptBinding.recipeId, scriptBinding.version);
|
|
1489
|
+
if (body) {
|
|
1490
|
+
responseParts.push(`Script used:
|
|
1491
|
+
${body}`);
|
|
1492
|
+
scriptAttached = true;
|
|
1493
|
+
}
|
|
1494
|
+
} catch {
|
|
1490
1495
|
}
|
|
1491
1496
|
}
|
|
1492
|
-
if (
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1497
|
+
if (!scriptAttached) {
|
|
1498
|
+
if (scriptBinding?.params && Object.keys(scriptBinding.params).length > 0) {
|
|
1499
|
+
responseParts.push(`Parameters used: ${JSON.stringify(scriptBinding.params)}`);
|
|
1500
|
+
}
|
|
1501
|
+
const hasComponent = metadata && Object.keys(metadata).length > 0 && metadata.type;
|
|
1502
|
+
if (hasComponent) {
|
|
1503
|
+
const label = metadata.name ? `${metadata.type} \u2014 ${metadata.name}` : metadata.type;
|
|
1504
|
+
responseParts.push(`[Rendered: ${label}]`);
|
|
1505
|
+
const innerComponents = metadata.props?.config?.components;
|
|
1506
|
+
if (Array.isArray(innerComponents) && innerComponents.length > 0) {
|
|
1507
|
+
const summary = innerComponents.map((c) => c?.props?.title ? `${c.name} ("${c.props.title}")` : c?.name).filter(Boolean).join(", ");
|
|
1508
|
+
if (summary) responseParts.push(` Components: ${summary}`);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1496
1511
|
}
|
|
1512
|
+
const assistantResponse = responseParts.length > 0 ? responseParts.join("\n") : "No response generated";
|
|
1497
1513
|
contextLines.push(`User:
|
|
1498
1514
|
${question}`);
|
|
1499
1515
|
contextLines.push(`Assistant:
|
|
1500
1516
|
${assistantResponse}`);
|
|
1501
1517
|
contextLines.push("---");
|
|
1502
|
-
}
|
|
1518
|
+
}
|
|
1503
1519
|
return contextLines.join("\n").trim();
|
|
1504
1520
|
}
|
|
1505
1521
|
/**
|
|
@@ -10173,16 +10189,20 @@ ${formatted}`;
|
|
|
10173
10189
|
} catch (execError) {
|
|
10174
10190
|
const errorMsg = execError instanceof Error ? execError.message : typeof execError === "object" && execError !== null ? execError.message || execError.error || JSON.stringify(execError) : String(execError);
|
|
10175
10191
|
logger.warn(`[SourceAgent:${this.tool.name}] Tool execution failed (attempt ${this.attempts}/${this.config.maxRetries}): ${errorMsg}`);
|
|
10192
|
+
let hint = "";
|
|
10193
|
+
if (this.extractSourceType() === "netsuite" && /"o:errorCode"\s*:\s*"UNEXPECTED_ERROR"/.test(errorMsg)) {
|
|
10194
|
+
hint = ` This gives no column/syntax detail \u2014 it usually means a column that doesn't exist on that table (wrong field name), or a column referenced outside the CTE that selects it (a CTE only exposes columns it projects). Recheck every column name against the schema.`;
|
|
10195
|
+
}
|
|
10176
10196
|
if (this.streamBuffer.hasCallback()) {
|
|
10177
10197
|
const querySeconds = ((Date.now() - queryStartTime) / 1e3).toFixed(1);
|
|
10178
10198
|
this.streamBuffer.write(`__QUERY_TIMER_DONE_${querySeconds}__
|
|
10179
10199
|
|
|
10180
10200
|
`);
|
|
10181
|
-
this.streamBuffer.write(`\u274C **Query failed:** ${errorMsg}
|
|
10201
|
+
this.streamBuffer.write(`\u274C **Query failed:** ${errorMsg}${hint}
|
|
10182
10202
|
|
|
10183
10203
|
`);
|
|
10184
10204
|
}
|
|
10185
|
-
return `\u274C ERROR: ${errorMsg}
|
|
10205
|
+
return `\u274C ERROR: ${errorMsg}${hint}
|
|
10186
10206
|
|
|
10187
10207
|
Analyze the error and try again with a corrected query.`;
|
|
10188
10208
|
}
|
|
@@ -11195,13 +11215,34 @@ function findHardcodedComponentText(scriptBody) {
|
|
|
11195
11215
|
}
|
|
11196
11216
|
return violations;
|
|
11197
11217
|
}
|
|
11218
|
+
function findHardcodedEntityNames(source, entityDisplayNames) {
|
|
11219
|
+
const names = entityDisplayNames.filter((n) => n && n.trim().length >= 2);
|
|
11220
|
+
if (!names.length) return [];
|
|
11221
|
+
const violations = [];
|
|
11222
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11223
|
+
for (const chunk of literalChunks(source)) {
|
|
11224
|
+
const trimmed = chunk.trim();
|
|
11225
|
+
if (!trimmed) continue;
|
|
11226
|
+
for (const name of names) {
|
|
11227
|
+
if (seen.has(name)) continue;
|
|
11228
|
+
if (trimmed.toLowerCase().includes(name.toLowerCase())) {
|
|
11229
|
+
seen.add(name);
|
|
11230
|
+
violations.push({
|
|
11231
|
+
text: trimmed.slice(0, 120),
|
|
11232
|
+
reason: `"${name}" \u2014 a name resolved via resolve_entities this turn \u2014 is written as fixed text. Read it from \`data\`/\`datasets\` instead (e.g. \`data[0]?.<name_column>\`), or filter/join on the resolved id (\`params.<name>Id\`) rather than the name string \u2014 a name typed as a literal is exactly as wrong on replay as a number typed as a literal: the id a future run resolves will differ, but the printed/filtered name will not.`
|
|
11233
|
+
});
|
|
11234
|
+
}
|
|
11235
|
+
}
|
|
11236
|
+
}
|
|
11237
|
+
return violations;
|
|
11238
|
+
}
|
|
11198
11239
|
function formatLintError(violations) {
|
|
11199
11240
|
if (!violations.length) return null;
|
|
11200
11241
|
const lines = violations.map((v) => ` \u2022 "${v.text}" \u2014 ${v.reason}`).join("\n");
|
|
11201
|
-
return `The analysis body hardcodes ${violations.length}
|
|
11242
|
+
return `The analysis body hardcodes ${violations.length} value${violations.length > 1 ? "s" : ""} as text:
|
|
11202
11243
|
${lines}
|
|
11203
11244
|
|
|
11204
|
-
FIX: every figure must come from the data via a fmt helper \u2014 fmt.money, fmt.num, fmt.pct, fmt.delta, fmt.count, fmt.date, fmt.range. A
|
|
11245
|
+
FIX: every figure must come from the data via a fmt helper \u2014 fmt.money, fmt.num, fmt.pct, fmt.delta, fmt.count, fmt.date, fmt.range \u2014 and every resolved name must come from \`data\`/\`datasets\`, not be typed in. A value typed into a string is frozen at authoring time and will be wrong on replay when the data or params change. Rewording will not clear this \u2014 only moving the value out of the string will. A constant the code itself uses is fine once interpolated from a variable; a figure or name read off the data is not.`;
|
|
11205
11246
|
}
|
|
11206
11247
|
|
|
11207
11248
|
// src/userResponse/scripts/component-validator.ts
|
|
@@ -11972,7 +12013,8 @@ ${catalog}`,
|
|
|
11972
12013
|
this.renderComponentAttempts++;
|
|
11973
12014
|
const hardcodedAggregates = findHardcodedAggregates(body);
|
|
11974
12015
|
const hardcodedComponentText = findHardcodedComponentText(body);
|
|
11975
|
-
const
|
|
12016
|
+
const hardcodedEntityNames = findHardcodedEntityNames(body, this.resolvedEntities.map((e) => e.displayName));
|
|
12017
|
+
const componentViolations = [...hardcodedAggregates, ...hardcodedComponentText, ...hardcodedEntityNames];
|
|
11976
12018
|
if (componentViolations.length > 0) {
|
|
11977
12019
|
const named = componentViolations.map((v) => `"${v.text}"`).join(", ");
|
|
11978
12020
|
const repeatedTexts = componentViolations.map((v) => v.text).filter((t) => this.lastComponentsViolationTexts.includes(t));
|
|
@@ -12055,7 +12097,10 @@ Now write your answer for the user. Do not restate the component list.`;
|
|
|
12055
12097
|
return `Reached the max of ${MAX_ANALYSIS_ATTEMPTS} write_analysis attempts. Write your final analysis as prose instead, using the values from the script result exactly as shown.`;
|
|
12056
12098
|
}
|
|
12057
12099
|
this.scriptState.analysisAttempts++;
|
|
12058
|
-
const violations =
|
|
12100
|
+
const violations = [
|
|
12101
|
+
...lintAnalysisBody(body),
|
|
12102
|
+
...findHardcodedEntityNames(body, this.resolvedEntities.map((e) => e.displayName))
|
|
12103
|
+
];
|
|
12059
12104
|
const repeatedTexts = violations.map((v) => v.text).filter((t) => this.scriptState.lastAnalysisViolationTexts.includes(t));
|
|
12060
12105
|
this.scriptState.lastAnalysisViolationTexts = violations.map((v) => v.text);
|
|
12061
12106
|
const lintError = formatLintError(violations);
|
|
@@ -12133,6 +12178,14 @@ FIX: derive every such value from the data itself \u2014 \`data.length\`, a real
|
|
|
12133
12178
|
${lines}
|
|
12134
12179
|
|
|
12135
12180
|
FIX: build the title/description with a template literal that reads the value from \`data\`/\`datasets\` at render time (e.g. \` \\\`Total: \\\${fmt.money(datasets.summary[0].total)}\\\` \`), never a number typed directly into the string.`;
|
|
12181
|
+
}
|
|
12182
|
+
const hardcodedEntityNames = findHardcodedEntityNames(scriptBody, this.resolvedEntities.map((e) => e.displayName));
|
|
12183
|
+
if (hardcodedEntityNames.length > 0) {
|
|
12184
|
+
const lines = hardcodedEntityNames.map((v) => ` \u2022 "${v.text}" \u2014 ${v.reason}`).join("\n");
|
|
12185
|
+
return `write_script rejected: the script hardcodes ${hardcodedEntityNames.length} resolved entity name(s) as fixed text:
|
|
12186
|
+
${lines}
|
|
12187
|
+
|
|
12188
|
+
FIX: read the name from \`data\`/\`datasets\` at render time, or filter/join on the resolved id (\`params.<name>Id\`) instead of the name string.`;
|
|
12136
12189
|
}
|
|
12137
12190
|
const draft = await this.scriptStore.saveDraft({
|
|
12138
12191
|
recipeId: this.scriptState.recipeId ?? void 0,
|
|
@@ -13338,6 +13391,22 @@ function stripNamedExport(body, fnName) {
|
|
|
13338
13391
|
}
|
|
13339
13392
|
return body.slice(0, start);
|
|
13340
13393
|
}
|
|
13394
|
+
function extractNamedExport(body, fnName) {
|
|
13395
|
+
const start = body.search(new RegExp(`export\\s+(?:async\\s+)?function\\s+${fnName}\\b`));
|
|
13396
|
+
if (start === -1) return null;
|
|
13397
|
+
const open = body.indexOf("{", start);
|
|
13398
|
+
if (open === -1) return null;
|
|
13399
|
+
let depth = 0;
|
|
13400
|
+
for (let i = open; i < body.length; i++) {
|
|
13401
|
+
const c = body[i];
|
|
13402
|
+
if (c === "{") depth++;
|
|
13403
|
+
else if (c === "}") {
|
|
13404
|
+
depth--;
|
|
13405
|
+
if (depth === 0) return body.slice(start, i + 1);
|
|
13406
|
+
}
|
|
13407
|
+
}
|
|
13408
|
+
return null;
|
|
13409
|
+
}
|
|
13341
13410
|
function normalizeScriptBody(scriptBody) {
|
|
13342
13411
|
let i = 0;
|
|
13343
13412
|
const n = scriptBody.length;
|
|
@@ -13437,6 +13506,37 @@ var ScriptStore = class {
|
|
|
13437
13506
|
return null;
|
|
13438
13507
|
}
|
|
13439
13508
|
}
|
|
13509
|
+
/**
|
|
13510
|
+
* Fetch one recipe by id, pinned to a SPECIFIC past version rather than
|
|
13511
|
+
* whatever is live today. Needed to render a past turn's conversation
|
|
13512
|
+
* history accurately after the script has since been edited — get() alone
|
|
13513
|
+
* always returns the CURRENT body, which may no longer be what that turn
|
|
13514
|
+
* actually ran.
|
|
13515
|
+
*
|
|
13516
|
+
* Returns null (never a silently-wrong version) when the requested version
|
|
13517
|
+
* isn't the current one and its archived body can't be found — e.g. the
|
|
13518
|
+
* archive was cleaned up after the whole recipe was later deleted.
|
|
13519
|
+
*/
|
|
13520
|
+
async getVersion(id, version) {
|
|
13521
|
+
if (!this.store) return null;
|
|
13522
|
+
try {
|
|
13523
|
+
const row = await this.store.getById(id);
|
|
13524
|
+
if (!row) return null;
|
|
13525
|
+
if (version === row.version) {
|
|
13526
|
+
return this.rowToRecipe(row, this.readBody(row.fileBase));
|
|
13527
|
+
}
|
|
13528
|
+
const archivePath = path6.join(this.archiveDir, `${row.fileBase}.v${version}.ts`);
|
|
13529
|
+
if (!fs6.existsSync(archivePath)) {
|
|
13530
|
+
logger.warn(`[ScriptStore] getVersion(${id}, v${version}) \u2014 archived body not found, recipe is now at v${row.version}`);
|
|
13531
|
+
return null;
|
|
13532
|
+
}
|
|
13533
|
+
const body = fs6.readFileSync(archivePath, "utf-8");
|
|
13534
|
+
return this.rowToRecipe(row, body);
|
|
13535
|
+
} catch (err) {
|
|
13536
|
+
logger.warn(`[ScriptStore] getVersion(${id}, v${version}) failed: ${err}`);
|
|
13537
|
+
return null;
|
|
13538
|
+
}
|
|
13539
|
+
}
|
|
13440
13540
|
// ============================================
|
|
13441
13541
|
// Write
|
|
13442
13542
|
// ============================================
|
|
@@ -13476,7 +13576,20 @@ var ScriptStore = class {
|
|
|
13476
13576
|
const reuse = existing && existing.status === "draft" && existing.turnId === input.turnId;
|
|
13477
13577
|
const id = reuse ? existing.id : `script_${Date.now()}_${randomBytes(3).toString("hex")}`;
|
|
13478
13578
|
const fileBase = reuse ? existing.fileBase : input.parentId ? `${this.toSlug(input.name)}-${id.slice(-6)}` : await this.computeFileBase(input.name, id);
|
|
13479
|
-
|
|
13579
|
+
let normalized = normalizeScriptBody(input.scriptBody);
|
|
13580
|
+
if (reuse) {
|
|
13581
|
+
const existingBody = this.readBody(fileBase);
|
|
13582
|
+
if (existingBody) {
|
|
13583
|
+
for (const fnName of ["getComponents", "getAnalysis"]) {
|
|
13584
|
+
if (new RegExp(`export\\s+(?:async\\s+)?function\\s+${fnName}\\b`).test(normalized)) continue;
|
|
13585
|
+
const carried = extractNamedExport(existingBody, fnName);
|
|
13586
|
+
if (carried) normalized = `${normalized.trimEnd()}
|
|
13587
|
+
|
|
13588
|
+
${carried.trim()}
|
|
13589
|
+
`;
|
|
13590
|
+
}
|
|
13591
|
+
}
|
|
13592
|
+
}
|
|
13480
13593
|
const bodyHash = this.hash(normalized);
|
|
13481
13594
|
this.writeBody(fileBase, normalized);
|
|
13482
13595
|
const recipe = {
|
|
@@ -17726,6 +17839,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${scriptMatch
|
|
|
17726
17839
|
// ("use mode instead") can edit THIS script.
|
|
17727
17840
|
scriptBinding: {
|
|
17728
17841
|
recipeId: scriptMatch.recipe.id,
|
|
17842
|
+
version: scriptMatch.recipe.version,
|
|
17729
17843
|
params: scriptMatch.extractedParams || {},
|
|
17730
17844
|
name: scriptMatch.recipe.name,
|
|
17731
17845
|
columns: scriptResult.executedQueries?.[0]?.data?.[0] ? Object.keys(scriptResult.executedQueries[0].data[0]) : void 0
|
|
@@ -18153,6 +18267,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${carriedRequ
|
|
|
18153
18267
|
if (committed) {
|
|
18154
18268
|
scriptBinding = {
|
|
18155
18269
|
recipeId: committed.id,
|
|
18270
|
+
version: committed.version,
|
|
18156
18271
|
params: {},
|
|
18157
18272
|
name: committed.name,
|
|
18158
18273
|
columns: authored.executedQueries?.[0]?.data?.[0] ? Object.keys(authored.executedQueries[0].data[0]) : void 0
|
|
@@ -18210,6 +18325,7 @@ RENDERING REQUIREMENT (overrides the default choice of component): ${carriedRequ
|
|
|
18210
18325
|
}
|
|
18211
18326
|
scriptBinding = {
|
|
18212
18327
|
recipeId: authored.recipeId,
|
|
18328
|
+
version: promoted?.version,
|
|
18213
18329
|
params: {},
|
|
18214
18330
|
name: authored.name,
|
|
18215
18331
|
columns: authored.executedQueries?.[0]?.data?.[0] ? Object.keys(authored.executedQueries[0].data[0]) : void 0
|
|
@@ -18528,7 +18644,7 @@ var CONTEXT_CONFIG = {
|
|
|
18528
18644
|
* Set to 0 to disable conversation history
|
|
18529
18645
|
* Higher values provide more context but may increase token usage
|
|
18530
18646
|
*/
|
|
18531
|
-
MAX_CONVERSATION_CONTEXT_BLOCKS:
|
|
18647
|
+
MAX_CONVERSATION_CONTEXT_BLOCKS: 2
|
|
18532
18648
|
};
|
|
18533
18649
|
|
|
18534
18650
|
// src/handlers/user-prompt-request.ts
|
|
@@ -18626,7 +18742,12 @@ var get_user_request = async (data, components, sendMessage, anthropicApiKey, gr
|
|
|
18626
18742
|
}
|
|
18627
18743
|
const requestStartTime = Date.now();
|
|
18628
18744
|
logger.info(`Starting user prompt request with ${components.length} components`);
|
|
18629
|
-
const
|
|
18745
|
+
const conversationScriptStore = new ScriptStore({ collections, projectId: process.env.SUPERATOM_PROJECT_ID || "" });
|
|
18746
|
+
const conversationHistory = await thread.getConversationContext(
|
|
18747
|
+
CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS,
|
|
18748
|
+
existingUiBlockId,
|
|
18749
|
+
async (recipeId, version) => (await conversationScriptStore.getVersion(recipeId, version))?.scriptBody ?? null
|
|
18750
|
+
);
|
|
18630
18751
|
const MAX_EDIT_CANDIDATES = 5;
|
|
18631
18752
|
let activeScriptBindings = thread.getScriptBindings(MAX_EDIT_CANDIDATES, existingUiBlockId);
|
|
18632
18753
|
if (activeScriptBindings.length === 0 && collections?.["conversation-history"]?.["threadScriptBindings"]) {
|
|
@@ -19274,7 +19395,7 @@ async function handleActionsRequest(data, sendMessage, anthropicApiKey, groqApiK
|
|
|
19274
19395
|
logger.debug(`[ACTIONS_REQ ${id}] Component: ${component?.name || "unknown"} (${component?.type || "unknown"})`);
|
|
19275
19396
|
logger.debug(`[ACTIONS_REQ ${id}] Component data available: ${componentData ? "yes" : "no"}`);
|
|
19276
19397
|
logger.debug(`[ACTIONS_REQ ${id}] Extracting conversation history (max ${CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS} blocks)`);
|
|
19277
|
-
const conversationHistory = thread.getConversationContext(CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS, uiBlockId);
|
|
19398
|
+
const conversationHistory = await thread.getConversationContext(CONTEXT_CONFIG.MAX_CONVERSATION_CONTEXT_BLOCKS, uiBlockId);
|
|
19278
19399
|
const historyLineCount = conversationHistory.split("\n").filter((l) => l.trim()).length;
|
|
19279
19400
|
logger.info(`[ACTIONS_REQ ${id}] Conversation history extracted: ${historyLineCount} lines`);
|
|
19280
19401
|
logger.debug(`[ACTIONS_REQ ${id}] Conversation history preview:
|