@gakim-digital/dexter-bridge 0.11.16 → 0.11.19
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/package.json +1 -1
- package/src/agent.js +192 -33
- package/src/agentOutput.js +51 -1
- package/src/api.js +31 -0
- package/src/config.js +1 -0
- package/src/framerAgentTools.js +215 -77
- package/src/framerBehaviorContract.js +15 -1
- package/src/googleFontsFallback.js +196 -0
- package/src/protocol.js +89 -68
- package/src/providers/codexAppServer.js +105 -5
package/src/framerAgentTools.js
CHANGED
|
@@ -4,6 +4,7 @@ import { spawn } from 'node:child_process';
|
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import jsTokens from 'js-tokens';
|
|
7
|
+
import { loadGoogleFontsCatalog, searchGoogleFonts } from './googleFontsFallback.js';
|
|
7
8
|
import {
|
|
8
9
|
canonicalIdMappings,
|
|
9
10
|
evidenceNodeIds,
|
|
@@ -49,7 +50,7 @@ const PAGE_PATH_PATTERN = /^\/[^\0\r\n]{0,499}$/;
|
|
|
49
50
|
const UNSAFE_EXEC_PATTERN =
|
|
50
51
|
/\b(?:process|globalThis|global|require|import\s*\(|child_process|worker_threads|fs|os|net|tls|dgram|http|https|fetch|WebSocket|eval|Function)\b/;
|
|
51
52
|
const MUTATING_EXEC_PATTERN =
|
|
52
|
-
/\.(?:set|add|create|remove|
|
|
53
|
+
/\.(?:(?:set|add|delete|join|apply)(?:[A-Z_][A-Za-z0-9_]*)|(?:create|remove|update|insert|move|clone|duplicate|upload|publish|deploy|switch|merge|rename|leave)[A-Za-z0-9_]*)\s*\(/;
|
|
53
54
|
const CODE_FILE_SOURCE_WRITE_PATTERN =
|
|
54
55
|
/(?:\.setFileContent|\[\s*['"]setFileContent['"]\s*\])\s*\(/;
|
|
55
56
|
const TASK_DOMAINS = [
|
|
@@ -81,19 +82,6 @@ const EMPTY_SCHEMA = {
|
|
|
81
82
|
};
|
|
82
83
|
|
|
83
84
|
export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
84
|
-
{
|
|
85
|
-
name: 'progress_update',
|
|
86
|
-
description:
|
|
87
|
-
'Tell the user what you are about to do or are doing now in one or two natural first-person sentences. Name the specific page area, component, or behavior and the concrete change; optionally say what you will check next. Avoid generic updates such as "working on the next step", "thinking", "loading", or "making changes". Call this before the first Framer inspection or edit and at meaningful phase changes.',
|
|
88
|
-
inputSchema: {
|
|
89
|
-
type: 'object',
|
|
90
|
-
properties: {
|
|
91
|
-
message: { type: 'string', minLength: 1, maxLength: 500 },
|
|
92
|
-
},
|
|
93
|
-
required: ['message'],
|
|
94
|
-
additionalProperties: false,
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
85
|
{
|
|
98
86
|
name: 'framer_instructions',
|
|
99
87
|
description:
|
|
@@ -124,7 +112,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
124
112
|
{
|
|
125
113
|
name: 'framer_read_project',
|
|
126
114
|
description:
|
|
127
|
-
'Run Framer Agent readProject queries
|
|
115
|
+
'Run Framer Agent readProject queries: "screenshot", "font-search", and "implementation-guide-from-index" are the only query types. Read nodes and layout with framer_read instead.',
|
|
128
116
|
inputSchema: {
|
|
129
117
|
type: 'object',
|
|
130
118
|
properties: {
|
|
@@ -148,6 +136,24 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
148
136
|
additionalProperties: false,
|
|
149
137
|
},
|
|
150
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: 'framer_read_website',
|
|
141
|
+
description:
|
|
142
|
+
'Read one page of a website the user named: exact copy per section, fonts and text styles, colours, image URLs (including CSS backgrounds), and desktop and phone screenshots. Use it before recreating or migrating that page. Only sites the user named can be read.',
|
|
143
|
+
inputSchema: {
|
|
144
|
+
type: 'object',
|
|
145
|
+
properties: {
|
|
146
|
+
url: { type: 'string', minLength: 1, maxLength: 2048 },
|
|
147
|
+
screenshotTop: {
|
|
148
|
+
type: 'integer',
|
|
149
|
+
minimum: 0,
|
|
150
|
+
description: 'Desktop page offset in pixels for the screenshots. Use the value the previous result suggests to see further down.',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
required: ['url'],
|
|
154
|
+
additionalProperties: false,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
151
157
|
{
|
|
152
158
|
name: 'framer_plan_task',
|
|
153
159
|
description:
|
|
@@ -210,7 +216,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
210
216
|
{
|
|
211
217
|
name: 'framer_verify_interactions',
|
|
212
218
|
description:
|
|
213
|
-
'Confirm planned behaviors against the live project. For each target node, name the Framer property that carries the behavior (for example loopEffect, hoverEffect, appearEffect, link, or onTap) and optionally a literal the value must contain (such as a route or SHOW_OVERLAY). Results: verified (the property is set and matches), contradicted (the property holds a different value), or unknown (the property was not found on the node). Unknown never blocks your work; it is reported as unverified. Checks accumulate across calls, so you can verify targets on several pages.',
|
|
219
|
+
'Confirm planned behaviors against the live project. For each target node, name the Framer property that carries the behavior (for example loopEffect, hoverEffect, appearEffect, link, or onTap, or a field inside one such as link.href or hoverEffect.opacity) and optionally a literal the value must contain (such as a route or SHOW_OVERLAY). Results: verified (the property is set and matches), contradicted (the property holds a different value), or unknown (the property was not found on the node). Unknown never blocks your work; it is reported as unverified. Checks accumulate across calls, so you can verify targets on several pages.',
|
|
214
220
|
inputSchema: {
|
|
215
221
|
type: 'object',
|
|
216
222
|
properties: {
|
|
@@ -270,12 +276,20 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
270
276
|
{
|
|
271
277
|
name: 'framer_apply_changes',
|
|
272
278
|
description:
|
|
273
|
-
'Apply Framer Agent DSL changes directly to the connected project. Prefer this for page, layout, style, design-token, component, and CMS-on-canvas work.',
|
|
279
|
+
'Apply Framer Agent DSL changes directly to the connected project. Prefer this for page, layout, style, design-token, component, and CMS-on-canvas work. Pass screenshotNodeIds to get screenshots of those nodes taken after the change in the same result, instead of a separate read.',
|
|
274
280
|
inputSchema: {
|
|
275
281
|
type: 'object',
|
|
276
282
|
properties: {
|
|
277
283
|
changes: { type: 'string', minLength: 1, maxLength: 250_000 },
|
|
278
284
|
pagePath: { type: 'string', maxLength: 500, default: '/' },
|
|
285
|
+
screenshotNodeIds: {
|
|
286
|
+
type: 'array',
|
|
287
|
+
maxItems: MAX_SCREENSHOTS_PER_CALL,
|
|
288
|
+
uniqueItems: true,
|
|
289
|
+
items: { type: 'string', minLength: 1, maxLength: 200 },
|
|
290
|
+
description:
|
|
291
|
+
'Nodes (or component variants) to screenshot after the change is applied. The screenshots count as visual evidence after this mutation.',
|
|
292
|
+
},
|
|
279
293
|
},
|
|
280
294
|
required: ['changes'],
|
|
281
295
|
additionalProperties: false,
|
|
@@ -342,7 +356,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
342
356
|
{
|
|
343
357
|
name: 'framer_read',
|
|
344
358
|
description:
|
|
345
|
-
'Execute read-only JavaScript with the official Framer Agent connection for
|
|
359
|
+
'Execute read-only JavaScript with the official Framer Agent connection. Use it to read nodes and layout, for example with framer.agent.getNode, serialize, or serializeNodes; combine several reads in one script.',
|
|
346
360
|
inputSchema: {
|
|
347
361
|
type: 'object',
|
|
348
362
|
properties: {
|
|
@@ -521,12 +535,21 @@ function onDemandGuideCatalog(projectRoot) {
|
|
|
521
535
|
return catalog;
|
|
522
536
|
}
|
|
523
537
|
|
|
538
|
+
// Returns a "## " or "### " section up to the next heading of the same or a
|
|
539
|
+
// higher level.
|
|
524
540
|
function markdownSection(markdown, heading) {
|
|
525
541
|
const lines = markdown.split('\n');
|
|
526
|
-
const
|
|
542
|
+
const headingLevel = (line) => /^(#{1,6}) /.exec(line)?.[1].length || 0;
|
|
543
|
+
const start = lines.findIndex(
|
|
544
|
+
(line) => headingLevel(line) >= 2
|
|
545
|
+
&& line.trim().replace(/^#+ /, '') === heading,
|
|
546
|
+
);
|
|
527
547
|
if (start < 0) return '';
|
|
548
|
+
const level = headingLevel(lines[start]);
|
|
528
549
|
const end = lines.findIndex(
|
|
529
|
-
(line, index) => index > start
|
|
550
|
+
(line, index) => index > start
|
|
551
|
+
&& headingLevel(line) > 0
|
|
552
|
+
&& headingLevel(line) <= level,
|
|
530
553
|
);
|
|
531
554
|
return lines.slice(start, end < 0 ? undefined : end).join('\n').trim();
|
|
532
555
|
}
|
|
@@ -1129,6 +1152,7 @@ export function createFramerAgentToolRuntime({
|
|
|
1129
1152
|
fetchImpl = fetch,
|
|
1130
1153
|
authorizeProject,
|
|
1131
1154
|
verifyProjectAuthorization,
|
|
1155
|
+
readWebsite,
|
|
1132
1156
|
} = {}) {
|
|
1133
1157
|
const projectId = normalizeProjectId(assignment?.context?.framerProjectId);
|
|
1134
1158
|
const projectUrl = normalizeProjectUrl(
|
|
@@ -1177,7 +1201,6 @@ export function createFramerAgentToolRuntime({
|
|
|
1177
1201
|
let lowLevelWriteCalls = 0;
|
|
1178
1202
|
let imageSearchCalls = 0;
|
|
1179
1203
|
let readCalls = 0;
|
|
1180
|
-
let progressCalls = 0;
|
|
1181
1204
|
let toolCallCount = 0;
|
|
1182
1205
|
const toolCalls = [];
|
|
1183
1206
|
let repeatedFailureFingerprint = null;
|
|
@@ -1188,6 +1211,7 @@ export function createFramerAgentToolRuntime({
|
|
|
1188
1211
|
let lowLevelWriteDisabled = false;
|
|
1189
1212
|
let lowLevelWriteBlockedCalls = 0;
|
|
1190
1213
|
let instructionFallbacks = 0;
|
|
1214
|
+
let fontSearchFallbacks = 0;
|
|
1191
1215
|
let instructionLookupUnavailable = false;
|
|
1192
1216
|
let branchChange = null;
|
|
1193
1217
|
let lastInspectionAt = null;
|
|
@@ -1537,14 +1561,23 @@ export function createFramerAgentToolRuntime({
|
|
|
1537
1561
|
// Every run gets the guidance an edit always needs: principles, command
|
|
1538
1562
|
// syntax, tool APIs, scope and replica rules, and the edit loop. Topic
|
|
1539
1563
|
// guides (CMS, components, creation strategy, examples, ...) are listed
|
|
1540
|
-
// in a catalog and read on demand through framer_instructions.
|
|
1564
|
+
// in a catalog and read on demand through framer_instructions. The CLI,
|
|
1565
|
+
// session, and file-map parts of SKILL.md and index.md do not apply to
|
|
1566
|
+
// Dexter's tools, and index.md changes every session, so both are left
|
|
1567
|
+
// out; the guidance stays byte-identical across runs and can be cached.
|
|
1568
|
+
// The project inventory changes with the project and is returned apart.
|
|
1541
1569
|
const requestedFiles = [
|
|
1542
|
-
{ root: skillRoot, path: 'SKILL.md', label: 'SKILL.md' },
|
|
1543
|
-
{ root: projectRoot, path: 'index.md', label: 'project/index.md' },
|
|
1544
1570
|
{
|
|
1545
|
-
root:
|
|
1546
|
-
path: '
|
|
1547
|
-
|
|
1571
|
+
root: skillRoot,
|
|
1572
|
+
path: 'SKILL.md',
|
|
1573
|
+
section: 'Method Selection',
|
|
1574
|
+
label: 'SKILL.md#Method Selection',
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
root: skillRoot,
|
|
1578
|
+
path: 'SKILL.md',
|
|
1579
|
+
section: 'Core Usage Principles',
|
|
1580
|
+
label: 'SKILL.md#Core Usage Principles',
|
|
1548
1581
|
},
|
|
1549
1582
|
{ root: projectRoot, path: 'prompt/overview.md' },
|
|
1550
1583
|
{ root: projectRoot, path: 'prompt/core-principles.md' },
|
|
@@ -1590,8 +1623,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1590
1623
|
...files.map((file) => `## ${file.path}\n\n${file.content}`),
|
|
1591
1624
|
...(catalogText ? [catalogText] : []),
|
|
1592
1625
|
].join('\n\n');
|
|
1593
|
-
const inventory =
|
|
1594
|
-
|
|
1626
|
+
const inventory = readGuidanceFile(
|
|
1627
|
+
projectRoot,
|
|
1628
|
+
'project-inventory.md',
|
|
1629
|
+
MAX_GUIDANCE_FILE_BYTES,
|
|
1595
1630
|
)?.content || '';
|
|
1596
1631
|
guidanceMetadata = {
|
|
1597
1632
|
loaded: Boolean(content),
|
|
@@ -1601,10 +1636,13 @@ export function createFramerAgentToolRuntime({
|
|
|
1601
1636
|
projectContextHash: inventory
|
|
1602
1637
|
? crypto.createHash('sha256').update(inventory).digest('hex')
|
|
1603
1638
|
: null,
|
|
1604
|
-
files:
|
|
1605
|
-
|
|
1639
|
+
files: [
|
|
1640
|
+
...files.map((file) => file.path),
|
|
1641
|
+
...(inventory ? ['project/project-inventory.md'] : []),
|
|
1642
|
+
],
|
|
1643
|
+
bytes: Buffer.byteLength(content + inventory, 'utf8'),
|
|
1606
1644
|
};
|
|
1607
|
-
return { content, ...guidanceMetadata };
|
|
1645
|
+
return { content, inventory, ...guidanceMetadata };
|
|
1608
1646
|
};
|
|
1609
1647
|
|
|
1610
1648
|
const configurePromptContext = ({
|
|
@@ -1717,7 +1755,74 @@ export function createFramerAgentToolRuntime({
|
|
|
1717
1755
|
capture: captured.error,
|
|
1718
1756
|
});
|
|
1719
1757
|
}
|
|
1720
|
-
|
|
1758
|
+
screenshotsViaDownload += viaDownload;
|
|
1759
|
+
screenshotsViaSession += viaSession;
|
|
1760
|
+
screenshotsUndelivered += undelivered;
|
|
1761
|
+
screenshotFailures.push(...failures);
|
|
1762
|
+
screenshotFailures.splice(
|
|
1763
|
+
0,
|
|
1764
|
+
Math.max(0, screenshotFailures.length - MAX_SCREENSHOT_FAILURES_REPORTED),
|
|
1765
|
+
);
|
|
1766
|
+
return images;
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
const readProject = (queries, pagePath) => execCode(
|
|
1770
|
+
[
|
|
1771
|
+
`const result = await framer.agent.readProject(${JSON.stringify(queries)}, { pagePath: ${JSON.stringify(pagePath)} });`,
|
|
1772
|
+
'console.log(JSON.stringify(result, null, 2));',
|
|
1773
|
+
].join('\n'),
|
|
1774
|
+
);
|
|
1775
|
+
|
|
1776
|
+
// Runs readProject screenshot queries and hands the pixels to the model.
|
|
1777
|
+
const readScreenshots = async (queries, pagePath) => {
|
|
1778
|
+
const result = await readProject(queries, pagePath);
|
|
1779
|
+
return deliverScreenshots(queries, parseStructuredOutput(result.stdout));
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
// Framer bills style font searches to the workspace's AI credit and fails
|
|
1783
|
+
// the whole read when they cannot run. Those searches are then answered
|
|
1784
|
+
// from the Google Fonts catalog and the remaining queries are read again.
|
|
1785
|
+
// Returns null when there is nothing to answer, so the original error stands.
|
|
1786
|
+
const readProjectWithFontFallback = async (queries, pagePath) => {
|
|
1787
|
+
try {
|
|
1788
|
+
return await readProject(queries, pagePath);
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
const styleSearch = (query) =>
|
|
1791
|
+
query.type === 'font-search' && typeof query.query === 'string';
|
|
1792
|
+
if (!queries.some(styleSearch)) throw error;
|
|
1793
|
+
let catalog;
|
|
1794
|
+
try {
|
|
1795
|
+
catalog = await loadGoogleFontsCatalog(fetchImpl);
|
|
1796
|
+
} catch (catalogError) {
|
|
1797
|
+
trace?.warn?.('google_fonts_fallback_unavailable', {
|
|
1798
|
+
message: catalogError?.message,
|
|
1799
|
+
});
|
|
1800
|
+
throw error;
|
|
1801
|
+
}
|
|
1802
|
+
const remaining = queries.filter((query) => !styleSearch(query));
|
|
1803
|
+
const remainingResults = remaining.length
|
|
1804
|
+
? parseStructuredOutput((await readProject(remaining, pagePath)).stdout)?.results || []
|
|
1805
|
+
: [];
|
|
1806
|
+
fontSearchFallbacks += 1;
|
|
1807
|
+
trace?.warn?.('framer_font_search_fallback', {
|
|
1808
|
+
message: boundedText(String(error?.message || ''), 300),
|
|
1809
|
+
});
|
|
1810
|
+
const reason = boundedText(String(error?.message || 'unavailable'), 160);
|
|
1811
|
+
let next = 0;
|
|
1812
|
+
return {
|
|
1813
|
+
stdout: JSON.stringify({
|
|
1814
|
+
results: queries.map((query) => (styleSearch(query)
|
|
1815
|
+
? {
|
|
1816
|
+
type: 'font-search',
|
|
1817
|
+
source: 'google-fonts',
|
|
1818
|
+
note: `Framer's style font search failed (${reason}), so these candidates come from the Google Fonts library, which Framer includes. Choose by how the family looks, set fontName to it, and use a listed weight. If you know a Google Fonts family that fits better, confirm it with a font-search by name.`,
|
|
1819
|
+
results: searchGoogleFonts(catalog, query),
|
|
1820
|
+
}
|
|
1821
|
+
: remainingResults[next++] ?? null)),
|
|
1822
|
+
}),
|
|
1823
|
+
stderr: '',
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1721
1826
|
};
|
|
1722
1827
|
|
|
1723
1828
|
const referenceAssetFiles = Array.isArray(assignment?.context?.referenceAssetFiles)
|
|
@@ -2086,6 +2191,17 @@ export function createFramerAgentToolRuntime({
|
|
|
2086
2191
|
].join(' ');
|
|
2087
2192
|
};
|
|
2088
2193
|
|
|
2194
|
+
const evidenceRequirement = (check) => {
|
|
2195
|
+
if (check === 'structural') return 'a focused live read';
|
|
2196
|
+
if (check === 'visual') {
|
|
2197
|
+
return 'a screenshot you inspect (screenshotNodeIds on your last framer_apply_changes provides it)';
|
|
2198
|
+
}
|
|
2199
|
+
if (check === 'interactions') {
|
|
2200
|
+
return 'framer_verify_interactions for every declared behavior and target node';
|
|
2201
|
+
}
|
|
2202
|
+
return `a read tagged with verifies:["${check}"]`;
|
|
2203
|
+
};
|
|
2204
|
+
|
|
2089
2205
|
const verificationRepairPrompt = () => {
|
|
2090
2206
|
const current = verification();
|
|
2091
2207
|
if (current.completed || mutationCalls === 0) return null;
|
|
@@ -2154,7 +2270,6 @@ export function createFramerAgentToolRuntime({
|
|
|
2154
2270
|
|
|
2155
2271
|
const reserveToolCall = (name, category) => {
|
|
2156
2272
|
toolCallCount += 1;
|
|
2157
|
-
if (category === 'progress') progressCalls += 1;
|
|
2158
2273
|
if (category === 'read') readCalls += 1;
|
|
2159
2274
|
const call = {
|
|
2160
2275
|
sequence: toolCallCount,
|
|
@@ -2219,25 +2334,6 @@ export function createFramerAgentToolRuntime({
|
|
|
2219
2334
|
'The same Framer tool call has failed five times in a row. Change the approach or report the concrete blocker.',
|
|
2220
2335
|
);
|
|
2221
2336
|
}
|
|
2222
|
-
if (name === 'progress_update') {
|
|
2223
|
-
const message = String(rawArguments.message || '').trim().slice(0, 500);
|
|
2224
|
-
if (!message) {
|
|
2225
|
-
throw toolError(
|
|
2226
|
-
'FRAMER_AGENT_PROGRESS_INVALID',
|
|
2227
|
-
'A progress message is required.',
|
|
2228
|
-
);
|
|
2229
|
-
}
|
|
2230
|
-
const toolCall = reserveToolCall(name, 'progress');
|
|
2231
|
-
reportActivity({
|
|
2232
|
-
kind: 'status_update',
|
|
2233
|
-
source: 'harness',
|
|
2234
|
-
message,
|
|
2235
|
-
});
|
|
2236
|
-
repeatedFailureFingerprint = null;
|
|
2237
|
-
repeatedFailureCount = 0;
|
|
2238
|
-
toolCall.status = 'succeeded';
|
|
2239
|
-
return { acknowledged: true };
|
|
2240
|
-
}
|
|
2241
2337
|
|
|
2242
2338
|
const startedAt = Date.now();
|
|
2243
2339
|
const toolCall = reserveToolCall(name, category);
|
|
@@ -2256,6 +2352,7 @@ export function createFramerAgentToolRuntime({
|
|
|
2256
2352
|
try {
|
|
2257
2353
|
let result;
|
|
2258
2354
|
let modelImages = [];
|
|
2355
|
+
let screenshotRequestCount = 0;
|
|
2259
2356
|
if (name === 'framer_write' && lowLevelWriteDisabled) {
|
|
2260
2357
|
lowLevelWriteBlockedCalls += 1;
|
|
2261
2358
|
throw toolError(
|
|
@@ -2324,6 +2421,23 @@ export function createFramerAgentToolRuntime({
|
|
|
2324
2421
|
if (queries.length > 0) instructionFallbacks += 1;
|
|
2325
2422
|
result = builtInInstructions(queries.length > 0);
|
|
2326
2423
|
}
|
|
2424
|
+
} else if (name === 'framer_read_website') {
|
|
2425
|
+
if (typeof readWebsite !== 'function') {
|
|
2426
|
+
throw toolError(
|
|
2427
|
+
'FRAMER_WEBSITE_READ_UNAVAILABLE',
|
|
2428
|
+
'Reading websites is not available in this run.',
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
const website = await readWebsite({
|
|
2432
|
+
url: String(rawArguments.url || '').trim(),
|
|
2433
|
+
screenshotTop: Number.isInteger(rawArguments.screenshotTop)
|
|
2434
|
+
? rawArguments.screenshotTop
|
|
2435
|
+
: undefined,
|
|
2436
|
+
});
|
|
2437
|
+
modelImages = (Array.isArray(website?.images) ? website.images : [])
|
|
2438
|
+
.filter((image) => typeof image?.data === 'string' && image.data)
|
|
2439
|
+
.map((image) => ({ url: null, mimeType: image.mimeType || 'image/jpeg', data: image.data }));
|
|
2440
|
+
result = { stdout: JSON.stringify(website?.result ?? {}), stderr: '' };
|
|
2327
2441
|
} else if (name === 'framer_context') {
|
|
2328
2442
|
result = await execCode(
|
|
2329
2443
|
[
|
|
@@ -2356,7 +2470,10 @@ export function createFramerAgentToolRuntime({
|
|
|
2356
2470
|
...mergeBehaviorPlans(taskPlan, nextPlan),
|
|
2357
2471
|
};
|
|
2358
2472
|
result = {
|
|
2359
|
-
stdout: JSON.stringify({
|
|
2473
|
+
stdout: JSON.stringify({
|
|
2474
|
+
plan: taskPlanSnapshot(),
|
|
2475
|
+
beforeFinishing: `After your final change, and before returning, produce: ${requiredChecks().map(evidenceRequirement).join('; ')}.`,
|
|
2476
|
+
}),
|
|
2360
2477
|
stderr: '',
|
|
2361
2478
|
};
|
|
2362
2479
|
} else if (name === 'framer_verify_interactions') {
|
|
@@ -2386,7 +2503,15 @@ export function createFramerAgentToolRuntime({
|
|
|
2386
2503
|
.map((entry) => [entry.id, entry.node]),
|
|
2387
2504
|
);
|
|
2388
2505
|
const results = verifyBehaviorChecks({ checks, nodes });
|
|
2389
|
-
|
|
2506
|
+
// Live link values read here are link evidence too, so the model does
|
|
2507
|
+
// not spend another turn re-reading them under verifies:["links"].
|
|
2508
|
+
const linkChecks = results.filter((check) => /^link(\.|$)/.test(check.property));
|
|
2509
|
+
markInspection({
|
|
2510
|
+
verifies: linkChecks.length > 0
|
|
2511
|
+
&& linkChecks.every((check) => check.verdict === 'verified')
|
|
2512
|
+
? ['links']
|
|
2513
|
+
: [],
|
|
2514
|
+
});
|
|
2390
2515
|
interactionVerificationCalls += 1;
|
|
2391
2516
|
interactionVerifiedAt = new Date().toISOString();
|
|
2392
2517
|
for (const checkResult of results) {
|
|
@@ -2415,28 +2540,15 @@ export function createFramerAgentToolRuntime({
|
|
|
2415
2540
|
);
|
|
2416
2541
|
}
|
|
2417
2542
|
const pagePath = normalizePagePath(rawArguments.pagePath);
|
|
2418
|
-
result = await
|
|
2419
|
-
[
|
|
2420
|
-
`const result = await framer.agent.readProject(${JSON.stringify(queries)}, { pagePath: ${JSON.stringify(pagePath)} });`,
|
|
2421
|
-
'console.log(JSON.stringify(result, null, 2));',
|
|
2422
|
-
].join('\n'),
|
|
2423
|
-
);
|
|
2543
|
+
result = await readProjectWithFontFallback(queries, pagePath);
|
|
2424
2544
|
const requestedScreenshots = screenshotQueries(rawArguments);
|
|
2425
|
-
|
|
2545
|
+
screenshotRequestCount = requestedScreenshots.length;
|
|
2546
|
+
modelImages = requestedScreenshots.length
|
|
2426
2547
|
? await deliverScreenshots(
|
|
2427
2548
|
requestedScreenshots,
|
|
2428
2549
|
parseStructuredOutput(result.stdout),
|
|
2429
2550
|
)
|
|
2430
|
-
:
|
|
2431
|
-
modelImages = delivery.images;
|
|
2432
|
-
screenshotsViaDownload += delivery.viaDownload;
|
|
2433
|
-
screenshotsViaSession += delivery.viaSession;
|
|
2434
|
-
screenshotsUndelivered += delivery.undelivered;
|
|
2435
|
-
screenshotFailures.push(...delivery.failures);
|
|
2436
|
-
screenshotFailures.splice(
|
|
2437
|
-
0,
|
|
2438
|
-
Math.max(0, screenshotFailures.length - MAX_SCREENSHOT_FAILURES_REPORTED),
|
|
2439
|
-
);
|
|
2551
|
+
: [];
|
|
2440
2552
|
markInspection({
|
|
2441
2553
|
visual: modelImages.length > 0,
|
|
2442
2554
|
visualAttempted: requestedScreenshots.length > 0,
|
|
@@ -2832,6 +2944,20 @@ export function createFramerAgentToolRuntime({
|
|
|
2832
2944
|
...summarizeFramerChanges(changes),
|
|
2833
2945
|
renamedIds: canonicalIdMappings(parsedResult),
|
|
2834
2946
|
});
|
|
2947
|
+
// A screenshot taken here saves the model a whole turn re-reading
|
|
2948
|
+
// its context just to look at the change.
|
|
2949
|
+
const screenshotIds = Array.isArray(rawArguments.screenshotNodeIds)
|
|
2950
|
+
? uniqueStrings(rawArguments.screenshotNodeIds, MAX_SCREENSHOTS_PER_CALL)
|
|
2951
|
+
: [];
|
|
2952
|
+
if (screenshotIds.length > 0) {
|
|
2953
|
+
const queries = screenshotIds.map((id) => ({ type: 'screenshot', id }));
|
|
2954
|
+
modelImages = await readScreenshots(queries, pagePath);
|
|
2955
|
+
screenshotRequestCount = queries.length;
|
|
2956
|
+
markInspection({
|
|
2957
|
+
visual: modelImages.length > 0,
|
|
2958
|
+
visualAttempted: true,
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2835
2961
|
} else if (name === 'framer_read') {
|
|
2836
2962
|
result = await execCode(
|
|
2837
2963
|
validateCode(rawArguments.code, {
|
|
@@ -2898,17 +3024,25 @@ export function createFramerAgentToolRuntime({
|
|
|
2898
3024
|
// Result size is what this call adds to every later model turn.
|
|
2899
3025
|
toolCall.resultBytes = Buffer.byteLength(JSON.stringify(normalized), 'utf8');
|
|
2900
3026
|
if (modelImages.length > 0) toolCall.imageCount = modelImages.length;
|
|
2901
|
-
if (name
|
|
2902
|
-
|
|
3027
|
+
if (name === 'framer_read_website') {
|
|
3028
|
+
return modelImages.length ? { ...normalized, modelImages } : normalized;
|
|
3029
|
+
}
|
|
3030
|
+
// Naming the evidence still owed after the latest change lets the model
|
|
3031
|
+
// finish it in this session instead of in a resumed repair turn.
|
|
3032
|
+
const evidenceStillDue =
|
|
3033
|
+
taskPlan && mutationCalls > 0 && (category === 'read' || screenshotRequestCount > 0)
|
|
3034
|
+
? missingVerificationChecks()
|
|
3035
|
+
: [];
|
|
2903
3036
|
return {
|
|
2904
3037
|
...normalized,
|
|
2905
3038
|
...(modelImages.length ? { modelImages } : {}),
|
|
2906
|
-
...(
|
|
3039
|
+
...(screenshotRequestCount > modelImages.length
|
|
2907
3040
|
? {
|
|
2908
3041
|
screenshotWarning:
|
|
2909
|
-
`${
|
|
3042
|
+
`${screenshotRequestCount - modelImages.length} screenshot(s) could not be downloaded for you to view. Treat that visual state as unverified.`,
|
|
2910
3043
|
}
|
|
2911
3044
|
: {}),
|
|
3045
|
+
...(evidenceStillDue.length > 0 ? { evidenceStillDue } : {}),
|
|
2912
3046
|
};
|
|
2913
3047
|
} catch (error) {
|
|
2914
3048
|
if (
|
|
@@ -2955,6 +3089,10 @@ export function createFramerAgentToolRuntime({
|
|
|
2955
3089
|
}
|
|
2956
3090
|
toolCall.durationMs = Date.now() - startedAt;
|
|
2957
3091
|
toolCall.errorCode = error?.code || 'FRAMER_AGENT_TOOL_FAILED';
|
|
3092
|
+
toolCall.errorMessage = boundedText(
|
|
3093
|
+
String(error?.message || 'Framer Agent tool failed.'),
|
|
3094
|
+
300,
|
|
3095
|
+
);
|
|
2958
3096
|
trace?.warn?.('framer_agent_tool_failed', {
|
|
2959
3097
|
name,
|
|
2960
3098
|
code: error?.code,
|
|
@@ -3082,7 +3220,6 @@ export function createFramerAgentToolRuntime({
|
|
|
3082
3220
|
: {}),
|
|
3083
3221
|
})),
|
|
3084
3222
|
readCalls,
|
|
3085
|
-
progressCalls,
|
|
3086
3223
|
toolCallCount,
|
|
3087
3224
|
toolDurationMs: toolCalls.reduce(
|
|
3088
3225
|
(total, call) => total + call.durationMs,
|
|
@@ -3110,6 +3247,7 @@ export function createFramerAgentToolRuntime({
|
|
|
3110
3247
|
lowLevelWriteBlockedCalls,
|
|
3111
3248
|
instructionFallbacks,
|
|
3112
3249
|
},
|
|
3250
|
+
fontSearchFallbacks,
|
|
3113
3251
|
branch: branchChange,
|
|
3114
3252
|
lastInspectionAt,
|
|
3115
3253
|
lastMutationAt,
|
|
@@ -110,8 +110,22 @@ function hasContent(value) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// Finds the property on the node itself. Child nodes are skipped so a
|
|
113
|
-
// descendant cannot satisfy a claim about its parent.
|
|
113
|
+
// descendant cannot satisfy a claim about its parent. A dotted name such as
|
|
114
|
+
// `link.href` or `hoverEffect.opacity`, the way the change language writes
|
|
115
|
+
// it, reads the field inside the property's value.
|
|
114
116
|
function propertyValues(node, property) {
|
|
117
|
+
const values = keyValues(node, property);
|
|
118
|
+
const [head, ...path] = property.split('.');
|
|
119
|
+
if (values.length > 0 || path.length === 0) return values;
|
|
120
|
+
return keyValues(node, head)
|
|
121
|
+
.map((value) => path.reduce(
|
|
122
|
+
(current, key) => (isRecord(current) ? current[key] : undefined),
|
|
123
|
+
value,
|
|
124
|
+
))
|
|
125
|
+
.filter(hasContent);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function keyValues(node, property) {
|
|
115
129
|
const values = [];
|
|
116
130
|
const visit = (current, depth) => {
|
|
117
131
|
if (depth > MAX_SEARCH_DEPTH || values.length >= 10) return;
|