@gakim-digital/dexter-bridge 0.11.14 → 0.11.16
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/README.md +9 -12
- package/package.json +2 -1
- package/src/agent.js +20 -9
- package/src/agentOutput.js +1 -1
- package/src/config.js +1 -3
- package/src/framerAgentTools.js +486 -792
- package/src/framerBehaviorContract.js +124 -1326
- package/src/protocol.js +5 -5
package/src/framerAgentTools.js
CHANGED
|
@@ -3,24 +3,32 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import jsTokens from 'js-tokens';
|
|
6
7
|
import {
|
|
7
|
-
BEHAVIOR_ACTIVATIONS,
|
|
8
|
-
BEHAVIOR_LAYOUT_EFFECTS,
|
|
9
|
-
BEHAVIOR_MECHANISMS,
|
|
10
|
-
BEHAVIOR_PRESENTATIONS,
|
|
11
8
|
canonicalIdMappings,
|
|
12
9
|
evidenceNodeIds,
|
|
13
10
|
mergeBehaviorPlans,
|
|
14
11
|
normalizeBehaviorEvidence,
|
|
15
12
|
normalizeBehaviorPlan,
|
|
13
|
+
summarizeBehaviorVerification,
|
|
16
14
|
summarizeFramerChanges,
|
|
17
|
-
|
|
15
|
+
verifyBehaviorChecks,
|
|
18
16
|
} from './framerBehaviorContract.js';
|
|
19
17
|
|
|
20
18
|
const require = createRequire(import.meta.url);
|
|
21
19
|
const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
22
20
|
const MAX_MODEL_RESULT_BYTES = 120 * 1024;
|
|
23
21
|
const MAX_CODE_FILE_CONTENT_CHARS = 200_000;
|
|
22
|
+
const MAX_SVG_MARKUP_CHARS = 200_000;
|
|
23
|
+
const MAX_SVG_ATTRIBUTES = 24;
|
|
24
|
+
const UNSAFE_SVG_PATTERN =
|
|
25
|
+
/<\s*(?:script|foreignObject|iframe)\b|\son[a-z]+\s*=|javascript:|(?:xlink:)?href\s*=\s*["']\s*(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
26
|
+
const MUTATION_TOOL_NAMES = new Set([
|
|
27
|
+
'framer_apply_changes',
|
|
28
|
+
'framer_add_svg',
|
|
29
|
+
'framer_update_code_file',
|
|
30
|
+
'framer_write',
|
|
31
|
+
]);
|
|
24
32
|
const MAX_SCREENSHOT_BYTES = 4 * 1024 * 1024;
|
|
25
33
|
const MAX_SCREENSHOTS_PER_CALL = 4;
|
|
26
34
|
const SCREENSHOT_FETCH_TIMEOUT_MS = 20_000;
|
|
@@ -31,8 +39,6 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 5 * 60_000;
|
|
|
31
39
|
const SESSION_TIMEOUT_MS = 10 * 60_000;
|
|
32
40
|
const MAX_UNSAFE_WRITE_FAILURES = 2;
|
|
33
41
|
const MAX_LOW_LEVEL_WRITE_FAILURES = 3;
|
|
34
|
-
const MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR = 3;
|
|
35
|
-
const MAX_INTERACTION_REPAIRS_PER_BEHAVIOR = 1;
|
|
36
42
|
const MAX_GUIDANCE_BYTES = 180 * 1024;
|
|
37
43
|
const MAX_GUIDANCE_FILE_BYTES = 48 * 1024;
|
|
38
44
|
const PROJECT_ID_PATTERN = /^[A-Za-z0-9]{20}$/;
|
|
@@ -91,10 +97,15 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
91
97
|
{
|
|
92
98
|
name: 'framer_instructions',
|
|
93
99
|
description:
|
|
94
|
-
'
|
|
100
|
+
'Read on-demand project guides by id from the "On-demand guides" catalog, or request targeted official API documentation when an exact method or type is unclear.',
|
|
95
101
|
inputSchema: {
|
|
96
102
|
type: 'object',
|
|
97
103
|
properties: {
|
|
104
|
+
guides: {
|
|
105
|
+
type: 'array',
|
|
106
|
+
maxItems: 6,
|
|
107
|
+
items: { type: 'string', minLength: 1, maxLength: 120 },
|
|
108
|
+
},
|
|
98
109
|
queries: {
|
|
99
110
|
type: 'array',
|
|
100
111
|
maxItems: 8,
|
|
@@ -140,7 +151,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
140
151
|
{
|
|
141
152
|
name: 'framer_plan_task',
|
|
142
153
|
description:
|
|
143
|
-
'After inspecting the live Framer target and before the first mutation, declare your
|
|
154
|
+
'After inspecting the live Framer target and before the first mutation, declare your understanding of the task and the evidence you will produce. Dexter does not classify the request or choose how to build it; use any capability the Framer guidance documents.',
|
|
144
155
|
inputSchema: {
|
|
145
156
|
type: 'object',
|
|
146
157
|
properties: {
|
|
@@ -167,147 +178,27 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
167
178
|
type: 'array',
|
|
168
179
|
maxItems: 20,
|
|
169
180
|
description:
|
|
170
|
-
'
|
|
181
|
+
'Interactive or animated behaviors the result must have, such as links, hover or tap effects, overlays, variants, appear, loop, or ticker motion. Declaring behaviors adds interaction verification.',
|
|
171
182
|
items: {
|
|
172
183
|
type: 'object',
|
|
173
184
|
properties: {
|
|
174
185
|
id: { type: 'string', minLength: 1, maxLength: 120 },
|
|
175
|
-
description: {
|
|
176
|
-
triggerDescription: {
|
|
177
|
-
type: 'string',
|
|
178
|
-
minLength: 3,
|
|
179
|
-
maxLength: 300,
|
|
180
|
-
},
|
|
181
|
-
activation: {
|
|
182
|
-
type: 'string',
|
|
183
|
-
enum: BEHAVIOR_ACTIVATIONS,
|
|
184
|
-
},
|
|
185
|
-
presentation: {
|
|
186
|
-
type: 'string',
|
|
187
|
-
enum: BEHAVIOR_PRESENTATIONS,
|
|
188
|
-
},
|
|
189
|
-
stateChange: { type: 'boolean' },
|
|
190
|
-
layoutEffect: {
|
|
191
|
-
type: 'string',
|
|
192
|
-
enum: BEHAVIOR_LAYOUT_EFFECTS,
|
|
193
|
-
},
|
|
194
|
-
reversible: { type: 'boolean' },
|
|
195
|
-
repeatable: { type: 'boolean' },
|
|
196
|
-
expectedTargets: {
|
|
197
|
-
type: 'integer',
|
|
198
|
-
minimum: 1,
|
|
199
|
-
maximum: 50,
|
|
200
|
-
},
|
|
201
|
-
expectedContent: {
|
|
202
|
-
type: 'array',
|
|
203
|
-
maxItems: 20,
|
|
204
|
-
items: { type: 'string', minLength: 1, maxLength: 500 },
|
|
205
|
-
description:
|
|
206
|
-
'Optional exact literal text that must exist in the interaction target. Do not put behavioral descriptions or prose assertions here.',
|
|
207
|
-
},
|
|
208
|
-
expectedDestinations: {
|
|
209
|
-
type: 'array',
|
|
210
|
-
maxItems: 20,
|
|
211
|
-
items: { type: 'string', minLength: 1, maxLength: 500 },
|
|
212
|
-
description:
|
|
213
|
-
'Required for navigation behavior. Use exact routes or URLs such as /contact.',
|
|
214
|
-
},
|
|
215
|
-
initialState: {
|
|
216
|
-
type: 'string',
|
|
217
|
-
minLength: 1,
|
|
218
|
-
maxLength: 300,
|
|
219
|
-
},
|
|
220
|
-
activatedState: {
|
|
221
|
-
type: 'string',
|
|
222
|
-
minLength: 1,
|
|
223
|
-
maxLength: 300,
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
required: [
|
|
227
|
-
'id',
|
|
228
|
-
'description',
|
|
229
|
-
'triggerDescription',
|
|
230
|
-
'activation',
|
|
231
|
-
'presentation',
|
|
232
|
-
'stateChange',
|
|
233
|
-
'layoutEffect',
|
|
234
|
-
'reversible',
|
|
235
|
-
'repeatable',
|
|
236
|
-
'expectedTargets',
|
|
237
|
-
'initialState',
|
|
238
|
-
'activatedState',
|
|
239
|
-
],
|
|
240
|
-
additionalProperties: false,
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
mechanisms: {
|
|
244
|
-
type: 'array',
|
|
245
|
-
maxItems: 20,
|
|
246
|
-
description:
|
|
247
|
-
'Required for every declared behavior. Choose the mechanism from live project capabilities and explain why it satisfies the behavior.',
|
|
248
|
-
items: {
|
|
249
|
-
type: 'object',
|
|
250
|
-
properties: {
|
|
251
|
-
behaviorId: { type: 'string', minLength: 1, maxLength: 120 },
|
|
252
|
-
mechanism: {
|
|
253
|
-
type: 'string',
|
|
254
|
-
enum: BEHAVIOR_MECHANISMS,
|
|
255
|
-
},
|
|
256
|
-
rationale: {
|
|
186
|
+
description: {
|
|
257
187
|
type: 'string',
|
|
258
|
-
minLength:
|
|
259
|
-
maxLength:
|
|
188
|
+
minLength: 5,
|
|
189
|
+
maxLength: 500,
|
|
190
|
+
description: 'What the visitor should experience.',
|
|
260
191
|
},
|
|
261
|
-
representativeFirst: { type: 'boolean', default: true },
|
|
262
192
|
targetNodeIds: {
|
|
263
193
|
type: 'array',
|
|
264
194
|
maxItems: 50,
|
|
265
195
|
uniqueItems: true,
|
|
266
|
-
items: {
|
|
267
|
-
type: 'string',
|
|
268
|
-
minLength: 1,
|
|
269
|
-
maxLength: 200,
|
|
270
|
-
},
|
|
196
|
+
items: { type: 'string', minLength: 1, maxLength: 200 },
|
|
271
197
|
description:
|
|
272
|
-
'
|
|
273
|
-
},
|
|
274
|
-
representativeTargetNodeId: {
|
|
275
|
-
type: 'string',
|
|
276
|
-
minLength: 1,
|
|
277
|
-
maxLength: 200,
|
|
278
|
-
description:
|
|
279
|
-
'The single target to implement and verify before fan-out.',
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
required: ['behaviorId', 'mechanism', 'rationale'],
|
|
283
|
-
additionalProperties: false,
|
|
284
|
-
},
|
|
285
|
-
},
|
|
286
|
-
rejectedMechanisms: {
|
|
287
|
-
type: 'array',
|
|
288
|
-
maxItems: 30,
|
|
289
|
-
description:
|
|
290
|
-
'Mechanisms already disproven by user-observed failure or prior verification. Do not silently reuse them.',
|
|
291
|
-
items: {
|
|
292
|
-
type: 'object',
|
|
293
|
-
properties: {
|
|
294
|
-
behaviorId: { type: 'string', minLength: 1, maxLength: 120 },
|
|
295
|
-
behaviorSignature: {
|
|
296
|
-
type: 'string',
|
|
297
|
-
minLength: 1,
|
|
298
|
-
maxLength: 128,
|
|
299
|
-
},
|
|
300
|
-
mechanism: {
|
|
301
|
-
type: 'string',
|
|
302
|
-
enum: BEHAVIOR_MECHANISMS,
|
|
303
|
-
},
|
|
304
|
-
reason: { type: 'string', minLength: 5, maxLength: 800 },
|
|
305
|
-
source: {
|
|
306
|
-
type: 'string',
|
|
307
|
-
enum: ['user_reported_failure', 'verification_failure'],
|
|
198
|
+
'Every node that should carry this behavior once the work is done. Update the plan if they change, for example after replacing nodes.',
|
|
308
199
|
},
|
|
309
200
|
},
|
|
310
|
-
required: ['
|
|
201
|
+
required: ['id', 'description'],
|
|
311
202
|
additionalProperties: false,
|
|
312
203
|
},
|
|
313
204
|
},
|
|
@@ -319,7 +210,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
319
210
|
{
|
|
320
211
|
name: 'framer_verify_interactions',
|
|
321
212
|
description:
|
|
322
|
-
'
|
|
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.',
|
|
323
214
|
inputSchema: {
|
|
324
215
|
type: 'object',
|
|
325
216
|
properties: {
|
|
@@ -327,103 +218,16 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
327
218
|
checks: {
|
|
328
219
|
type: 'array',
|
|
329
220
|
minItems: 1,
|
|
330
|
-
maxItems:
|
|
221
|
+
maxItems: 50,
|
|
331
222
|
items: {
|
|
332
223
|
type: 'object',
|
|
333
224
|
properties: {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
},
|
|
339
|
-
behaviorId: {
|
|
340
|
-
type: 'string',
|
|
341
|
-
minLength: 1,
|
|
342
|
-
maxLength: 120,
|
|
343
|
-
},
|
|
344
|
-
triggerNodeId: {
|
|
345
|
-
type: 'string',
|
|
346
|
-
minLength: 1,
|
|
347
|
-
maxLength: 200,
|
|
348
|
-
},
|
|
349
|
-
event: {
|
|
350
|
-
type: 'string',
|
|
351
|
-
minLength: 1,
|
|
352
|
-
maxLength: 80,
|
|
353
|
-
},
|
|
354
|
-
expectedDestination: {
|
|
355
|
-
type: 'string',
|
|
356
|
-
minLength: 1,
|
|
357
|
-
maxLength: 500,
|
|
358
|
-
},
|
|
359
|
-
targetNodeId: {
|
|
360
|
-
type: 'string',
|
|
361
|
-
minLength: 1,
|
|
362
|
-
maxLength: 200,
|
|
363
|
-
},
|
|
364
|
-
componentNodeId: {
|
|
365
|
-
type: 'string',
|
|
366
|
-
minLength: 1,
|
|
367
|
-
maxLength: 200,
|
|
368
|
-
},
|
|
369
|
-
instanceNodeIds: {
|
|
370
|
-
type: 'array',
|
|
371
|
-
maxItems: 50,
|
|
372
|
-
uniqueItems: true,
|
|
373
|
-
items: {
|
|
374
|
-
type: 'string',
|
|
375
|
-
minLength: 1,
|
|
376
|
-
maxLength: 200,
|
|
377
|
-
},
|
|
378
|
-
},
|
|
379
|
-
openVariantNodeId: {
|
|
380
|
-
type: 'string',
|
|
381
|
-
minLength: 1,
|
|
382
|
-
maxLength: 200,
|
|
383
|
-
},
|
|
384
|
-
closedVariantNodeId: {
|
|
385
|
-
type: 'string',
|
|
386
|
-
minLength: 1,
|
|
387
|
-
maxLength: 200,
|
|
388
|
-
},
|
|
389
|
-
openAnswerNodeId: {
|
|
390
|
-
type: 'string',
|
|
391
|
-
minLength: 1,
|
|
392
|
-
maxLength: 200,
|
|
393
|
-
},
|
|
394
|
-
closedAnswerNodeId: {
|
|
395
|
-
type: 'string',
|
|
396
|
-
minLength: 1,
|
|
397
|
-
maxLength: 200,
|
|
398
|
-
},
|
|
399
|
-
semanticAssessment: {
|
|
400
|
-
type: 'object',
|
|
401
|
-
properties: {
|
|
402
|
-
verdict: {
|
|
403
|
-
type: 'string',
|
|
404
|
-
enum: ['verified', 'contradicted', 'unknown'],
|
|
405
|
-
},
|
|
406
|
-
rationale: {
|
|
407
|
-
type: 'string',
|
|
408
|
-
minLength: 10,
|
|
409
|
-
maxLength: 1200,
|
|
410
|
-
},
|
|
411
|
-
evidenceNodeIds: {
|
|
412
|
-
type: 'array',
|
|
413
|
-
maxItems: 50,
|
|
414
|
-
uniqueItems: true,
|
|
415
|
-
items: {
|
|
416
|
-
type: 'string',
|
|
417
|
-
minLength: 1,
|
|
418
|
-
maxLength: 200,
|
|
419
|
-
},
|
|
420
|
-
},
|
|
421
|
-
},
|
|
422
|
-
required: ['verdict', 'rationale', 'evidenceNodeIds'],
|
|
423
|
-
additionalProperties: false,
|
|
424
|
-
},
|
|
225
|
+
behaviorId: { type: 'string', minLength: 1, maxLength: 120 },
|
|
226
|
+
nodeId: { type: 'string', minLength: 1, maxLength: 200 },
|
|
227
|
+
property: { type: 'string', minLength: 1, maxLength: 120 },
|
|
228
|
+
contains: { type: 'string', minLength: 1, maxLength: 500 },
|
|
425
229
|
},
|
|
426
|
-
required: ['behaviorId'],
|
|
230
|
+
required: ['behaviorId', 'nodeId', 'property'],
|
|
427
231
|
additionalProperties: false,
|
|
428
232
|
},
|
|
429
233
|
},
|
|
@@ -477,6 +281,33 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
477
281
|
additionalProperties: false,
|
|
478
282
|
},
|
|
479
283
|
},
|
|
284
|
+
{
|
|
285
|
+
name: 'framer_add_svg',
|
|
286
|
+
description:
|
|
287
|
+
'Create an SVG layer from markup you write and place it inside a parent node. Use it for geometric and decorative artwork such as line work, starbursts, rings, sparkles, wireframe shapes, dividers, and simple marks. Use icon sets for icons, real text for text, and images for photos. Returns the new SVG node id; check it in a screenshot. To revise its markup later, use framer_write with setAttributes({ svg }) on that node.',
|
|
288
|
+
inputSchema: {
|
|
289
|
+
type: 'object',
|
|
290
|
+
properties: {
|
|
291
|
+
svg: {
|
|
292
|
+
type: 'string',
|
|
293
|
+
minLength: 1,
|
|
294
|
+
maxLength: MAX_SVG_MARKUP_CHARS,
|
|
295
|
+
description:
|
|
296
|
+
'One self-contained <svg> element with a viewBox. No scripts, event handlers, foreignObject, or external references.',
|
|
297
|
+
},
|
|
298
|
+
parentId: { type: 'string', minLength: 1, maxLength: 200 },
|
|
299
|
+
index: { type: 'integer', minimum: 0 },
|
|
300
|
+
name: { type: 'string', minLength: 1, maxLength: 120 },
|
|
301
|
+
attributes: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
description:
|
|
304
|
+
'Optional Framer layer attributes for the SVG node, for example {"position":"absolute","top":"40px","left":"0px","width":"320px","height":"320px","rotation":12,"opacity":0.6}. Defaults to position relative at the SVG\'s own size.',
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
required: ['svg', 'parentId'],
|
|
308
|
+
additionalProperties: false,
|
|
309
|
+
},
|
|
310
|
+
},
|
|
480
311
|
{
|
|
481
312
|
name: 'framer_read_code_file',
|
|
482
313
|
description:
|
|
@@ -646,6 +477,50 @@ function readGuidanceFile(projectRoot, relativePath, remainingBytes, section) {
|
|
|
646
477
|
};
|
|
647
478
|
}
|
|
648
479
|
|
|
480
|
+
const CORE_GUIDE_SECTIONS = [
|
|
481
|
+
{ file: 'how-projects-work', section: 'Scope Types' },
|
|
482
|
+
{ file: 'how-projects-work', section: 'Replicas' },
|
|
483
|
+
{ file: 'how-projects-work', section: 'Transforms' },
|
|
484
|
+
{ file: 'implementation-strategy', section: 'Determining Strategy' },
|
|
485
|
+
{ file: 'implementation-strategy', section: 'Edit Strategy' },
|
|
486
|
+
{ file: 'implementation-strategy', section: 'Update Loop' },
|
|
487
|
+
];
|
|
488
|
+
const ON_DEMAND_GUIDE_FILES = [
|
|
489
|
+
{ file: 'how-projects-work', sections: true },
|
|
490
|
+
{ file: 'implementation-strategy', sections: true },
|
|
491
|
+
{ file: 'core-examples', sections: false },
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
// Lists the topic guides that are not part of every prompt, keyed by
|
|
495
|
+
// "file#Section" (or "file" for a whole file), from the files on disk.
|
|
496
|
+
function onDemandGuideCatalog(projectRoot) {
|
|
497
|
+
const catalog = new Map();
|
|
498
|
+
for (const { file, sections } of ON_DEMAND_GUIDE_FILES) {
|
|
499
|
+
const relativePath = `prompt/${file}.md`;
|
|
500
|
+
const absolutePath = path.join(projectRoot, relativePath);
|
|
501
|
+
if (!fs.existsSync(absolutePath)) continue;
|
|
502
|
+
if (!sections) {
|
|
503
|
+
catalog.set(file, { root: projectRoot, path: relativePath });
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const headings = fs.readFileSync(absolutePath, 'utf8')
|
|
507
|
+
.split('\n')
|
|
508
|
+
.filter((line) => line.startsWith('## '))
|
|
509
|
+
.map((line) => line.slice(3).trim())
|
|
510
|
+
.filter((heading) => !CORE_GUIDE_SECTIONS.some(
|
|
511
|
+
(core) => core.file === file && core.section === heading,
|
|
512
|
+
));
|
|
513
|
+
for (const heading of headings) {
|
|
514
|
+
catalog.set(`${file}#${heading}`, {
|
|
515
|
+
root: projectRoot,
|
|
516
|
+
path: relativePath,
|
|
517
|
+
section: heading,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return catalog;
|
|
522
|
+
}
|
|
523
|
+
|
|
649
524
|
function markdownSection(markdown, heading) {
|
|
650
525
|
const lines = markdown.split('\n');
|
|
651
526
|
const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
|
@@ -742,7 +617,7 @@ function normalizeCodeFilePath(value) {
|
|
|
742
617
|
return filePath;
|
|
743
618
|
}
|
|
744
619
|
|
|
745
|
-
function
|
|
620
|
+
function structuredResultError(result, fallbackCode, fallbackMessage) {
|
|
746
621
|
const error = isRecord(result?.error) ? result.error : {};
|
|
747
622
|
const code =
|
|
748
623
|
typeof error.code === 'string' && error.code.trim()
|
|
@@ -753,7 +628,7 @@ function codeFileResultError(result, fallbackCode, fallbackMessage) {
|
|
|
753
628
|
? error.message.trim().slice(0, 2_000)
|
|
754
629
|
: fallbackMessage;
|
|
755
630
|
const statusCode =
|
|
756
|
-
code === 'FRAMER_CODE_FILE_NOT_FOUND'
|
|
631
|
+
code === 'FRAMER_CODE_FILE_NOT_FOUND' || code === 'FRAMER_SVG_PARENT_NOT_FOUND'
|
|
757
632
|
? 404
|
|
758
633
|
: code === 'FRAMER_CODE_FILE_VERSION_CONFLICT'
|
|
759
634
|
? 409
|
|
@@ -779,7 +654,7 @@ function normalizeVerificationValues(values, allowed) {
|
|
|
779
654
|
.filter((value) => allowed.includes(value));
|
|
780
655
|
}
|
|
781
656
|
|
|
782
|
-
function normalizeTaskPlan(rawArguments
|
|
657
|
+
function normalizeTaskPlan(rawArguments) {
|
|
783
658
|
const summary = String(rawArguments.summary || '').trim().slice(0, 1000);
|
|
784
659
|
const domains = normalizeVerificationValues(
|
|
785
660
|
rawArguments.domains,
|
|
@@ -798,19 +673,7 @@ function normalizeTaskPlan(rawArguments, inheritedRejectedMechanisms = []) {
|
|
|
798
673
|
// The model's declared verification is authoritative; domains describe the
|
|
799
674
|
// work and never add checks on their own.
|
|
800
675
|
const verification = new Set(['structural', ...requestedVerification]);
|
|
801
|
-
const behaviorPlan = normalizeBehaviorPlan(
|
|
802
|
-
rawArguments,
|
|
803
|
-
inheritedRejectedMechanisms,
|
|
804
|
-
);
|
|
805
|
-
if (
|
|
806
|
-
domains.includes('interactions')
|
|
807
|
-
&& behaviorPlan.behaviors.length === 0
|
|
808
|
-
) {
|
|
809
|
-
throw toolError(
|
|
810
|
-
'FRAMER_BEHAVIOR_CONTRACT_REQUIRED',
|
|
811
|
-
'Interaction work requires observable behaviors and one compatible mechanism decision per behavior.',
|
|
812
|
-
);
|
|
813
|
-
}
|
|
676
|
+
const behaviorPlan = normalizeBehaviorPlan(rawArguments);
|
|
814
677
|
if (behaviorPlan.behaviors.length > 0) {
|
|
815
678
|
if (!domains.includes('interactions')) domains.push('interactions');
|
|
816
679
|
verification.add('interactions');
|
|
@@ -936,6 +799,28 @@ function authoritativeNodeIdsFromAssignment(assignment) {
|
|
|
936
799
|
]);
|
|
937
800
|
}
|
|
938
801
|
|
|
802
|
+
const LITERAL_TOKEN_TYPES = new Set([
|
|
803
|
+
'StringLiteral',
|
|
804
|
+
'NoSubstitutionTemplate',
|
|
805
|
+
'TemplateHead',
|
|
806
|
+
'TemplateMiddle',
|
|
807
|
+
'TemplateTail',
|
|
808
|
+
'SingleLineComment',
|
|
809
|
+
'MultiLineComment',
|
|
810
|
+
'RegularExpressionLiteral',
|
|
811
|
+
]);
|
|
812
|
+
|
|
813
|
+
// Blanks string, template, comment, and regex text so data such as image URLs
|
|
814
|
+
// or product copy is not mistaken for a forbidden global. Template
|
|
815
|
+
// substitutions stay visible because they are code.
|
|
816
|
+
function codeOutsideLiterals(code) {
|
|
817
|
+
let visible = '';
|
|
818
|
+
for (const token of jsTokens(code)) {
|
|
819
|
+
visible += LITERAL_TOKEN_TYPES.has(token.type) ? ' ' : token.value;
|
|
820
|
+
}
|
|
821
|
+
return visible;
|
|
822
|
+
}
|
|
823
|
+
|
|
939
824
|
function validateCode(value, { write = false, allowPublishing = false } = {}) {
|
|
940
825
|
const code = String(value || '').trim();
|
|
941
826
|
if (!code) {
|
|
@@ -944,7 +829,7 @@ function validateCode(value, { write = false, allowPublishing = false } = {}) {
|
|
|
944
829
|
'Framer Agent code is required.',
|
|
945
830
|
);
|
|
946
831
|
}
|
|
947
|
-
if (UNSAFE_EXEC_PATTERN.test(code)) {
|
|
832
|
+
if (UNSAFE_EXEC_PATTERN.test(codeOutsideLiterals(code))) {
|
|
948
833
|
throw toolError(
|
|
949
834
|
'FRAMER_AGENT_CODE_UNSAFE',
|
|
950
835
|
'Framer Agent code may use only the provided framer, state, and console globals.',
|
|
@@ -968,6 +853,35 @@ function validateCode(value, { write = false, allowPublishing = false } = {}) {
|
|
|
968
853
|
return code;
|
|
969
854
|
}
|
|
970
855
|
|
|
856
|
+
function normalizeSvgMarkup(value) {
|
|
857
|
+
const svg = String(value || '').trim();
|
|
858
|
+
if (!/^<svg\b[\s\S]*<\/svg>$/i.test(svg) || svg.length > MAX_SVG_MARKUP_CHARS) {
|
|
859
|
+
throw toolError(
|
|
860
|
+
'FRAMER_SVG_INVALID',
|
|
861
|
+
`Provide one <svg>…</svg> element of at most ${MAX_SVG_MARKUP_CHARS} characters.`,
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
if (UNSAFE_SVG_PATTERN.test(svg)) {
|
|
865
|
+
throw toolError(
|
|
866
|
+
'FRAMER_SVG_UNSAFE',
|
|
867
|
+
'SVG markup must be self-contained: no scripts, event handlers, foreignObject, or external references.',
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
return svg;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function normalizeSvgAttributes(value) {
|
|
874
|
+
if (value === undefined) return {};
|
|
875
|
+
if (!isRecord(value) || Object.keys(value).length > MAX_SVG_ATTRIBUTES) {
|
|
876
|
+
throw toolError(
|
|
877
|
+
'FRAMER_SVG_ATTRIBUTES_INVALID',
|
|
878
|
+
`attributes must be an object with at most ${MAX_SVG_ATTRIBUTES} Framer layer attributes.`,
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
const { svg: _ignored, ...attributes } = value;
|
|
882
|
+
return attributes;
|
|
883
|
+
}
|
|
884
|
+
|
|
971
885
|
function extractJsonArray(value) {
|
|
972
886
|
const text = String(value || '').trim();
|
|
973
887
|
const start = text.indexOf('[');
|
|
@@ -1246,32 +1160,9 @@ export function createFramerAgentToolRuntime({
|
|
|
1246
1160
|
);
|
|
1247
1161
|
}
|
|
1248
1162
|
const allowPublishing = assignment?.context?.permissions?.publish === true;
|
|
1249
|
-
const inheritedRejectedMechanisms =
|
|
1250
|
-
assignment?.context?.repairContext?.rejectedMechanisms
|
|
1251
|
-
|| assignment?.repairContext?.rejectedMechanisms
|
|
1252
|
-
|| [];
|
|
1253
|
-
const authorizedInstanceContainerIds = uniqueStrings(
|
|
1254
|
-
assignment?.context?.permissions?.scopedComponentArtifacts
|
|
1255
|
-
?.instanceContainerNodeIds
|
|
1256
|
-
|| assignment?.context?.contextGrant?.writableNodeIds
|
|
1257
|
-
|| [],
|
|
1258
|
-
);
|
|
1259
1163
|
const activePagePath = normalizePagePath(
|
|
1260
1164
|
assignment?.context?.activePagePath,
|
|
1261
1165
|
);
|
|
1262
|
-
const semanticVerifierMode =
|
|
1263
|
-
['off', 'shadow', 'enforce'].includes(
|
|
1264
|
-
String(
|
|
1265
|
-
assignment?.context?.harnessAuthority?.semanticVerifierMode
|
|
1266
|
-
|| env.DEXTER_FRAMER_SEMANTIC_VERIFIER_MODE
|
|
1267
|
-
|| '',
|
|
1268
|
-
).trim(),
|
|
1269
|
-
)
|
|
1270
|
-
? String(
|
|
1271
|
-
assignment?.context?.harnessAuthority?.semanticVerifierMode
|
|
1272
|
-
|| env.DEXTER_FRAMER_SEMANTIC_VERIFIER_MODE,
|
|
1273
|
-
).trim()
|
|
1274
|
-
: 'enforce';
|
|
1275
1166
|
const authoritativeNodeIds =
|
|
1276
1167
|
authoritativeNodeIdsFromAssignment(assignment);
|
|
1277
1168
|
let sessionId = null;
|
|
@@ -1303,20 +1194,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1303
1194
|
let lastMutationAt = null;
|
|
1304
1195
|
let screenshotCalls = 0;
|
|
1305
1196
|
let interactionVerificationCalls = 0;
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
behaviors: [],
|
|
1311
|
-
budgetExhaustedBehaviorIds: [],
|
|
1312
|
-
verifiedAt: null,
|
|
1313
|
-
};
|
|
1314
|
-
const representativeVerifiedBehaviorIds = new Set();
|
|
1315
|
-
const representativeMutatedBehaviorIds = new Set();
|
|
1316
|
-
const representativeRepairNodeIds = new Set();
|
|
1317
|
-
const representativeRepairBehaviorByNode = new Map();
|
|
1318
|
-
const representativeEvidenceBehaviorByNode = new Map();
|
|
1319
|
-
const behaviorVerificationAttempts = new Map();
|
|
1197
|
+
// Latest result per behavior/node/property, with the operation sequence it
|
|
1198
|
+
// was observed at, so checks from several calls (or pages) accumulate.
|
|
1199
|
+
const interactionCheckResults = new Map();
|
|
1200
|
+
let interactionVerifiedAt = null;
|
|
1320
1201
|
const mutationReceipts = [];
|
|
1321
1202
|
let lastScreenshotSequence = 0;
|
|
1322
1203
|
let lastScreenshotAttemptSequence = 0;
|
|
@@ -1326,6 +1207,8 @@ export function createFramerAgentToolRuntime({
|
|
|
1326
1207
|
const screenshotFailures = [];
|
|
1327
1208
|
let taskPlan = null;
|
|
1328
1209
|
const verificationEvidence = new Map();
|
|
1210
|
+
let guideCatalog = new Map();
|
|
1211
|
+
const guidesRead = [];
|
|
1329
1212
|
let guidanceMetadata = {
|
|
1330
1213
|
loaded: false,
|
|
1331
1214
|
contentHash: null,
|
|
@@ -1334,6 +1217,7 @@ export function createFramerAgentToolRuntime({
|
|
|
1334
1217
|
bytes: 0,
|
|
1335
1218
|
};
|
|
1336
1219
|
let promptMetadata = {
|
|
1220
|
+
promptBytes: 0,
|
|
1337
1221
|
doctrineLoaded: false,
|
|
1338
1222
|
doctrineVersion: null,
|
|
1339
1223
|
};
|
|
@@ -1343,6 +1227,7 @@ export function createFramerAgentToolRuntime({
|
|
|
1343
1227
|
const framerAgentVersion = installedFramerAgentVersion();
|
|
1344
1228
|
let operationSequence = 0;
|
|
1345
1229
|
let lastMutationSequence = 0;
|
|
1230
|
+
let lastCodeMutationSequence = 0;
|
|
1346
1231
|
let lastInspectionSequence = 0;
|
|
1347
1232
|
const abortController = new AbortController();
|
|
1348
1233
|
|
|
@@ -1649,6 +1534,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1649
1534
|
if (!fs.existsSync(projectRoot)) {
|
|
1650
1535
|
return { content: '', ...guidanceMetadata };
|
|
1651
1536
|
}
|
|
1537
|
+
// Every run gets the guidance an edit always needs: principles, command
|
|
1538
|
+
// syntax, tool APIs, scope and replica rules, and the edit loop. Topic
|
|
1539
|
+
// guides (CMS, components, creation strategy, examples, ...) are listed
|
|
1540
|
+
// in a catalog and read on demand through framer_instructions.
|
|
1652
1541
|
const requestedFiles = [
|
|
1653
1542
|
{ root: skillRoot, path: 'SKILL.md', label: 'SKILL.md' },
|
|
1654
1543
|
{ root: projectRoot, path: 'index.md', label: 'project/index.md' },
|
|
@@ -1657,24 +1546,19 @@ export function createFramerAgentToolRuntime({
|
|
|
1657
1546
|
path: 'project-inventory.md',
|
|
1658
1547
|
label: 'project/project-inventory.md',
|
|
1659
1548
|
},
|
|
1660
|
-
{ root: projectRoot, path: 'prompt/core-principles.md' },
|
|
1661
|
-
{ root: projectRoot, path: 'prompt/core-examples.md' },
|
|
1662
|
-
{ root: projectRoot, path: 'prompt/implementation-strategy.md' },
|
|
1663
|
-
{ root: projectRoot, path: 'prompt/updating-the-project.md' },
|
|
1664
|
-
{ root: projectRoot, path: 'prompt/tools.md' },
|
|
1665
1549
|
{ root: projectRoot, path: 'prompt/overview.md' },
|
|
1550
|
+
{ root: projectRoot, path: 'prompt/core-principles.md' },
|
|
1666
1551
|
{ root: projectRoot, path: 'prompt/guardrails.md' },
|
|
1667
1552
|
{ root: projectRoot, path: 'prompt/critical-reminders.md' },
|
|
1668
|
-
// The byte cap clips how-projects-work.md before its 3D transform
|
|
1669
|
-
// guidance, so that section loads on its own first.
|
|
1670
|
-
{
|
|
1671
|
-
root: projectRoot,
|
|
1672
|
-
path: 'prompt/how-projects-work.md',
|
|
1673
|
-
section: 'Transforms',
|
|
1674
|
-
label: 'prompt/how-projects-work.md#transforms',
|
|
1675
|
-
},
|
|
1676
1553
|
{ root: projectRoot, path: 'prompt/design-rules.md' },
|
|
1677
|
-
{ root: projectRoot, path: 'prompt/
|
|
1554
|
+
{ root: projectRoot, path: 'prompt/updating-the-project.md' },
|
|
1555
|
+
{ root: projectRoot, path: 'prompt/tools.md' },
|
|
1556
|
+
...CORE_GUIDE_SECTIONS.map(({ file, section }) => ({
|
|
1557
|
+
root: projectRoot,
|
|
1558
|
+
path: `prompt/${file}.md`,
|
|
1559
|
+
section,
|
|
1560
|
+
label: `prompt/${file}.md#${section}`,
|
|
1561
|
+
})),
|
|
1678
1562
|
];
|
|
1679
1563
|
const files = [];
|
|
1680
1564
|
let remainingBytes = MAX_GUIDANCE_BYTES;
|
|
@@ -1693,9 +1577,19 @@ export function createFramerAgentToolRuntime({
|
|
|
1693
1577
|
remainingBytes -= Buffer.byteLength(loaded.content, 'utf8');
|
|
1694
1578
|
if (remainingBytes <= 0) break;
|
|
1695
1579
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1580
|
+
guideCatalog = onDemandGuideCatalog(projectRoot);
|
|
1581
|
+
const catalogText = guideCatalog.size > 0
|
|
1582
|
+
? [
|
|
1583
|
+
'## On-demand guides',
|
|
1584
|
+
'',
|
|
1585
|
+
'Read a guide with framer_instructions({ guides: [id] }) before work that depends on it.',
|
|
1586
|
+
...[...guideCatalog.keys()].map((id) => `- ${id}`),
|
|
1587
|
+
].join('\n')
|
|
1588
|
+
: '';
|
|
1589
|
+
const content = [
|
|
1590
|
+
...files.map((file) => `## ${file.path}\n\n${file.content}`),
|
|
1591
|
+
...(catalogText ? [catalogText] : []),
|
|
1592
|
+
].join('\n\n');
|
|
1699
1593
|
const inventory = files.find(
|
|
1700
1594
|
(file) => file.path.endsWith('project-inventory.md'),
|
|
1701
1595
|
)?.content || '';
|
|
@@ -1716,8 +1610,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1716
1610
|
const configurePromptContext = ({
|
|
1717
1611
|
doctrineLoaded = false,
|
|
1718
1612
|
doctrineVersion = null,
|
|
1613
|
+
promptBytes = 0,
|
|
1719
1614
|
} = {}) => {
|
|
1720
1615
|
promptMetadata = {
|
|
1616
|
+
promptBytes: Math.max(0, Number(promptBytes) || 0),
|
|
1721
1617
|
doctrineLoaded: doctrineLoaded === true,
|
|
1722
1618
|
doctrineVersion:
|
|
1723
1619
|
typeof doctrineVersion === 'string' && doctrineVersion.trim()
|
|
@@ -1858,9 +1754,14 @@ export function createFramerAgentToolRuntime({
|
|
|
1858
1754
|
return result;
|
|
1859
1755
|
};
|
|
1860
1756
|
|
|
1757
|
+
// Selected layers that do not resolve to native nodes (stale selections,
|
|
1758
|
+
// aliases, layers inside component instances) no longer stop the run. The
|
|
1759
|
+
// model is told which targets to locate by name, and the reason is kept for
|
|
1760
|
+
// debugging.
|
|
1761
|
+
let unresolvedTargets = [];
|
|
1861
1762
|
const validateAuthoritativeNodeIds = async () => {
|
|
1862
1763
|
await ensureSession();
|
|
1863
|
-
if (authoritativeNodeIds.length === 0) return;
|
|
1764
|
+
if (authoritativeNodeIds.length === 0) return { unresolvedTargets };
|
|
1864
1765
|
if (nodeValidationPromise) return nodeValidationPromise;
|
|
1865
1766
|
nodeValidationPromise = (async () => {
|
|
1866
1767
|
reportActivity({
|
|
@@ -1872,9 +1773,13 @@ export function createFramerAgentToolRuntime({
|
|
|
1872
1773
|
[
|
|
1873
1774
|
`const requestedIds = ${JSON.stringify(authoritativeNodeIds)};`,
|
|
1874
1775
|
'const resolved = await Promise.all(requestedIds.map(async (requestedId) => {',
|
|
1875
|
-
'
|
|
1876
|
-
|
|
1877
|
-
|
|
1776
|
+
' try {',
|
|
1777
|
+
' const node = await framer.agent.getNode({ id: requestedId },',
|
|
1778
|
+
` { pagePath: ${JSON.stringify(activePagePath)} });`,
|
|
1779
|
+
' return { requestedId, resolvedId: node?.id ?? null, error: node ? null : "not found on the active page" };',
|
|
1780
|
+
' } catch (error) {',
|
|
1781
|
+
' return { requestedId, resolvedId: null, error: String(error?.message || error).slice(0, 200) };',
|
|
1782
|
+
' }',
|
|
1878
1783
|
'}));',
|
|
1879
1784
|
'console.log(JSON.stringify({ resolved }));',
|
|
1880
1785
|
].join('\n'),
|
|
@@ -1884,26 +1789,30 @@ export function createFramerAgentToolRuntime({
|
|
|
1884
1789
|
const resolved = Array.isArray(parsed?.resolved)
|
|
1885
1790
|
? parsed.resolved
|
|
1886
1791
|
: [];
|
|
1887
|
-
|
|
1792
|
+
unresolvedTargets = authoritativeNodeIds.flatMap((requestedId) => {
|
|
1888
1793
|
const match = resolved.find(
|
|
1889
1794
|
(entry) =>
|
|
1890
1795
|
isRecord(entry)
|
|
1891
1796
|
&& entry.requestedId === requestedId,
|
|
1892
1797
|
);
|
|
1893
|
-
|
|
1798
|
+
if (match?.resolvedId === requestedId) return [];
|
|
1799
|
+
return [{
|
|
1800
|
+
id: requestedId,
|
|
1801
|
+
reason: !match
|
|
1802
|
+
? 'not checked'
|
|
1803
|
+
: match.resolvedId
|
|
1804
|
+
? `resolves to ${match.resolvedId}`
|
|
1805
|
+
: String(match.error || 'not found on the active page'),
|
|
1806
|
+
}];
|
|
1894
1807
|
});
|
|
1895
|
-
if (invalid.length > 0) {
|
|
1896
|
-
throw toolError(
|
|
1897
|
-
'FRAMER_AGENT_NODE_ID_INVALID',
|
|
1898
|
-
`Dexter received Framer targets that do not resolve to exact native node IDs: ${invalid.join(', ')}. Re-select the affected layers in Framer and try again.`,
|
|
1899
|
-
409,
|
|
1900
|
-
);
|
|
1901
|
-
}
|
|
1902
1808
|
reportActivity({
|
|
1903
1809
|
kind: 'status_update',
|
|
1904
1810
|
source: 'framer-agent',
|
|
1905
|
-
message:
|
|
1811
|
+
message: unresolvedTargets.length > 0
|
|
1812
|
+
? 'Some selected layers could not be matched exactly, so Dexter will locate them by name.'
|
|
1813
|
+
: 'The selected Framer layers are ready.',
|
|
1906
1814
|
});
|
|
1815
|
+
return { unresolvedTargets };
|
|
1907
1816
|
})().catch((error) => {
|
|
1908
1817
|
nodeValidationPromise = null;
|
|
1909
1818
|
throw error;
|
|
@@ -1954,9 +1863,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1954
1863
|
}
|
|
1955
1864
|
};
|
|
1956
1865
|
|
|
1957
|
-
const markMutation = () => {
|
|
1866
|
+
const markMutation = ({ code = false } = {}) => {
|
|
1958
1867
|
operationSequence += 1;
|
|
1959
1868
|
lastMutationSequence = operationSequence;
|
|
1869
|
+
if (code) lastCodeMutationSequence = operationSequence;
|
|
1960
1870
|
lastMutationAt = new Date().toISOString();
|
|
1961
1871
|
};
|
|
1962
1872
|
|
|
@@ -1972,30 +1882,35 @@ export function createFramerAgentToolRuntime({
|
|
|
1972
1882
|
? {
|
|
1973
1883
|
behaviors: taskPlan.behaviors.map((behavior) => ({
|
|
1974
1884
|
...behavior,
|
|
1975
|
-
|
|
1976
|
-
expectedDestinations: [...behavior.expectedDestinations],
|
|
1977
|
-
})),
|
|
1978
|
-
mechanisms: taskPlan.mechanisms.map((decision) => ({
|
|
1979
|
-
...decision,
|
|
1980
|
-
targetNodeIds: [...decision.targetNodeIds],
|
|
1981
|
-
})),
|
|
1982
|
-
rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
|
|
1983
|
-
...entry,
|
|
1984
|
-
})),
|
|
1985
|
-
compatibility: taskPlan.compatibility.map((entry) => ({
|
|
1986
|
-
...entry,
|
|
1987
|
-
requiredCapabilities: [...entry.requiredCapabilities],
|
|
1885
|
+
targetNodeIds: [...behavior.targetNodeIds],
|
|
1988
1886
|
})),
|
|
1989
1887
|
}
|
|
1990
1888
|
: {}),
|
|
1991
1889
|
};
|
|
1992
1890
|
};
|
|
1891
|
+
// Only checks observed after the latest mutation describe the current project.
|
|
1892
|
+
const freshInteractionChecks = () =>
|
|
1893
|
+
[...interactionCheckResults.values()]
|
|
1894
|
+
.filter((entry) => entry.sequence > lastMutationSequence)
|
|
1895
|
+
.map((entry) => entry.result);
|
|
1896
|
+
const interactionSummary = () =>
|
|
1897
|
+
summarizeBehaviorVerification(taskPlan, freshInteractionChecks());
|
|
1898
|
+
const interactionStatus = () => {
|
|
1899
|
+
if (!taskPlan?.behaviors?.length || mutationCalls === 0) return 'not_required';
|
|
1900
|
+
return interactionSummary().status;
|
|
1901
|
+
};
|
|
1993
1902
|
const synchronized = () =>
|
|
1994
1903
|
mutationCalls === 0 || lastInspectionSequence > lastMutationSequence;
|
|
1995
1904
|
const verificationCheckCompleted = (check) => {
|
|
1996
1905
|
if (mutationCalls === 0) return true;
|
|
1997
1906
|
if (check === 'structural') return synchronized();
|
|
1998
|
-
|
|
1907
|
+
if (check === 'interactions') {
|
|
1908
|
+
return ['verified', 'not_required'].includes(interactionStatus());
|
|
1909
|
+
}
|
|
1910
|
+
// Canvas edits cannot change a code file, so only code edits stale it.
|
|
1911
|
+
const staleAfter =
|
|
1912
|
+
check === 'code' ? lastCodeMutationSequence : lastMutationSequence;
|
|
1913
|
+
return Number(verificationEvidence.get(check) || 0) > staleAfter;
|
|
1999
1914
|
};
|
|
2000
1915
|
const visualVerificationRequired = () =>
|
|
2001
1916
|
requiredVerificationChecks().includes('visual');
|
|
@@ -2013,12 +1928,19 @@ export function createFramerAgentToolRuntime({
|
|
|
2013
1928
|
const requiredChecks = () => [
|
|
2014
1929
|
...new Set(['structural', ...requiredVerificationChecks()]),
|
|
2015
1930
|
];
|
|
1931
|
+
// Checks the model attempted but that live evidence could not confirm. They
|
|
1932
|
+
// are reported as unverified and do not fail the run.
|
|
1933
|
+
const unavailableChecks = () => [
|
|
1934
|
+
...(visualVerificationStatus() === 'unavailable' ? ['visual'] : []),
|
|
1935
|
+
...(interactionStatus() === 'unverified' ? ['interactions'] : []),
|
|
1936
|
+
];
|
|
2016
1937
|
const missingVerificationChecks = () => {
|
|
2017
1938
|
if (mutationCalls === 0) return [];
|
|
2018
1939
|
const missing = [];
|
|
2019
1940
|
if (!taskPlan) missing.push('task-plan');
|
|
1941
|
+
const unavailable = unavailableChecks();
|
|
2020
1942
|
for (const check of requiredChecks()) {
|
|
2021
|
-
if (check
|
|
1943
|
+
if (unavailable.includes(check)) continue;
|
|
2022
1944
|
if (!verificationCheckCompleted(check)) missing.push(check);
|
|
2023
1945
|
}
|
|
2024
1946
|
return missing;
|
|
@@ -2046,48 +1968,17 @@ export function createFramerAgentToolRuntime({
|
|
|
2046
1968
|
? {
|
|
2047
1969
|
interactionVerificationCalls,
|
|
2048
1970
|
interactionVerification: {
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
: 'unknown',
|
|
2054
|
-
checks: interactionVerification.checks.map((check) => ({
|
|
2055
|
-
...check,
|
|
2056
|
-
contradictions: [...check.contradictions],
|
|
2057
|
-
unknowns: [...check.unknowns],
|
|
2058
|
-
observations: check.observations.map((observation) => ({
|
|
2059
|
-
...observation,
|
|
2060
|
-
})),
|
|
2061
|
-
nextSafeActions: [...check.nextSafeActions],
|
|
2062
|
-
...(check.semanticAssessment
|
|
2063
|
-
? {
|
|
2064
|
-
semanticAssessment: {
|
|
2065
|
-
...check.semanticAssessment,
|
|
2066
|
-
evidenceNodeIds: [
|
|
2067
|
-
...check.semanticAssessment.evidenceNodeIds,
|
|
2068
|
-
],
|
|
2069
|
-
},
|
|
2070
|
-
}
|
|
2071
|
-
: {}),
|
|
2072
|
-
})),
|
|
2073
|
-
behaviors: interactionVerification.behaviors.map((behavior) => ({
|
|
2074
|
-
...behavior,
|
|
2075
|
-
contradictions: [...behavior.contradictions],
|
|
2076
|
-
unknowns: [...behavior.unknowns],
|
|
2077
|
-
})),
|
|
2078
|
-
semanticVerifierMode,
|
|
2079
|
-
budgetExhaustedBehaviorIds: [
|
|
2080
|
-
...interactionVerification.budgetExhaustedBehaviorIds,
|
|
2081
|
-
],
|
|
2082
|
-
verifiedAt: interactionVerification.verifiedAt,
|
|
1971
|
+
status: interactionStatus(),
|
|
1972
|
+
behaviors: taskPlan?.behaviors?.length ? interactionSummary().behaviors : [],
|
|
1973
|
+
checks: freshInteractionChecks().map((check) => ({ ...check })),
|
|
1974
|
+
verifiedAt: interactionVerifiedAt,
|
|
2083
1975
|
},
|
|
2084
1976
|
}
|
|
2085
1977
|
: {}),
|
|
2086
1978
|
taskPlan: taskPlanSnapshot(),
|
|
2087
1979
|
requiredChecks: requiredChecks(),
|
|
2088
1980
|
completedChecks: requiredChecks().filter(verificationCheckCompleted),
|
|
2089
|
-
unavailableChecks:
|
|
2090
|
-
visualVerificationStatus() === 'unavailable' ? ['visual'] : [],
|
|
1981
|
+
unavailableChecks: unavailableChecks(),
|
|
2091
1982
|
missingChecks: missingVerificationChecks(),
|
|
2092
1983
|
lastMutationAt,
|
|
2093
1984
|
lastInspectionAt,
|
|
@@ -2109,49 +2000,58 @@ export function createFramerAgentToolRuntime({
|
|
|
2109
2000
|
};
|
|
2110
2001
|
};
|
|
2111
2002
|
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2003
|
+
const withUnavailableChecks = (completion) => {
|
|
2004
|
+
const unavailable = unavailableChecks();
|
|
2005
|
+
if (unavailable.length === 0) return completion;
|
|
2006
|
+
const unverifiedBehaviors = interactionSummary().behaviors
|
|
2007
|
+
.filter((behavior) => behavior.verdict === 'unknown')
|
|
2008
|
+
.map((behavior) => `${behavior.behaviorId} (${behavior.unknowns[0] || 'not confirmed'})`);
|
|
2009
|
+
return {
|
|
2010
|
+
...completion,
|
|
2011
|
+
checks: [
|
|
2012
|
+
...(Array.isArray(completion?.checks) ? completion.checks : []),
|
|
2013
|
+
...(unavailable.includes('visual')
|
|
2014
|
+
? [{
|
|
2119
2015
|
command: 'Visual verification',
|
|
2120
2016
|
status: 'skipped',
|
|
2121
2017
|
output:
|
|
2122
2018
|
'Screenshots of the final canvas could not be delivered to the model, so the visual result is unverified.',
|
|
2123
|
-
}
|
|
2124
|
-
],
|
|
2125
|
-
|
|
2126
|
-
|
|
2019
|
+
}]
|
|
2020
|
+
: []),
|
|
2021
|
+
...(unavailable.includes('interactions')
|
|
2022
|
+
? [{
|
|
2023
|
+
command: 'Interaction verification',
|
|
2024
|
+
status: 'skipped',
|
|
2025
|
+
output:
|
|
2026
|
+
`The live project did not expose evidence for: ${unverifiedBehaviors.join('; ')}. These behaviors are unverified.`,
|
|
2027
|
+
}]
|
|
2028
|
+
: []),
|
|
2029
|
+
],
|
|
2030
|
+
};
|
|
2031
|
+
};
|
|
2127
2032
|
|
|
2128
2033
|
const finalizeCompletion = (completion) => {
|
|
2129
2034
|
if (completion?.status !== 'completed' || mutationCalls === 0) {
|
|
2130
2035
|
return completion;
|
|
2131
2036
|
}
|
|
2132
|
-
if (verification().completed) return
|
|
2037
|
+
if (verification().completed) return withUnavailableChecks(completion);
|
|
2133
2038
|
const missing = missingVerificationChecks().map((check) => {
|
|
2134
2039
|
if (check === 'task-plan') return 'a harness task and verification plan';
|
|
2135
2040
|
if (check === 'structural') return 'a final structural read';
|
|
2136
2041
|
if (check === 'visual') return 'post-mutation screenshot verification';
|
|
2137
2042
|
return `post-mutation ${check} verification`;
|
|
2138
2043
|
});
|
|
2139
|
-
const interactionDetails =
|
|
2140
|
-
.filter((behavior) => behavior.verdict
|
|
2141
|
-
.map((behavior) =>
|
|
2142
|
-
|
|
2143
|
-
behavior.contradictions[0]
|
|
2144
|
-
|| behavior.unknowns[0]
|
|
2145
|
-
|| 'no conclusive live evidence';
|
|
2146
|
-
return `${behavior.behaviorId}=${behavior.verdict} (${detail})`;
|
|
2147
|
-
});
|
|
2044
|
+
const interactionDetails = (taskPlan?.behaviors?.length ? interactionSummary().behaviors : [])
|
|
2045
|
+
.filter((behavior) => behavior.verdict === 'contradicted' || !behavior.checked)
|
|
2046
|
+
.map((behavior) =>
|
|
2047
|
+
`${behavior.behaviorId}=${behavior.verdict === 'contradicted' ? behavior.contradictions[0] : 'not checked after the latest change'}`);
|
|
2148
2048
|
const reason = [
|
|
2149
2049
|
`The harness ended before completing ${missing.join(' and ')}.`,
|
|
2150
2050
|
...(interactionDetails.length > 0
|
|
2151
2051
|
? [`Interaction evidence: ${interactionDetails.join('; ')}.`]
|
|
2152
2052
|
: []),
|
|
2153
2053
|
].join(' ');
|
|
2154
|
-
const failed =
|
|
2054
|
+
const failed = withUnavailableChecks(completion);
|
|
2155
2055
|
return {
|
|
2156
2056
|
...failed,
|
|
2157
2057
|
status: 'failed',
|
|
@@ -2170,6 +2070,22 @@ export function createFramerAgentToolRuntime({
|
|
|
2170
2070
|
};
|
|
2171
2071
|
};
|
|
2172
2072
|
|
|
2073
|
+
// A model that reports failure without changing anything and without a
|
|
2074
|
+
// refused plan or edit gave up on its own. It gets one prompt to continue.
|
|
2075
|
+
const unblockedFailurePrompt = (completion) => {
|
|
2076
|
+
if (completion?.status !== 'failed' || mutationCalls > 0) return null;
|
|
2077
|
+
const lastGate = [...toolCalls]
|
|
2078
|
+
.reverse()
|
|
2079
|
+
.find((call) => call.category === 'plan' || call.category === 'mutation');
|
|
2080
|
+
if (lastGate?.status === 'failed') return null;
|
|
2081
|
+
return [
|
|
2082
|
+
'You returned failed without changing the Framer project, and no tool refused the work.',
|
|
2083
|
+
'Nothing in this run is blocking you and there is no time limit.',
|
|
2084
|
+
'Continue the same task from the current project state: plan it, apply the change, verify it, then return the required JSON outcome.',
|
|
2085
|
+
'Return failed or blocked only for a concrete blocker you can name.',
|
|
2086
|
+
].join(' ');
|
|
2087
|
+
};
|
|
2088
|
+
|
|
2173
2089
|
const verificationRepairPrompt = () => {
|
|
2174
2090
|
const current = verification();
|
|
2175
2091
|
if (current.completed || mutationCalls === 0) return null;
|
|
@@ -2185,27 +2101,33 @@ export function createFramerAgentToolRuntime({
|
|
|
2185
2101
|
missing.push(
|
|
2186
2102
|
'capture and inspect a screenshot of the affected canvas after the latest mutation',
|
|
2187
2103
|
);
|
|
2104
|
+
} else if (check === 'interactions') {
|
|
2105
|
+
const summary = interactionSummary();
|
|
2106
|
+
const unchecked = summary.behaviors
|
|
2107
|
+
.filter((behavior) => !behavior.checked)
|
|
2108
|
+
.map((behavior) => behavior.uncheckedTargets.length
|
|
2109
|
+
? `${behavior.behaviorId} on ${behavior.uncheckedTargets.join(', ')}`
|
|
2110
|
+
: behavior.behaviorId);
|
|
2111
|
+
const contradicted = summary.behaviors
|
|
2112
|
+
.filter((behavior) => behavior.verdict === 'contradicted')
|
|
2113
|
+
.map((behavior) => behavior.contradictions[0]);
|
|
2114
|
+
missing.push([
|
|
2115
|
+
unchecked.length
|
|
2116
|
+
? `call framer_verify_interactions for ${unchecked.join('; ')}, naming the Framer property that carries each behavior`
|
|
2117
|
+
: '',
|
|
2118
|
+
contradicted.length
|
|
2119
|
+
? `fix or explain these mismatches: ${contradicted.join('; ')}`
|
|
2120
|
+
: '',
|
|
2121
|
+
].filter(Boolean).join(' and '));
|
|
2188
2122
|
} else {
|
|
2189
|
-
|
|
2190
|
-
.map((behavior) =>
|
|
2191
|
-
`${behavior.behaviorId}:${behavior.verdict}`)
|
|
2192
|
-
.join(', ');
|
|
2193
|
-
const exhausted =
|
|
2194
|
-
interactionVerification.budgetExhaustedBehaviorIds.length > 0;
|
|
2195
|
-
missing.push(
|
|
2196
|
-
check === 'interactions'
|
|
2197
|
-
? exhausted
|
|
2198
|
-
? `report interaction verification as incomplete without further mutation or repeated verification (${interactionState || 'unknown'})`
|
|
2199
|
-
: `call framer_verify_interactions with canonical evidence node IDs for every declared behavior and chosen mechanism; repair only contradicted evidence, while unknown evidence requires a different read or grounded semantic assessment (${interactionState || 'not yet checked'})`
|
|
2200
|
-
: `perform a focused read tagged with verifies:["${check}"] after the latest mutation`,
|
|
2201
|
-
);
|
|
2123
|
+
missing.push(`perform a focused read tagged with verifies:["${check}"] after the latest mutation`);
|
|
2202
2124
|
}
|
|
2203
2125
|
}
|
|
2204
2126
|
return [
|
|
2205
2127
|
'Continue the same Framer task from its current project state.',
|
|
2206
2128
|
'Do not redo successful work.',
|
|
2207
2129
|
`Before returning, ${missing.join(' and ')}.`,
|
|
2208
|
-
'
|
|
2130
|
+
'If the live project does not expose a property, report that behavior as unverified rather than changing working parts. Then return the required JSON outcome.',
|
|
2209
2131
|
].join(' ');
|
|
2210
2132
|
};
|
|
2211
2133
|
|
|
@@ -2219,6 +2141,7 @@ export function createFramerAgentToolRuntime({
|
|
|
2219
2141
|
'Use framer_read_project for supported project queries such as screenshots.',
|
|
2220
2142
|
'To place a user attachment in the project, call framer_upload_attachment with its id from context.referenceAssetFiles and use the returned url as the image fill. Never upload user files to external services.',
|
|
2221
2143
|
'Use framer_query_images for approved stock imagery only after checking user attachments and suitable existing project images.',
|
|
2144
|
+
'Use framer_add_svg for geometric or decorative artwork you draw as SVG markup; use icon sets for icons and real text for text.',
|
|
2222
2145
|
'Use framer_apply_changes for layout and styling. Read its diagnostics, verify the result, and repair concrete issues.',
|
|
2223
2146
|
'Use framer_read_code_file and framer_update_code_file for existing code components and overrides instead of generic JavaScript.',
|
|
2224
2147
|
'If framer_write is rejected as unsafe, do not retry JavaScript variants. Switch to framer_apply_changes or report the concrete blocker.',
|
|
@@ -2250,109 +2173,13 @@ export function createFramerAgentToolRuntime({
|
|
|
2250
2173
|
.update(`${name}\n${JSON.stringify(rawArguments)}`)
|
|
2251
2174
|
.digest('hex');
|
|
2252
2175
|
|
|
2253
|
-
const
|
|
2254
|
-
const commands = summarizeFramerChanges(changes).commands;
|
|
2255
|
-
if (
|
|
2256
|
-
commands.length === 0
|
|
2257
|
-
|| commands.some((command) =>
|
|
2258
|
-
command.operation !== 'set'
|
|
2259
|
-
|| !command.nodeId
|
|
2260
|
-
|| !representativeRepairNodeIds.has(command.nodeId))
|
|
2261
|
-
) {
|
|
2262
|
-
return [];
|
|
2263
|
-
}
|
|
2264
|
-
return uniqueStrings(commands.map((command) => command.nodeId));
|
|
2265
|
-
};
|
|
2266
|
-
|
|
2267
|
-
const requireTaskPlanForMutation = ({ changes = '' } = {}) => {
|
|
2176
|
+
const requireTaskPlanForMutation = () => {
|
|
2268
2177
|
if (!taskPlan) {
|
|
2269
2178
|
throw toolError(
|
|
2270
2179
|
'FRAMER_HARNESS_PLAN_REQUIRED',
|
|
2271
2180
|
'Inspect the live Framer target, then call framer_plan_task before making a mutation.',
|
|
2272
2181
|
);
|
|
2273
2182
|
}
|
|
2274
|
-
const repairTargets = representativeRepairTargets(changes);
|
|
2275
|
-
const isRepresentativeRepair = repairTargets.length > 0;
|
|
2276
|
-
const rejectedSelectedMechanisms = taskPlan.behaviors.filter((behavior) => {
|
|
2277
|
-
const selected = taskPlan.mechanisms.find((decision) =>
|
|
2278
|
-
decision.behaviorId === behavior.id);
|
|
2279
|
-
return selected && taskPlan.rejectedMechanisms.some((rejected) =>
|
|
2280
|
-
rejected.mechanism === selected.mechanism
|
|
2281
|
-
&& (
|
|
2282
|
-
rejected.behaviorId === behavior.id
|
|
2283
|
-
|| rejected.behaviorSignature === behavior.signature
|
|
2284
|
-
));
|
|
2285
|
-
});
|
|
2286
|
-
if (rejectedSelectedMechanisms.length > 0) {
|
|
2287
|
-
throw toolError(
|
|
2288
|
-
'FRAMER_BEHAVIOR_MECHANISM_REPLAN_REQUIRED',
|
|
2289
|
-
`Choose a different mechanism for ${rejectedSelectedMechanisms.map((behavior) => behavior.id).join(', ')} before making another mutation.`,
|
|
2290
|
-
);
|
|
2291
|
-
}
|
|
2292
|
-
const changedNodeIds = new Set(
|
|
2293
|
-
summarizeFramerChanges(changes).commands
|
|
2294
|
-
.flatMap((command) => [
|
|
2295
|
-
command.nodeId,
|
|
2296
|
-
command.sourceNodeId,
|
|
2297
|
-
])
|
|
2298
|
-
.filter(Boolean),
|
|
2299
|
-
);
|
|
2300
|
-
const rawChanges = String(changes || '');
|
|
2301
|
-
const pendingRepresentative = taskPlan.behaviors
|
|
2302
|
-
.filter((behavior) => behavior.expectedTargets > 1)
|
|
2303
|
-
.map((behavior) => ({
|
|
2304
|
-
behavior,
|
|
2305
|
-
decision: taskPlan.mechanisms.find((decision) =>
|
|
2306
|
-
decision.behaviorId === behavior.id),
|
|
2307
|
-
}))
|
|
2308
|
-
.filter(({ decision }) => decision?.representativeFirst)
|
|
2309
|
-
.filter(({ behavior }) =>
|
|
2310
|
-
!representativeVerifiedBehaviorIds.has(behavior.id));
|
|
2311
|
-
for (const { behavior, decision } of pendingRepresentative) {
|
|
2312
|
-
const touchedTargets = decision.targetNodeIds.filter((nodeId) =>
|
|
2313
|
-
rawChanges.includes(nodeId));
|
|
2314
|
-
const touchedEvidence = [...changedNodeIds].filter((nodeId) =>
|
|
2315
|
-
representativeEvidenceBehaviorByNode.get(nodeId) === behavior.id);
|
|
2316
|
-
if (
|
|
2317
|
-
touchedTargets.length > 1
|
|
2318
|
-
|| (
|
|
2319
|
-
touchedTargets.length === 1
|
|
2320
|
-
&& touchedTargets[0] !== decision.representativeTargetNodeId
|
|
2321
|
-
)
|
|
2322
|
-
) {
|
|
2323
|
-
throw toolError(
|
|
2324
|
-
'FRAMER_BEHAVIOR_REPRESENTATIVE_MUTATION_REQUIRED',
|
|
2325
|
-
`Implement only representative target ${decision.representativeTargetNodeId} for ${behavior.id} before changing the remaining targets.`,
|
|
2326
|
-
);
|
|
2327
|
-
}
|
|
2328
|
-
if (touchedTargets.length === 0 && touchedEvidence.length === 0) {
|
|
2329
|
-
continue;
|
|
2330
|
-
}
|
|
2331
|
-
if (!representativeMutatedBehaviorIds.has(behavior.id)) continue;
|
|
2332
|
-
const attempt = behaviorVerificationAttempts.get(behavior.id);
|
|
2333
|
-
const repairForBehavior =
|
|
2334
|
-
isRepresentativeRepair
|
|
2335
|
-
&& repairTargets.every((nodeId) =>
|
|
2336
|
-
representativeRepairBehaviorByNode.get(nodeId) === behavior.id);
|
|
2337
|
-
if (
|
|
2338
|
-
!repairForBehavior
|
|
2339
|
-
|| Number(attempt?.repairs || 0)
|
|
2340
|
-
>= MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
|
|
2341
|
-
) {
|
|
2342
|
-
throw toolError(
|
|
2343
|
-
'FRAMER_BEHAVIOR_REPRESENTATIVE_VERIFICATION_REQUIRED',
|
|
2344
|
-
`Verify representative target ${decision.representativeTargetNodeId} for ${behavior.id} before changing it again or batching the remaining targets.`,
|
|
2345
|
-
);
|
|
2346
|
-
}
|
|
2347
|
-
behaviorVerificationAttempts.set(behavior.id, {
|
|
2348
|
-
...attempt,
|
|
2349
|
-
repairs: Number(attempt?.repairs || 0) + 1,
|
|
2350
|
-
});
|
|
2351
|
-
}
|
|
2352
|
-
return pendingRepresentative
|
|
2353
|
-
.filter(({ decision }) =>
|
|
2354
|
-
rawChanges.includes(decision.representativeTargetNodeId))
|
|
2355
|
-
.map(({ behavior }) => behavior.id);
|
|
2356
2183
|
};
|
|
2357
2184
|
|
|
2358
2185
|
async function invokeUnsafe(name, rawArguments = {}) {
|
|
@@ -2374,10 +2201,7 @@ export function createFramerAgentToolRuntime({
|
|
|
2374
2201
|
'Framer Agent tool arguments must be an object.',
|
|
2375
2202
|
);
|
|
2376
2203
|
}
|
|
2377
|
-
const mutation =
|
|
2378
|
-
name === 'framer_apply_changes'
|
|
2379
|
-
|| name === 'framer_update_code_file'
|
|
2380
|
-
|| name === 'framer_write';
|
|
2204
|
+
const mutation = MUTATION_TOOL_NAMES.has(name);
|
|
2381
2205
|
const category =
|
|
2382
2206
|
name === 'framer_plan_task'
|
|
2383
2207
|
? 'plan'
|
|
@@ -2423,9 +2247,7 @@ export function createFramerAgentToolRuntime({
|
|
|
2423
2247
|
name,
|
|
2424
2248
|
phase: 'started',
|
|
2425
2249
|
message:
|
|
2426
|
-
|
|
2427
|
-
|| name === 'framer_update_code_file'
|
|
2428
|
-
|| name === 'framer_write'
|
|
2250
|
+
mutation
|
|
2429
2251
|
? 'Updating the Framer project'
|
|
2430
2252
|
: name === 'framer_plan_task'
|
|
2431
2253
|
? 'Planning the Framer work and verification'
|
|
@@ -2444,7 +2266,40 @@ export function createFramerAgentToolRuntime({
|
|
|
2444
2266
|
const queries = Array.isArray(rawArguments.queries)
|
|
2445
2267
|
? uniqueStrings(rawArguments.queries, 8)
|
|
2446
2268
|
: [];
|
|
2447
|
-
|
|
2269
|
+
const guideIds = Array.isArray(rawArguments.guides)
|
|
2270
|
+
? uniqueStrings(rawArguments.guides, 6)
|
|
2271
|
+
: [];
|
|
2272
|
+
if (guideIds.length > 0) {
|
|
2273
|
+
const guides = guideIds.flatMap((id) => {
|
|
2274
|
+
const entry = guideCatalog.get(id);
|
|
2275
|
+
const loaded = entry
|
|
2276
|
+
? readGuidanceFile(
|
|
2277
|
+
entry.root,
|
|
2278
|
+
entry.path,
|
|
2279
|
+
MAX_GUIDANCE_FILE_BYTES,
|
|
2280
|
+
entry.section,
|
|
2281
|
+
)
|
|
2282
|
+
: null;
|
|
2283
|
+
if (!loaded) return [];
|
|
2284
|
+
if (!guidesRead.includes(id)) guidesRead.push(id);
|
|
2285
|
+
return [{ id, content: loaded.content }];
|
|
2286
|
+
});
|
|
2287
|
+
const unknown = guideIds.filter(
|
|
2288
|
+
(id) => !guides.some((guide) => guide.id === id),
|
|
2289
|
+
);
|
|
2290
|
+
result = {
|
|
2291
|
+
stdout: JSON.stringify({
|
|
2292
|
+
guides,
|
|
2293
|
+
...(unknown.length > 0
|
|
2294
|
+
? {
|
|
2295
|
+
unknownGuides: unknown,
|
|
2296
|
+
availableGuides: [...guideCatalog.keys()],
|
|
2297
|
+
}
|
|
2298
|
+
: {}),
|
|
2299
|
+
}),
|
|
2300
|
+
stderr: '',
|
|
2301
|
+
};
|
|
2302
|
+
} else if (queries.length > 0 && !instructionLookupUnavailable) {
|
|
2448
2303
|
try {
|
|
2449
2304
|
result = await executeCli(
|
|
2450
2305
|
['docs', ...queries],
|
|
@@ -2487,22 +2342,7 @@ export function createFramerAgentToolRuntime({
|
|
|
2487
2342
|
'Inspect the live Framer target before declaring the task and verification plan.',
|
|
2488
2343
|
);
|
|
2489
2344
|
}
|
|
2490
|
-
const nextPlan = normalizeTaskPlan(
|
|
2491
|
-
rawArguments,
|
|
2492
|
-
inheritedRejectedMechanisms,
|
|
2493
|
-
);
|
|
2494
|
-
const previousMechanisms = new Map(
|
|
2495
|
-
(taskPlan?.mechanisms || []).map((decision) => [
|
|
2496
|
-
decision.behaviorId,
|
|
2497
|
-
JSON.stringify({
|
|
2498
|
-
mechanism: decision.mechanism,
|
|
2499
|
-
targetNodeIds: decision.targetNodeIds,
|
|
2500
|
-
representativeTargetNodeId:
|
|
2501
|
-
decision.representativeTargetNodeId,
|
|
2502
|
-
}),
|
|
2503
|
-
]),
|
|
2504
|
-
);
|
|
2505
|
-
const behaviorPlan = mergeBehaviorPlans(taskPlan, nextPlan);
|
|
2345
|
+
const nextPlan = normalizeTaskPlan(rawArguments);
|
|
2506
2346
|
taskPlan = {
|
|
2507
2347
|
summary: nextPlan.summary,
|
|
2508
2348
|
domains: uniqueStrings([
|
|
@@ -2513,280 +2353,55 @@ export function createFramerAgentToolRuntime({
|
|
|
2513
2353
|
...(taskPlan?.verification || []),
|
|
2514
2354
|
...nextPlan.verification,
|
|
2515
2355
|
]),
|
|
2516
|
-
...
|
|
2356
|
+
...mergeBehaviorPlans(taskPlan, nextPlan),
|
|
2517
2357
|
};
|
|
2518
|
-
for (const decision of taskPlan.mechanisms) {
|
|
2519
|
-
const prior = previousMechanisms.get(decision.behaviorId);
|
|
2520
|
-
const current = JSON.stringify({
|
|
2521
|
-
mechanism: decision.mechanism,
|
|
2522
|
-
targetNodeIds: decision.targetNodeIds,
|
|
2523
|
-
representativeTargetNodeId:
|
|
2524
|
-
decision.representativeTargetNodeId,
|
|
2525
|
-
});
|
|
2526
|
-
if (prior && prior !== current) {
|
|
2527
|
-
representativeVerifiedBehaviorIds.delete(decision.behaviorId);
|
|
2528
|
-
representativeMutatedBehaviorIds.delete(decision.behaviorId);
|
|
2529
|
-
behaviorVerificationAttempts.delete(decision.behaviorId);
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
2358
|
result = {
|
|
2533
|
-
stdout: JSON.stringify({
|
|
2534
|
-
plan: {
|
|
2535
|
-
summary: taskPlan.summary,
|
|
2536
|
-
domains: [...taskPlan.domains],
|
|
2537
|
-
verification: [...taskPlan.verification],
|
|
2538
|
-
behaviors: taskPlan.behaviors.map((behavior) => ({
|
|
2539
|
-
...behavior,
|
|
2540
|
-
expectedContent: [...behavior.expectedContent],
|
|
2541
|
-
})),
|
|
2542
|
-
mechanisms: taskPlan.mechanisms.map((decision) => ({
|
|
2543
|
-
...decision,
|
|
2544
|
-
})),
|
|
2545
|
-
compatibility: taskPlan.compatibility.map((entry) => ({
|
|
2546
|
-
...entry,
|
|
2547
|
-
requiredCapabilities: [...entry.requiredCapabilities],
|
|
2548
|
-
})),
|
|
2549
|
-
rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
|
|
2550
|
-
...entry,
|
|
2551
|
-
})),
|
|
2552
|
-
},
|
|
2553
|
-
instruction:
|
|
2554
|
-
'Implement one representative first when required, verify it, then complete coverage and gather every derived verification result after the final mutation.',
|
|
2555
|
-
}),
|
|
2359
|
+
stdout: JSON.stringify({ plan: taskPlanSnapshot() }),
|
|
2556
2360
|
stderr: '',
|
|
2557
2361
|
};
|
|
2558
2362
|
} else if (name === 'framer_verify_interactions') {
|
|
2559
|
-
if (mutationCalls === 0) {
|
|
2560
|
-
throw toolError(
|
|
2561
|
-
'FRAMER_HARNESS_INTERACTION_MUTATION_REQUIRED',
|
|
2562
|
-
'Verify interactions only after the relevant Framer mutation has succeeded.',
|
|
2563
|
-
);
|
|
2564
|
-
}
|
|
2565
2363
|
if (!taskPlan?.behaviors?.length) {
|
|
2566
2364
|
throw toolError(
|
|
2567
2365
|
'FRAMER_BEHAVIOR_CONTRACT_REQUIRED',
|
|
2568
|
-
'Declare
|
|
2569
|
-
);
|
|
2570
|
-
}
|
|
2571
|
-
const checks = normalizeBehaviorEvidence(
|
|
2572
|
-
rawArguments.checks,
|
|
2573
|
-
taskPlan,
|
|
2574
|
-
);
|
|
2575
|
-
const exhaustedBehaviorIds = uniqueStrings(
|
|
2576
|
-
checks
|
|
2577
|
-
.map((check) => check.behaviorId)
|
|
2578
|
-
.filter((behaviorId) =>
|
|
2579
|
-
behaviorVerificationAttempts.get(behaviorId)?.exhausted),
|
|
2580
|
-
);
|
|
2581
|
-
if (exhaustedBehaviorIds.length > 0) {
|
|
2582
|
-
throw toolError(
|
|
2583
|
-
'FRAMER_INTERACTION_VERIFICATION_BUDGET_EXHAUSTED',
|
|
2584
|
-
`Interaction verification is exhausted for ${exhaustedBehaviorIds.join(', ')}. Stop retrying unchanged evidence and report the work as partially verified.`,
|
|
2366
|
+
'Declare the behaviors to confirm in framer_plan_task before verifying them.',
|
|
2585
2367
|
);
|
|
2586
2368
|
}
|
|
2369
|
+
const checks = normalizeBehaviorEvidence(rawArguments.checks, taskPlan);
|
|
2587
2370
|
const pagePath = normalizePagePath(rawArguments.pagePath);
|
|
2588
|
-
const requestedIds = evidenceNodeIds(checks);
|
|
2589
|
-
const componentIds = uniqueStrings(
|
|
2590
|
-
checks.map((check) => check.componentNodeId),
|
|
2591
|
-
);
|
|
2592
2371
|
const liveResult = await execCode(
|
|
2593
2372
|
[
|
|
2594
|
-
`const requestedIds = ${JSON.stringify(
|
|
2595
|
-
|
|
2596
|
-
'
|
|
2597
|
-
'
|
|
2598
|
-
|
|
2599
|
-
'
|
|
2600
|
-
|
|
2601
|
-
' parentId: await framer.agent.getParentNode({ id },',
|
|
2602
|
-
` { pagePath: ${JSON.stringify(pagePath)} }).then((node) => node?.id ?? null).catch(() => null),`,
|
|
2603
|
-
' ancestorIds: await framer.agent.getAncestors({ id },',
|
|
2604
|
-
` { pagePath: ${JSON.stringify(pagePath)} }).then((nodes) => Array.isArray(nodes) ? nodes.map((node) => node?.id).filter(Boolean) : []).catch(() => []),`,
|
|
2605
|
-
' }))),',
|
|
2606
|
-
' componentIds.length > 0',
|
|
2607
|
-
' ? framer.agent.readComponentControls({ componentIds },',
|
|
2608
|
-
` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null)`,
|
|
2609
|
-
' : null,',
|
|
2610
|
-
']);',
|
|
2611
|
-
'console.log(JSON.stringify({ nodes, componentControls }, null, 2));',
|
|
2373
|
+
`const requestedIds = ${JSON.stringify(evidenceNodeIds(checks))};`,
|
|
2374
|
+
'const nodes = await Promise.all(requestedIds.map(async (id) => ({',
|
|
2375
|
+
' id,',
|
|
2376
|
+
' node: await framer.agent.serialize({ id, depth: 1 },',
|
|
2377
|
+
` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null),`,
|
|
2378
|
+
'})));',
|
|
2379
|
+
'console.log(JSON.stringify({ nodes }, null, 2));',
|
|
2612
2380
|
].join('\n'),
|
|
2613
2381
|
);
|
|
2614
2382
|
const parsed = parseStructuredOutput(liveResult.stdout);
|
|
2615
|
-
const
|
|
2383
|
+
const nodes = new Map(
|
|
2616
2384
|
(Array.isArray(parsed?.nodes) ? parsed.nodes : [])
|
|
2617
|
-
.filter((entry) => isRecord(entry) && typeof entry.id === 'string')
|
|
2618
|
-
.map((entry) => [entry.id,
|
|
2619
|
-
node: entry.node || null,
|
|
2620
|
-
parentId:
|
|
2621
|
-
typeof entry.parentId === 'string'
|
|
2622
|
-
? entry.parentId
|
|
2623
|
-
: null,
|
|
2624
|
-
ancestorIds: Array.isArray(entry.ancestorIds)
|
|
2625
|
-
? entry.ancestorIds
|
|
2626
|
-
: [],
|
|
2627
|
-
}]),
|
|
2385
|
+
.filter((entry) => isRecord(entry) && typeof entry.id === 'string' && entry.node)
|
|
2386
|
+
.map((entry) => [entry.id, entry.node]),
|
|
2628
2387
|
);
|
|
2629
|
-
const
|
|
2630
|
-
plan: taskPlan,
|
|
2631
|
-
checks,
|
|
2632
|
-
snapshots: nodesById,
|
|
2633
|
-
context: {
|
|
2634
|
-
lowLevelWriteCalls,
|
|
2635
|
-
authorizedInstanceContainerIds,
|
|
2636
|
-
componentControls: parsed?.componentControls || null,
|
|
2637
|
-
semanticVerifierMode,
|
|
2638
|
-
},
|
|
2639
|
-
});
|
|
2388
|
+
const results = verifyBehaviorChecks({ checks, nodes });
|
|
2640
2389
|
markInspection();
|
|
2641
2390
|
interactionVerificationCalls += 1;
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
for (const behaviorResult of verified.behaviors) {
|
|
2649
|
-
if (!checkedBehaviorIds.has(behaviorResult.behaviorId)) continue;
|
|
2650
|
-
const behaviorChecks = verified.checks.filter((check) =>
|
|
2651
|
-
check.behaviorId === behaviorResult.behaviorId);
|
|
2652
|
-
for (const check of behaviorChecks) {
|
|
2653
|
-
evidenceNodeIds([check]).forEach((nodeId) =>
|
|
2654
|
-
representativeEvidenceBehaviorByNode.set(
|
|
2655
|
-
nodeId,
|
|
2656
|
-
behaviorResult.behaviorId,
|
|
2657
|
-
));
|
|
2658
|
-
}
|
|
2659
|
-
const deterministicallyContradicted = behaviorChecks.some((check) =>
|
|
2660
|
-
check.deterministicVerdict === 'contradicted');
|
|
2661
|
-
const prior = behaviorVerificationAttempts.get(
|
|
2662
|
-
behaviorResult.behaviorId,
|
|
2663
|
-
) || {
|
|
2664
|
-
attempts: 0,
|
|
2665
|
-
unchangedEvidenceCount: 0,
|
|
2666
|
-
repairs: 0,
|
|
2667
|
-
fingerprint: null,
|
|
2668
|
-
exhausted: false,
|
|
2669
|
-
};
|
|
2670
|
-
if (behaviorResult.verdict === 'verified') {
|
|
2671
|
-
behaviorVerificationAttempts.delete(behaviorResult.behaviorId);
|
|
2672
|
-
Object.assign(behaviorResult, {
|
|
2673
|
-
verificationAttempts: prior.attempts + 1,
|
|
2674
|
-
verificationAttemptsRemaining: Math.max(
|
|
2675
|
-
0,
|
|
2676
|
-
MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR
|
|
2677
|
-
- prior.attempts
|
|
2678
|
-
- 1,
|
|
2679
|
-
),
|
|
2680
|
-
repairAttempts: prior.repairs,
|
|
2681
|
-
budgetExhausted: false,
|
|
2682
|
-
});
|
|
2683
|
-
continue;
|
|
2684
|
-
}
|
|
2685
|
-
const evidenceFingerprint = crypto
|
|
2686
|
-
.createHash('sha256')
|
|
2687
|
-
.update(JSON.stringify({
|
|
2688
|
-
mechanism: behaviorResult.mechanism,
|
|
2689
|
-
verdict: behaviorResult.verdict,
|
|
2690
|
-
contradictions: [...behaviorResult.contradictions].sort(),
|
|
2691
|
-
unknowns: [...behaviorResult.unknowns].sort(),
|
|
2692
|
-
checks: behaviorChecks.map((check) => ({
|
|
2693
|
-
verdict: check.verdict,
|
|
2694
|
-
nodeIds: evidenceNodeIds([check]).sort(),
|
|
2695
|
-
observations: check.observations,
|
|
2696
|
-
})),
|
|
2697
|
-
}))
|
|
2698
|
-
.digest('hex');
|
|
2699
|
-
const attempts = prior.attempts + 1;
|
|
2700
|
-
const unchangedEvidenceCount =
|
|
2701
|
-
prior.fingerprint === evidenceFingerprint
|
|
2702
|
-
? prior.unchangedEvidenceCount + 1
|
|
2703
|
-
: 1;
|
|
2704
|
-
const exhausted =
|
|
2705
|
-
attempts >= MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR
|
|
2706
|
-
|| unchangedEvidenceCount >= 2
|
|
2707
|
-
|| (
|
|
2708
|
-
behaviorResult.verdict === 'contradicted'
|
|
2709
|
-
&& prior.repairs >= MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
|
|
2710
|
-
);
|
|
2711
|
-
behaviorVerificationAttempts.set(behaviorResult.behaviorId, {
|
|
2712
|
-
attempts,
|
|
2713
|
-
unchangedEvidenceCount,
|
|
2714
|
-
repairs: prior.repairs,
|
|
2715
|
-
fingerprint: evidenceFingerprint,
|
|
2716
|
-
exhausted,
|
|
2717
|
-
});
|
|
2718
|
-
Object.assign(behaviorResult, {
|
|
2719
|
-
verificationAttempts: attempts,
|
|
2720
|
-
verificationAttemptsRemaining: Math.max(
|
|
2721
|
-
0,
|
|
2722
|
-
MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR - attempts,
|
|
2723
|
-
),
|
|
2724
|
-
repairAttempts: prior.repairs,
|
|
2725
|
-
budgetExhausted: exhausted,
|
|
2726
|
-
});
|
|
2727
|
-
for (const check of behaviorChecks) {
|
|
2728
|
-
check.repairAllowed =
|
|
2729
|
-
check.deterministicVerdict === 'contradicted'
|
|
2730
|
-
&& prior.repairs < MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
|
|
2731
|
-
&& !exhausted;
|
|
2732
|
-
if (!check.repairAllowed) continue;
|
|
2733
|
-
evidenceNodeIds([check]).forEach((nodeId) => {
|
|
2734
|
-
representativeRepairNodeIds.add(nodeId);
|
|
2735
|
-
representativeRepairBehaviorByNode.set(
|
|
2736
|
-
nodeId,
|
|
2737
|
-
behaviorResult.behaviorId,
|
|
2738
|
-
);
|
|
2739
|
-
});
|
|
2740
|
-
}
|
|
2741
|
-
if (
|
|
2742
|
-
!deterministicallyContradicted
|
|
2743
|
-
|| !exhausted
|
|
2744
|
-
|| !behaviorResult.mechanism
|
|
2745
|
-
) continue;
|
|
2746
|
-
const alreadyRejected = taskPlan.rejectedMechanisms.some((rejected) =>
|
|
2747
|
-
rejected.mechanism === behaviorResult.mechanism
|
|
2748
|
-
&& (
|
|
2749
|
-
rejected.behaviorId === behaviorResult.behaviorId
|
|
2750
|
-
|| rejected.behaviorSignature
|
|
2751
|
-
=== behaviorResult.behaviorSignature
|
|
2752
|
-
));
|
|
2753
|
-
if (alreadyRejected) continue;
|
|
2754
|
-
taskPlan = {
|
|
2755
|
-
...taskPlan,
|
|
2756
|
-
rejectedMechanisms: [
|
|
2757
|
-
...taskPlan.rejectedMechanisms,
|
|
2758
|
-
{
|
|
2759
|
-
behaviorId: behaviorResult.behaviorId,
|
|
2760
|
-
behaviorSignature: behaviorResult.behaviorSignature,
|
|
2761
|
-
mechanism: behaviorResult.mechanism,
|
|
2762
|
-
reason:
|
|
2763
|
-
`Repeated live evidence contradicted this mechanism: ${behaviorChecks
|
|
2764
|
-
.flatMap((check) => check.contradictions)
|
|
2765
|
-
.join('; ')
|
|
2766
|
-
.slice(0, 700)}`,
|
|
2767
|
-
source: 'verification_failure',
|
|
2768
|
-
},
|
|
2769
|
-
],
|
|
2770
|
-
};
|
|
2771
|
-
}
|
|
2772
|
-
const budgetExhaustedBehaviorIds = [...behaviorVerificationAttempts]
|
|
2773
|
-
.filter(([, attempt]) => attempt.exhausted)
|
|
2774
|
-
.map(([behaviorId]) => behaviorId);
|
|
2775
|
-
interactionVerification = {
|
|
2776
|
-
...verified,
|
|
2777
|
-
semanticVerifierMode,
|
|
2778
|
-
budgetExhaustedBehaviorIds,
|
|
2779
|
-
verifiedAt: new Date().toISOString(),
|
|
2780
|
-
};
|
|
2781
|
-
verified.representativeBehaviorIds.forEach((behaviorId) =>
|
|
2782
|
-
representativeVerifiedBehaviorIds.add(behaviorId));
|
|
2783
|
-
if (interactionVerification.completed) {
|
|
2784
|
-
verificationEvidence.set('interactions', operationSequence);
|
|
2785
|
-
} else {
|
|
2786
|
-
verificationEvidence.delete('interactions');
|
|
2391
|
+
interactionVerifiedAt = new Date().toISOString();
|
|
2392
|
+
for (const checkResult of results) {
|
|
2393
|
+
interactionCheckResults.set(
|
|
2394
|
+
`${checkResult.behaviorId}\n${checkResult.nodeId}\n${checkResult.property}`,
|
|
2395
|
+
{ sequence: operationSequence, result: checkResult },
|
|
2396
|
+
);
|
|
2787
2397
|
}
|
|
2398
|
+
const summary = interactionSummary();
|
|
2788
2399
|
result = {
|
|
2789
|
-
stdout: JSON.stringify(
|
|
2400
|
+
stdout: JSON.stringify({
|
|
2401
|
+
status: summary.status,
|
|
2402
|
+
checks: results,
|
|
2403
|
+
behaviors: summary.behaviors,
|
|
2404
|
+
}),
|
|
2790
2405
|
stderr: liveResult.stderr,
|
|
2791
2406
|
};
|
|
2792
2407
|
} else if (name === 'framer_read_project') {
|
|
@@ -2860,6 +2475,88 @@ export function createFramerAgentToolRuntime({
|
|
|
2860
2475
|
imageSearchCalls += 1;
|
|
2861
2476
|
} else if (name === 'framer_upload_attachment') {
|
|
2862
2477
|
result = await uploadAttachment(String(rawArguments.attachmentId || ''));
|
|
2478
|
+
} else if (name === 'framer_add_svg') {
|
|
2479
|
+
const svg = normalizeSvgMarkup(rawArguments.svg);
|
|
2480
|
+
const parentId = String(rawArguments.parentId || '').trim();
|
|
2481
|
+
if (!parentId) {
|
|
2482
|
+
throw toolError(
|
|
2483
|
+
'FRAMER_SVG_PARENT_REQUIRED',
|
|
2484
|
+
'framer_add_svg needs the id of the parent node.',
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
const index = Number.isInteger(rawArguments.index) && rawArguments.index >= 0
|
|
2488
|
+
? rawArguments.index
|
|
2489
|
+
: null;
|
|
2490
|
+
const svgName = String(rawArguments.name || '').trim().slice(0, 120) || 'SVG';
|
|
2491
|
+
const attributes = normalizeSvgAttributes(rawArguments.attributes);
|
|
2492
|
+
requireTaskPlanForMutation();
|
|
2493
|
+
// addSVG has no parent option and replaces a selected SVG, so clear
|
|
2494
|
+
// the selection, find the one new SVG node, then move it into place.
|
|
2495
|
+
result = await execCode(
|
|
2496
|
+
[
|
|
2497
|
+
`const svg = ${JSON.stringify(svg)};`,
|
|
2498
|
+
`const parentId = ${JSON.stringify(parentId)};`,
|
|
2499
|
+
`const index = ${JSON.stringify(index)};`,
|
|
2500
|
+
`const attributes = ${JSON.stringify({ name: svgName, position: 'relative', ...attributes })};`,
|
|
2501
|
+
'const fail = (code, message) => console.log(JSON.stringify({ ok: false, error: { code, message } }));',
|
|
2502
|
+
'const parent = await framer.getNode(parentId);',
|
|
2503
|
+
'if (!parent) {',
|
|
2504
|
+
' fail("FRAMER_SVG_PARENT_NOT_FOUND", `No node exists with id "${parentId}".`);',
|
|
2505
|
+
'} else {',
|
|
2506
|
+
' const previousIds = new Set((await framer.getNodesWithType("SVGNode")).map((node) => node.id));',
|
|
2507
|
+
' try { await framer.setSelection([]); } catch {}',
|
|
2508
|
+
' await framer.addSVG({ svg, name: attributes.name });',
|
|
2509
|
+
' let node = null;',
|
|
2510
|
+
' for (let attempt = 0; attempt < 6 && !node; attempt += 1) {',
|
|
2511
|
+
' if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, 150));',
|
|
2512
|
+
' const inserted = (await framer.getNodesWithType("SVGNode")).filter((candidate) => !previousIds.has(candidate.id));',
|
|
2513
|
+
' if (inserted.length === 1) node = inserted[0];',
|
|
2514
|
+
' }',
|
|
2515
|
+
' if (!node) {',
|
|
2516
|
+
' fail("FRAMER_SVG_NOT_INSERTED", "Framer did not expose exactly one new SVG node after inserting the markup.");',
|
|
2517
|
+
' } else {',
|
|
2518
|
+
' try {',
|
|
2519
|
+
' await framer.setParent(node.id, parentId, index ?? undefined);',
|
|
2520
|
+
' await node.setAttributes(attributes);',
|
|
2521
|
+
' const placedParent = await node.getParent();',
|
|
2522
|
+
' if (placedParent?.id !== parentId) throw new Error("The SVG did not land inside the requested parent.");',
|
|
2523
|
+
' const placed = await framer.getNode(node.id);',
|
|
2524
|
+
' console.log(JSON.stringify({',
|
|
2525
|
+
' ok: true,',
|
|
2526
|
+
' node: {',
|
|
2527
|
+
' id: node.id,',
|
|
2528
|
+
' name: placed?.name ?? attributes.name,',
|
|
2529
|
+
' parentId,',
|
|
2530
|
+
' position: placed?.position ?? null,',
|
|
2531
|
+
' width: placed?.width ?? null,',
|
|
2532
|
+
' height: placed?.height ?? null,',
|
|
2533
|
+
' },',
|
|
2534
|
+
' }));',
|
|
2535
|
+
' } catch (error) {',
|
|
2536
|
+
' await framer.removeNodes([node.id]).catch(() => undefined);',
|
|
2537
|
+
' fail("FRAMER_SVG_PLACEMENT_FAILED", String(error?.message || error));',
|
|
2538
|
+
' }',
|
|
2539
|
+
' }',
|
|
2540
|
+
'}',
|
|
2541
|
+
].join('\n'),
|
|
2542
|
+
);
|
|
2543
|
+
const parsed = parseStructuredOutput(result.stdout);
|
|
2544
|
+
if (!isRecord(parsed) || parsed.ok !== true) {
|
|
2545
|
+
throw structuredResultError(
|
|
2546
|
+
parsed,
|
|
2547
|
+
'FRAMER_SVG_PLACEMENT_FAILED',
|
|
2548
|
+
'Dexter could not place the SVG in the Framer project.',
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
mutationCalls += 1;
|
|
2552
|
+
markMutation();
|
|
2553
|
+
mutationReceipts.push({
|
|
2554
|
+
sequence: operationSequence,
|
|
2555
|
+
kind: 'framer_add_svg',
|
|
2556
|
+
nodeId: parsed.node?.id ?? null,
|
|
2557
|
+
parentId,
|
|
2558
|
+
svgHash: crypto.createHash('sha256').update(svg).digest('hex'),
|
|
2559
|
+
});
|
|
2863
2560
|
} else if (name === 'framer_read_code_file') {
|
|
2864
2561
|
const filePath = normalizeCodeFilePath(rawArguments.path);
|
|
2865
2562
|
result = await execCode(
|
|
@@ -2913,7 +2610,7 @@ export function createFramerAgentToolRuntime({
|
|
|
2913
2610
|
);
|
|
2914
2611
|
const parsed = parseStructuredOutput(result.stdout);
|
|
2915
2612
|
if (!isRecord(parsed) || parsed.ok !== true) {
|
|
2916
|
-
throw
|
|
2613
|
+
throw structuredResultError(
|
|
2917
2614
|
parsed,
|
|
2918
2615
|
'FRAMER_CODE_FILE_READ_FAILED',
|
|
2919
2616
|
`Dexter could not read ${filePath}.`,
|
|
@@ -3072,14 +2769,14 @@ export function createFramerAgentToolRuntime({
|
|
|
3072
2769
|
);
|
|
3073
2770
|
const parsed = parseStructuredOutput(result.stdout);
|
|
3074
2771
|
if (!isRecord(parsed) || parsed.ok !== true) {
|
|
3075
|
-
throw
|
|
2772
|
+
throw structuredResultError(
|
|
3076
2773
|
parsed,
|
|
3077
2774
|
'FRAMER_CODE_FILE_UPDATE_FAILED',
|
|
3078
2775
|
`Dexter could not update ${filePath}.`,
|
|
3079
2776
|
);
|
|
3080
2777
|
}
|
|
3081
2778
|
mutationCalls += 1;
|
|
3082
|
-
markMutation();
|
|
2779
|
+
markMutation({ code: true });
|
|
3083
2780
|
const mutationSequence = operationSequence;
|
|
3084
2781
|
markInspection({
|
|
3085
2782
|
verifies: parsed.typecheck?.passed === true ? ['code'] : [],
|
|
@@ -3116,8 +2813,7 @@ export function createFramerAgentToolRuntime({
|
|
|
3116
2813
|
'Framer Agent changes are required.',
|
|
3117
2814
|
);
|
|
3118
2815
|
}
|
|
3119
|
-
|
|
3120
|
-
requireTaskPlanForMutation({ changes });
|
|
2816
|
+
requireTaskPlanForMutation();
|
|
3121
2817
|
const pagePath = normalizePagePath(rawArguments.pagePath);
|
|
3122
2818
|
result = await execCode(
|
|
3123
2819
|
[
|
|
@@ -3128,8 +2824,6 @@ export function createFramerAgentToolRuntime({
|
|
|
3128
2824
|
mutationCalls += 1;
|
|
3129
2825
|
applyChangesCalls += 1;
|
|
3130
2826
|
markMutation();
|
|
3131
|
-
mutatedRepresentativeBehaviorIds.forEach((behaviorId) =>
|
|
3132
|
-
representativeMutatedBehaviorIds.add(behaviorId));
|
|
3133
2827
|
const parsedResult = parseStructuredOutput(result.stdout);
|
|
3134
2828
|
mutationReceipts.push({
|
|
3135
2829
|
sequence: operationSequence,
|
|
@@ -3192,9 +2886,7 @@ export function createFramerAgentToolRuntime({
|
|
|
3192
2886
|
name,
|
|
3193
2887
|
phase: 'completed',
|
|
3194
2888
|
message:
|
|
3195
|
-
|
|
3196
|
-
|| name === 'framer_update_code_file'
|
|
3197
|
-
|| name === 'framer_write'
|
|
2889
|
+
mutation
|
|
3198
2890
|
? 'Updated the Framer project'
|
|
3199
2891
|
: 'Finished inspecting the Framer project',
|
|
3200
2892
|
durationMs,
|
|
@@ -3203,6 +2895,9 @@ export function createFramerAgentToolRuntime({
|
|
|
3203
2895
|
name === 'framer_read_code_file'
|
|
3204
2896
|
? normalizedCodeFileReadResult(result)
|
|
3205
2897
|
: normalizedResult(result);
|
|
2898
|
+
// Result size is what this call adds to every later model turn.
|
|
2899
|
+
toolCall.resultBytes = Buffer.byteLength(JSON.stringify(normalized), 'utf8');
|
|
2900
|
+
if (modelImages.length > 0) toolCall.imageCount = modelImages.length;
|
|
3206
2901
|
if (name !== 'framer_read_project') return normalized;
|
|
3207
2902
|
const screenshotCount = screenshotQueries(rawArguments).length;
|
|
3208
2903
|
return {
|
|
@@ -3271,7 +2966,7 @@ export function createFramerAgentToolRuntime({
|
|
|
3271
2966
|
name,
|
|
3272
2967
|
phase: 'failed',
|
|
3273
2968
|
message:
|
|
3274
|
-
|
|
2969
|
+
mutation
|
|
3275
2970
|
? 'The Framer project update failed'
|
|
3276
2971
|
: 'The Framer project inspection failed',
|
|
3277
2972
|
durationMs: Date.now() - startedAt,
|
|
@@ -3353,12 +3048,14 @@ export function createFramerAgentToolRuntime({
|
|
|
3353
3048
|
inspect: inspectFinalState,
|
|
3354
3049
|
finalizeCompletion,
|
|
3355
3050
|
verificationRepairPrompt,
|
|
3051
|
+
unblockedFailurePrompt,
|
|
3356
3052
|
sourceHash: () => null,
|
|
3357
3053
|
hasPendingChanges: () => false,
|
|
3358
3054
|
directWorkspace: true,
|
|
3359
3055
|
summary: () => ({
|
|
3360
3056
|
projectId,
|
|
3361
3057
|
projectUrl,
|
|
3058
|
+
unresolvedTargets: unresolvedTargets.map((target) => ({ ...target })),
|
|
3362
3059
|
mutationCalls,
|
|
3363
3060
|
applyChangesCalls,
|
|
3364
3061
|
lowLevelWriteCalls,
|
|
@@ -3412,10 +3109,6 @@ export function createFramerAgentToolRuntime({
|
|
|
3412
3109
|
lowLevelWriteDisabled,
|
|
3413
3110
|
lowLevelWriteBlockedCalls,
|
|
3414
3111
|
instructionFallbacks,
|
|
3415
|
-
interactionVerificationAttemptLimit:
|
|
3416
|
-
MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR,
|
|
3417
|
-
interactionRepairLimit:
|
|
3418
|
-
MAX_INTERACTION_REPAIRS_PER_BEHAVIOR,
|
|
3419
3112
|
},
|
|
3420
3113
|
branch: branchChange,
|
|
3421
3114
|
lastInspectionAt,
|
|
@@ -3442,6 +3135,7 @@ export function createFramerAgentToolRuntime({
|
|
|
3442
3135
|
projectContextHash: guidanceMetadata.projectContextHash,
|
|
3443
3136
|
files: [...guidanceMetadata.files],
|
|
3444
3137
|
bytes: guidanceMetadata.bytes,
|
|
3138
|
+
guidesRead: [...guidesRead],
|
|
3445
3139
|
},
|
|
3446
3140
|
doctrine: { ...promptMetadata },
|
|
3447
3141
|
}),
|