@gakim-digital/dexter-bridge 0.11.13 → 0.11.15
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 +3 -2
- package/src/agent.js +31 -9
- package/src/cli.js +85 -17
- package/src/config.js +1 -3
- package/src/framerAgentTools.js +348 -776
- package/src/framerBehaviorContract.js +124 -1326
- package/src/outcomeWorkspace.js +24 -0
- package/src/protocol.js +6 -6
package/src/framerAgentTools.js
CHANGED
|
@@ -3,18 +3,16 @@ 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);
|
|
@@ -31,8 +29,6 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 5 * 60_000;
|
|
|
31
29
|
const SESSION_TIMEOUT_MS = 10 * 60_000;
|
|
32
30
|
const MAX_UNSAFE_WRITE_FAILURES = 2;
|
|
33
31
|
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
32
|
const MAX_GUIDANCE_BYTES = 180 * 1024;
|
|
37
33
|
const MAX_GUIDANCE_FILE_BYTES = 48 * 1024;
|
|
38
34
|
const PROJECT_ID_PATTERN = /^[A-Za-z0-9]{20}$/;
|
|
@@ -91,10 +87,15 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
91
87
|
{
|
|
92
88
|
name: 'framer_instructions',
|
|
93
89
|
description:
|
|
94
|
-
'
|
|
90
|
+
'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
91
|
inputSchema: {
|
|
96
92
|
type: 'object',
|
|
97
93
|
properties: {
|
|
94
|
+
guides: {
|
|
95
|
+
type: 'array',
|
|
96
|
+
maxItems: 6,
|
|
97
|
+
items: { type: 'string', minLength: 1, maxLength: 120 },
|
|
98
|
+
},
|
|
98
99
|
queries: {
|
|
99
100
|
type: 'array',
|
|
100
101
|
maxItems: 8,
|
|
@@ -140,7 +141,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
140
141
|
{
|
|
141
142
|
name: 'framer_plan_task',
|
|
142
143
|
description:
|
|
143
|
-
'After inspecting the live Framer target and before the first mutation, declare your
|
|
144
|
+
'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
145
|
inputSchema: {
|
|
145
146
|
type: 'object',
|
|
146
147
|
properties: {
|
|
@@ -167,147 +168,27 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
167
168
|
type: 'array',
|
|
168
169
|
maxItems: 20,
|
|
169
170
|
description:
|
|
170
|
-
'
|
|
171
|
+
'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
172
|
items: {
|
|
172
173
|
type: 'object',
|
|
173
174
|
properties: {
|
|
174
175
|
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: {
|
|
176
|
+
description: {
|
|
257
177
|
type: 'string',
|
|
258
|
-
minLength:
|
|
259
|
-
maxLength:
|
|
178
|
+
minLength: 5,
|
|
179
|
+
maxLength: 500,
|
|
180
|
+
description: 'What the visitor should experience.',
|
|
260
181
|
},
|
|
261
|
-
representativeFirst: { type: 'boolean', default: true },
|
|
262
182
|
targetNodeIds: {
|
|
263
183
|
type: 'array',
|
|
264
184
|
maxItems: 50,
|
|
265
185
|
uniqueItems: true,
|
|
266
|
-
items: {
|
|
267
|
-
type: 'string',
|
|
268
|
-
minLength: 1,
|
|
269
|
-
maxLength: 200,
|
|
270
|
-
},
|
|
186
|
+
items: { type: 'string', minLength: 1, maxLength: 200 },
|
|
271
187
|
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'],
|
|
188
|
+
'Every node that should carry this behavior once the work is done. Update the plan if they change, for example after replacing nodes.',
|
|
308
189
|
},
|
|
309
190
|
},
|
|
310
|
-
required: ['
|
|
191
|
+
required: ['id', 'description'],
|
|
311
192
|
additionalProperties: false,
|
|
312
193
|
},
|
|
313
194
|
},
|
|
@@ -319,7 +200,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
319
200
|
{
|
|
320
201
|
name: 'framer_verify_interactions',
|
|
321
202
|
description:
|
|
322
|
-
'
|
|
203
|
+
'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
204
|
inputSchema: {
|
|
324
205
|
type: 'object',
|
|
325
206
|
properties: {
|
|
@@ -327,103 +208,16 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
327
208
|
checks: {
|
|
328
209
|
type: 'array',
|
|
329
210
|
minItems: 1,
|
|
330
|
-
maxItems:
|
|
211
|
+
maxItems: 50,
|
|
331
212
|
items: {
|
|
332
213
|
type: 'object',
|
|
333
214
|
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
|
-
},
|
|
215
|
+
behaviorId: { type: 'string', minLength: 1, maxLength: 120 },
|
|
216
|
+
nodeId: { type: 'string', minLength: 1, maxLength: 200 },
|
|
217
|
+
property: { type: 'string', minLength: 1, maxLength: 120 },
|
|
218
|
+
contains: { type: 'string', minLength: 1, maxLength: 500 },
|
|
425
219
|
},
|
|
426
|
-
required: ['behaviorId'],
|
|
220
|
+
required: ['behaviorId', 'nodeId', 'property'],
|
|
427
221
|
additionalProperties: false,
|
|
428
222
|
},
|
|
429
223
|
},
|
|
@@ -619,7 +413,7 @@ function installedFramerAgentVersion() {
|
|
|
619
413
|
return null;
|
|
620
414
|
}
|
|
621
415
|
|
|
622
|
-
function readGuidanceFile(projectRoot, relativePath, remainingBytes) {
|
|
416
|
+
function readGuidanceFile(projectRoot, relativePath, remainingBytes, section) {
|
|
623
417
|
const normalized = relativePath.replaceAll('\\', '/');
|
|
624
418
|
if (
|
|
625
419
|
normalized.startsWith('/')
|
|
@@ -635,12 +429,71 @@ function readGuidanceFile(projectRoot, relativePath, remainingBytes) {
|
|
|
635
429
|
}
|
|
636
430
|
const maximum = Math.min(MAX_GUIDANCE_FILE_BYTES, remainingBytes);
|
|
637
431
|
if (maximum <= 0) return null;
|
|
432
|
+
let content = fs.readFileSync(absolutePath, 'utf8');
|
|
433
|
+
if (section) {
|
|
434
|
+
content = markdownSection(content, section);
|
|
435
|
+
if (!content) return null;
|
|
436
|
+
}
|
|
638
437
|
return {
|
|
639
438
|
path: normalized,
|
|
640
|
-
content: boundedText(
|
|
439
|
+
content: boundedText(content, maximum),
|
|
641
440
|
};
|
|
642
441
|
}
|
|
643
442
|
|
|
443
|
+
const CORE_GUIDE_SECTIONS = [
|
|
444
|
+
{ file: 'how-projects-work', section: 'Scope Types' },
|
|
445
|
+
{ file: 'how-projects-work', section: 'Replicas' },
|
|
446
|
+
{ file: 'how-projects-work', section: 'Transforms' },
|
|
447
|
+
{ file: 'implementation-strategy', section: 'Determining Strategy' },
|
|
448
|
+
{ file: 'implementation-strategy', section: 'Edit Strategy' },
|
|
449
|
+
{ file: 'implementation-strategy', section: 'Update Loop' },
|
|
450
|
+
];
|
|
451
|
+
const ON_DEMAND_GUIDE_FILES = [
|
|
452
|
+
{ file: 'how-projects-work', sections: true },
|
|
453
|
+
{ file: 'implementation-strategy', sections: true },
|
|
454
|
+
{ file: 'core-examples', sections: false },
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
// Lists the topic guides that are not part of every prompt, keyed by
|
|
458
|
+
// "file#Section" (or "file" for a whole file), from the files on disk.
|
|
459
|
+
function onDemandGuideCatalog(projectRoot) {
|
|
460
|
+
const catalog = new Map();
|
|
461
|
+
for (const { file, sections } of ON_DEMAND_GUIDE_FILES) {
|
|
462
|
+
const relativePath = `prompt/${file}.md`;
|
|
463
|
+
const absolutePath = path.join(projectRoot, relativePath);
|
|
464
|
+
if (!fs.existsSync(absolutePath)) continue;
|
|
465
|
+
if (!sections) {
|
|
466
|
+
catalog.set(file, { root: projectRoot, path: relativePath });
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const headings = fs.readFileSync(absolutePath, 'utf8')
|
|
470
|
+
.split('\n')
|
|
471
|
+
.filter((line) => line.startsWith('## '))
|
|
472
|
+
.map((line) => line.slice(3).trim())
|
|
473
|
+
.filter((heading) => !CORE_GUIDE_SECTIONS.some(
|
|
474
|
+
(core) => core.file === file && core.section === heading,
|
|
475
|
+
));
|
|
476
|
+
for (const heading of headings) {
|
|
477
|
+
catalog.set(`${file}#${heading}`, {
|
|
478
|
+
root: projectRoot,
|
|
479
|
+
path: relativePath,
|
|
480
|
+
section: heading,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return catalog;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function markdownSection(markdown, heading) {
|
|
488
|
+
const lines = markdown.split('\n');
|
|
489
|
+
const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
|
490
|
+
if (start < 0) return '';
|
|
491
|
+
const end = lines.findIndex(
|
|
492
|
+
(line, index) => index > start && line.startsWith('## '),
|
|
493
|
+
);
|
|
494
|
+
return lines.slice(start, end < 0 ? undefined : end).join('\n').trim();
|
|
495
|
+
}
|
|
496
|
+
|
|
644
497
|
function normalizeProjectId(value) {
|
|
645
498
|
const projectId = String(value || '').trim();
|
|
646
499
|
if (!PROJECT_ID_PATTERN.test(projectId)) {
|
|
@@ -764,7 +617,7 @@ function normalizeVerificationValues(values, allowed) {
|
|
|
764
617
|
.filter((value) => allowed.includes(value));
|
|
765
618
|
}
|
|
766
619
|
|
|
767
|
-
function normalizeTaskPlan(rawArguments
|
|
620
|
+
function normalizeTaskPlan(rawArguments) {
|
|
768
621
|
const summary = String(rawArguments.summary || '').trim().slice(0, 1000);
|
|
769
622
|
const domains = normalizeVerificationValues(
|
|
770
623
|
rawArguments.domains,
|
|
@@ -783,19 +636,7 @@ function normalizeTaskPlan(rawArguments, inheritedRejectedMechanisms = []) {
|
|
|
783
636
|
// The model's declared verification is authoritative; domains describe the
|
|
784
637
|
// work and never add checks on their own.
|
|
785
638
|
const verification = new Set(['structural', ...requestedVerification]);
|
|
786
|
-
const behaviorPlan = normalizeBehaviorPlan(
|
|
787
|
-
rawArguments,
|
|
788
|
-
inheritedRejectedMechanisms,
|
|
789
|
-
);
|
|
790
|
-
if (
|
|
791
|
-
domains.includes('interactions')
|
|
792
|
-
&& behaviorPlan.behaviors.length === 0
|
|
793
|
-
) {
|
|
794
|
-
throw toolError(
|
|
795
|
-
'FRAMER_BEHAVIOR_CONTRACT_REQUIRED',
|
|
796
|
-
'Interaction work requires observable behaviors and one compatible mechanism decision per behavior.',
|
|
797
|
-
);
|
|
798
|
-
}
|
|
639
|
+
const behaviorPlan = normalizeBehaviorPlan(rawArguments);
|
|
799
640
|
if (behaviorPlan.behaviors.length > 0) {
|
|
800
641
|
if (!domains.includes('interactions')) domains.push('interactions');
|
|
801
642
|
verification.add('interactions');
|
|
@@ -921,6 +762,28 @@ function authoritativeNodeIdsFromAssignment(assignment) {
|
|
|
921
762
|
]);
|
|
922
763
|
}
|
|
923
764
|
|
|
765
|
+
const LITERAL_TOKEN_TYPES = new Set([
|
|
766
|
+
'StringLiteral',
|
|
767
|
+
'NoSubstitutionTemplate',
|
|
768
|
+
'TemplateHead',
|
|
769
|
+
'TemplateMiddle',
|
|
770
|
+
'TemplateTail',
|
|
771
|
+
'SingleLineComment',
|
|
772
|
+
'MultiLineComment',
|
|
773
|
+
'RegularExpressionLiteral',
|
|
774
|
+
]);
|
|
775
|
+
|
|
776
|
+
// Blanks string, template, comment, and regex text so data such as image URLs
|
|
777
|
+
// or product copy is not mistaken for a forbidden global. Template
|
|
778
|
+
// substitutions stay visible because they are code.
|
|
779
|
+
function codeOutsideLiterals(code) {
|
|
780
|
+
let visible = '';
|
|
781
|
+
for (const token of jsTokens(code)) {
|
|
782
|
+
visible += LITERAL_TOKEN_TYPES.has(token.type) ? ' ' : token.value;
|
|
783
|
+
}
|
|
784
|
+
return visible;
|
|
785
|
+
}
|
|
786
|
+
|
|
924
787
|
function validateCode(value, { write = false, allowPublishing = false } = {}) {
|
|
925
788
|
const code = String(value || '').trim();
|
|
926
789
|
if (!code) {
|
|
@@ -929,7 +792,7 @@ function validateCode(value, { write = false, allowPublishing = false } = {}) {
|
|
|
929
792
|
'Framer Agent code is required.',
|
|
930
793
|
);
|
|
931
794
|
}
|
|
932
|
-
if (UNSAFE_EXEC_PATTERN.test(code)) {
|
|
795
|
+
if (UNSAFE_EXEC_PATTERN.test(codeOutsideLiterals(code))) {
|
|
933
796
|
throw toolError(
|
|
934
797
|
'FRAMER_AGENT_CODE_UNSAFE',
|
|
935
798
|
'Framer Agent code may use only the provided framer, state, and console globals.',
|
|
@@ -1231,32 +1094,9 @@ export function createFramerAgentToolRuntime({
|
|
|
1231
1094
|
);
|
|
1232
1095
|
}
|
|
1233
1096
|
const allowPublishing = assignment?.context?.permissions?.publish === true;
|
|
1234
|
-
const inheritedRejectedMechanisms =
|
|
1235
|
-
assignment?.context?.repairContext?.rejectedMechanisms
|
|
1236
|
-
|| assignment?.repairContext?.rejectedMechanisms
|
|
1237
|
-
|| [];
|
|
1238
|
-
const authorizedInstanceContainerIds = uniqueStrings(
|
|
1239
|
-
assignment?.context?.permissions?.scopedComponentArtifacts
|
|
1240
|
-
?.instanceContainerNodeIds
|
|
1241
|
-
|| assignment?.context?.contextGrant?.writableNodeIds
|
|
1242
|
-
|| [],
|
|
1243
|
-
);
|
|
1244
1097
|
const activePagePath = normalizePagePath(
|
|
1245
1098
|
assignment?.context?.activePagePath,
|
|
1246
1099
|
);
|
|
1247
|
-
const semanticVerifierMode =
|
|
1248
|
-
['off', 'shadow', 'enforce'].includes(
|
|
1249
|
-
String(
|
|
1250
|
-
assignment?.context?.harnessAuthority?.semanticVerifierMode
|
|
1251
|
-
|| env.DEXTER_FRAMER_SEMANTIC_VERIFIER_MODE
|
|
1252
|
-
|| '',
|
|
1253
|
-
).trim(),
|
|
1254
|
-
)
|
|
1255
|
-
? String(
|
|
1256
|
-
assignment?.context?.harnessAuthority?.semanticVerifierMode
|
|
1257
|
-
|| env.DEXTER_FRAMER_SEMANTIC_VERIFIER_MODE,
|
|
1258
|
-
).trim()
|
|
1259
|
-
: 'enforce';
|
|
1260
1100
|
const authoritativeNodeIds =
|
|
1261
1101
|
authoritativeNodeIdsFromAssignment(assignment);
|
|
1262
1102
|
let sessionId = null;
|
|
@@ -1288,20 +1128,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1288
1128
|
let lastMutationAt = null;
|
|
1289
1129
|
let screenshotCalls = 0;
|
|
1290
1130
|
let interactionVerificationCalls = 0;
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
behaviors: [],
|
|
1296
|
-
budgetExhaustedBehaviorIds: [],
|
|
1297
|
-
verifiedAt: null,
|
|
1298
|
-
};
|
|
1299
|
-
const representativeVerifiedBehaviorIds = new Set();
|
|
1300
|
-
const representativeMutatedBehaviorIds = new Set();
|
|
1301
|
-
const representativeRepairNodeIds = new Set();
|
|
1302
|
-
const representativeRepairBehaviorByNode = new Map();
|
|
1303
|
-
const representativeEvidenceBehaviorByNode = new Map();
|
|
1304
|
-
const behaviorVerificationAttempts = new Map();
|
|
1131
|
+
// Latest result per behavior/node/property, with the operation sequence it
|
|
1132
|
+
// was observed at, so checks from several calls (or pages) accumulate.
|
|
1133
|
+
const interactionCheckResults = new Map();
|
|
1134
|
+
let interactionVerifiedAt = null;
|
|
1305
1135
|
const mutationReceipts = [];
|
|
1306
1136
|
let lastScreenshotSequence = 0;
|
|
1307
1137
|
let lastScreenshotAttemptSequence = 0;
|
|
@@ -1311,6 +1141,8 @@ export function createFramerAgentToolRuntime({
|
|
|
1311
1141
|
const screenshotFailures = [];
|
|
1312
1142
|
let taskPlan = null;
|
|
1313
1143
|
const verificationEvidence = new Map();
|
|
1144
|
+
let guideCatalog = new Map();
|
|
1145
|
+
const guidesRead = [];
|
|
1314
1146
|
let guidanceMetadata = {
|
|
1315
1147
|
loaded: false,
|
|
1316
1148
|
contentHash: null,
|
|
@@ -1319,6 +1151,7 @@ export function createFramerAgentToolRuntime({
|
|
|
1319
1151
|
bytes: 0,
|
|
1320
1152
|
};
|
|
1321
1153
|
let promptMetadata = {
|
|
1154
|
+
promptBytes: 0,
|
|
1322
1155
|
doctrineLoaded: false,
|
|
1323
1156
|
doctrineVersion: null,
|
|
1324
1157
|
};
|
|
@@ -1328,6 +1161,7 @@ export function createFramerAgentToolRuntime({
|
|
|
1328
1161
|
const framerAgentVersion = installedFramerAgentVersion();
|
|
1329
1162
|
let operationSequence = 0;
|
|
1330
1163
|
let lastMutationSequence = 0;
|
|
1164
|
+
let lastCodeMutationSequence = 0;
|
|
1331
1165
|
let lastInspectionSequence = 0;
|
|
1332
1166
|
const abortController = new AbortController();
|
|
1333
1167
|
|
|
@@ -1634,6 +1468,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1634
1468
|
if (!fs.existsSync(projectRoot)) {
|
|
1635
1469
|
return { content: '', ...guidanceMetadata };
|
|
1636
1470
|
}
|
|
1471
|
+
// Every run gets the guidance an edit always needs: principles, command
|
|
1472
|
+
// syntax, tool APIs, scope and replica rules, and the edit loop. Topic
|
|
1473
|
+
// guides (CMS, components, creation strategy, examples, ...) are listed
|
|
1474
|
+
// in a catalog and read on demand through framer_instructions.
|
|
1637
1475
|
const requestedFiles = [
|
|
1638
1476
|
{ root: skillRoot, path: 'SKILL.md', label: 'SKILL.md' },
|
|
1639
1477
|
{ root: projectRoot, path: 'index.md', label: 'project/index.md' },
|
|
@@ -1642,20 +1480,19 @@ export function createFramerAgentToolRuntime({
|
|
|
1642
1480
|
path: 'project-inventory.md',
|
|
1643
1481
|
label: 'project/project-inventory.md',
|
|
1644
1482
|
},
|
|
1645
|
-
{ root: projectRoot, path: 'prompt/core-principles.md' },
|
|
1646
|
-
{ root: projectRoot, path: 'prompt/core-examples.md' },
|
|
1647
|
-
{ root: projectRoot, path: 'prompt/implementation-strategy.md' },
|
|
1648
|
-
{ root: projectRoot, path: 'prompt/updating-the-project.md' },
|
|
1649
|
-
{ root: projectRoot, path: 'prompt/tools.md' },
|
|
1650
1483
|
{ root: projectRoot, path: 'prompt/overview.md' },
|
|
1484
|
+
{ root: projectRoot, path: 'prompt/core-principles.md' },
|
|
1651
1485
|
{ root: projectRoot, path: 'prompt/guardrails.md' },
|
|
1652
1486
|
{ root: projectRoot, path: 'prompt/critical-reminders.md' },
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
:
|
|
1487
|
+
{ root: projectRoot, path: 'prompt/design-rules.md' },
|
|
1488
|
+
{ root: projectRoot, path: 'prompt/updating-the-project.md' },
|
|
1489
|
+
{ root: projectRoot, path: 'prompt/tools.md' },
|
|
1490
|
+
...CORE_GUIDE_SECTIONS.map(({ file, section }) => ({
|
|
1491
|
+
root: projectRoot,
|
|
1492
|
+
path: `prompt/${file}.md`,
|
|
1493
|
+
section,
|
|
1494
|
+
label: `prompt/${file}.md#${section}`,
|
|
1495
|
+
})),
|
|
1659
1496
|
];
|
|
1660
1497
|
const files = [];
|
|
1661
1498
|
let remainingBytes = MAX_GUIDANCE_BYTES;
|
|
@@ -1664,6 +1501,7 @@ export function createFramerAgentToolRuntime({
|
|
|
1664
1501
|
requestedFile.root,
|
|
1665
1502
|
requestedFile.path,
|
|
1666
1503
|
remainingBytes,
|
|
1504
|
+
requestedFile.section,
|
|
1667
1505
|
);
|
|
1668
1506
|
if (!loaded) continue;
|
|
1669
1507
|
files.push({
|
|
@@ -1673,9 +1511,19 @@ export function createFramerAgentToolRuntime({
|
|
|
1673
1511
|
remainingBytes -= Buffer.byteLength(loaded.content, 'utf8');
|
|
1674
1512
|
if (remainingBytes <= 0) break;
|
|
1675
1513
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1514
|
+
guideCatalog = onDemandGuideCatalog(projectRoot);
|
|
1515
|
+
const catalogText = guideCatalog.size > 0
|
|
1516
|
+
? [
|
|
1517
|
+
'## On-demand guides',
|
|
1518
|
+
'',
|
|
1519
|
+
'Read a guide with framer_instructions({ guides: [id] }) before work that depends on it.',
|
|
1520
|
+
...[...guideCatalog.keys()].map((id) => `- ${id}`),
|
|
1521
|
+
].join('\n')
|
|
1522
|
+
: '';
|
|
1523
|
+
const content = [
|
|
1524
|
+
...files.map((file) => `## ${file.path}\n\n${file.content}`),
|
|
1525
|
+
...(catalogText ? [catalogText] : []),
|
|
1526
|
+
].join('\n\n');
|
|
1679
1527
|
const inventory = files.find(
|
|
1680
1528
|
(file) => file.path.endsWith('project-inventory.md'),
|
|
1681
1529
|
)?.content || '';
|
|
@@ -1696,8 +1544,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1696
1544
|
const configurePromptContext = ({
|
|
1697
1545
|
doctrineLoaded = false,
|
|
1698
1546
|
doctrineVersion = null,
|
|
1547
|
+
promptBytes = 0,
|
|
1699
1548
|
} = {}) => {
|
|
1700
1549
|
promptMetadata = {
|
|
1550
|
+
promptBytes: Math.max(0, Number(promptBytes) || 0),
|
|
1701
1551
|
doctrineLoaded: doctrineLoaded === true,
|
|
1702
1552
|
doctrineVersion:
|
|
1703
1553
|
typeof doctrineVersion === 'string' && doctrineVersion.trim()
|
|
@@ -1838,9 +1688,14 @@ export function createFramerAgentToolRuntime({
|
|
|
1838
1688
|
return result;
|
|
1839
1689
|
};
|
|
1840
1690
|
|
|
1691
|
+
// Selected layers that do not resolve to native nodes (stale selections,
|
|
1692
|
+
// aliases, layers inside component instances) no longer stop the run. The
|
|
1693
|
+
// model is told which targets to locate by name, and the reason is kept for
|
|
1694
|
+
// debugging.
|
|
1695
|
+
let unresolvedTargets = [];
|
|
1841
1696
|
const validateAuthoritativeNodeIds = async () => {
|
|
1842
1697
|
await ensureSession();
|
|
1843
|
-
if (authoritativeNodeIds.length === 0) return;
|
|
1698
|
+
if (authoritativeNodeIds.length === 0) return { unresolvedTargets };
|
|
1844
1699
|
if (nodeValidationPromise) return nodeValidationPromise;
|
|
1845
1700
|
nodeValidationPromise = (async () => {
|
|
1846
1701
|
reportActivity({
|
|
@@ -1852,9 +1707,13 @@ export function createFramerAgentToolRuntime({
|
|
|
1852
1707
|
[
|
|
1853
1708
|
`const requestedIds = ${JSON.stringify(authoritativeNodeIds)};`,
|
|
1854
1709
|
'const resolved = await Promise.all(requestedIds.map(async (requestedId) => {',
|
|
1855
|
-
'
|
|
1856
|
-
|
|
1857
|
-
|
|
1710
|
+
' try {',
|
|
1711
|
+
' const node = await framer.agent.getNode({ id: requestedId },',
|
|
1712
|
+
` { pagePath: ${JSON.stringify(activePagePath)} });`,
|
|
1713
|
+
' return { requestedId, resolvedId: node?.id ?? null, error: node ? null : "not found on the active page" };',
|
|
1714
|
+
' } catch (error) {',
|
|
1715
|
+
' return { requestedId, resolvedId: null, error: String(error?.message || error).slice(0, 200) };',
|
|
1716
|
+
' }',
|
|
1858
1717
|
'}));',
|
|
1859
1718
|
'console.log(JSON.stringify({ resolved }));',
|
|
1860
1719
|
].join('\n'),
|
|
@@ -1864,26 +1723,30 @@ export function createFramerAgentToolRuntime({
|
|
|
1864
1723
|
const resolved = Array.isArray(parsed?.resolved)
|
|
1865
1724
|
? parsed.resolved
|
|
1866
1725
|
: [];
|
|
1867
|
-
|
|
1726
|
+
unresolvedTargets = authoritativeNodeIds.flatMap((requestedId) => {
|
|
1868
1727
|
const match = resolved.find(
|
|
1869
1728
|
(entry) =>
|
|
1870
1729
|
isRecord(entry)
|
|
1871
1730
|
&& entry.requestedId === requestedId,
|
|
1872
1731
|
);
|
|
1873
|
-
|
|
1732
|
+
if (match?.resolvedId === requestedId) return [];
|
|
1733
|
+
return [{
|
|
1734
|
+
id: requestedId,
|
|
1735
|
+
reason: !match
|
|
1736
|
+
? 'not checked'
|
|
1737
|
+
: match.resolvedId
|
|
1738
|
+
? `resolves to ${match.resolvedId}`
|
|
1739
|
+
: String(match.error || 'not found on the active page'),
|
|
1740
|
+
}];
|
|
1874
1741
|
});
|
|
1875
|
-
if (invalid.length > 0) {
|
|
1876
|
-
throw toolError(
|
|
1877
|
-
'FRAMER_AGENT_NODE_ID_INVALID',
|
|
1878
|
-
`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.`,
|
|
1879
|
-
409,
|
|
1880
|
-
);
|
|
1881
|
-
}
|
|
1882
1742
|
reportActivity({
|
|
1883
1743
|
kind: 'status_update',
|
|
1884
1744
|
source: 'framer-agent',
|
|
1885
|
-
message:
|
|
1745
|
+
message: unresolvedTargets.length > 0
|
|
1746
|
+
? 'Some selected layers could not be matched exactly, so Dexter will locate them by name.'
|
|
1747
|
+
: 'The selected Framer layers are ready.',
|
|
1886
1748
|
});
|
|
1749
|
+
return { unresolvedTargets };
|
|
1887
1750
|
})().catch((error) => {
|
|
1888
1751
|
nodeValidationPromise = null;
|
|
1889
1752
|
throw error;
|
|
@@ -1934,9 +1797,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1934
1797
|
}
|
|
1935
1798
|
};
|
|
1936
1799
|
|
|
1937
|
-
const markMutation = () => {
|
|
1800
|
+
const markMutation = ({ code = false } = {}) => {
|
|
1938
1801
|
operationSequence += 1;
|
|
1939
1802
|
lastMutationSequence = operationSequence;
|
|
1803
|
+
if (code) lastCodeMutationSequence = operationSequence;
|
|
1940
1804
|
lastMutationAt = new Date().toISOString();
|
|
1941
1805
|
};
|
|
1942
1806
|
|
|
@@ -1952,30 +1816,35 @@ export function createFramerAgentToolRuntime({
|
|
|
1952
1816
|
? {
|
|
1953
1817
|
behaviors: taskPlan.behaviors.map((behavior) => ({
|
|
1954
1818
|
...behavior,
|
|
1955
|
-
|
|
1956
|
-
expectedDestinations: [...behavior.expectedDestinations],
|
|
1957
|
-
})),
|
|
1958
|
-
mechanisms: taskPlan.mechanisms.map((decision) => ({
|
|
1959
|
-
...decision,
|
|
1960
|
-
targetNodeIds: [...decision.targetNodeIds],
|
|
1961
|
-
})),
|
|
1962
|
-
rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
|
|
1963
|
-
...entry,
|
|
1964
|
-
})),
|
|
1965
|
-
compatibility: taskPlan.compatibility.map((entry) => ({
|
|
1966
|
-
...entry,
|
|
1967
|
-
requiredCapabilities: [...entry.requiredCapabilities],
|
|
1819
|
+
targetNodeIds: [...behavior.targetNodeIds],
|
|
1968
1820
|
})),
|
|
1969
1821
|
}
|
|
1970
1822
|
: {}),
|
|
1971
1823
|
};
|
|
1972
1824
|
};
|
|
1825
|
+
// Only checks observed after the latest mutation describe the current project.
|
|
1826
|
+
const freshInteractionChecks = () =>
|
|
1827
|
+
[...interactionCheckResults.values()]
|
|
1828
|
+
.filter((entry) => entry.sequence > lastMutationSequence)
|
|
1829
|
+
.map((entry) => entry.result);
|
|
1830
|
+
const interactionSummary = () =>
|
|
1831
|
+
summarizeBehaviorVerification(taskPlan, freshInteractionChecks());
|
|
1832
|
+
const interactionStatus = () => {
|
|
1833
|
+
if (!taskPlan?.behaviors?.length || mutationCalls === 0) return 'not_required';
|
|
1834
|
+
return interactionSummary().status;
|
|
1835
|
+
};
|
|
1973
1836
|
const synchronized = () =>
|
|
1974
1837
|
mutationCalls === 0 || lastInspectionSequence > lastMutationSequence;
|
|
1975
1838
|
const verificationCheckCompleted = (check) => {
|
|
1976
1839
|
if (mutationCalls === 0) return true;
|
|
1977
1840
|
if (check === 'structural') return synchronized();
|
|
1978
|
-
|
|
1841
|
+
if (check === 'interactions') {
|
|
1842
|
+
return ['verified', 'not_required'].includes(interactionStatus());
|
|
1843
|
+
}
|
|
1844
|
+
// Canvas edits cannot change a code file, so only code edits stale it.
|
|
1845
|
+
const staleAfter =
|
|
1846
|
+
check === 'code' ? lastCodeMutationSequence : lastMutationSequence;
|
|
1847
|
+
return Number(verificationEvidence.get(check) || 0) > staleAfter;
|
|
1979
1848
|
};
|
|
1980
1849
|
const visualVerificationRequired = () =>
|
|
1981
1850
|
requiredVerificationChecks().includes('visual');
|
|
@@ -1993,12 +1862,19 @@ export function createFramerAgentToolRuntime({
|
|
|
1993
1862
|
const requiredChecks = () => [
|
|
1994
1863
|
...new Set(['structural', ...requiredVerificationChecks()]),
|
|
1995
1864
|
];
|
|
1865
|
+
// Checks the model attempted but that live evidence could not confirm. They
|
|
1866
|
+
// are reported as unverified and do not fail the run.
|
|
1867
|
+
const unavailableChecks = () => [
|
|
1868
|
+
...(visualVerificationStatus() === 'unavailable' ? ['visual'] : []),
|
|
1869
|
+
...(interactionStatus() === 'unverified' ? ['interactions'] : []),
|
|
1870
|
+
];
|
|
1996
1871
|
const missingVerificationChecks = () => {
|
|
1997
1872
|
if (mutationCalls === 0) return [];
|
|
1998
1873
|
const missing = [];
|
|
1999
1874
|
if (!taskPlan) missing.push('task-plan');
|
|
1875
|
+
const unavailable = unavailableChecks();
|
|
2000
1876
|
for (const check of requiredChecks()) {
|
|
2001
|
-
if (check
|
|
1877
|
+
if (unavailable.includes(check)) continue;
|
|
2002
1878
|
if (!verificationCheckCompleted(check)) missing.push(check);
|
|
2003
1879
|
}
|
|
2004
1880
|
return missing;
|
|
@@ -2026,48 +1902,17 @@ export function createFramerAgentToolRuntime({
|
|
|
2026
1902
|
? {
|
|
2027
1903
|
interactionVerificationCalls,
|
|
2028
1904
|
interactionVerification: {
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
: 'unknown',
|
|
2034
|
-
checks: interactionVerification.checks.map((check) => ({
|
|
2035
|
-
...check,
|
|
2036
|
-
contradictions: [...check.contradictions],
|
|
2037
|
-
unknowns: [...check.unknowns],
|
|
2038
|
-
observations: check.observations.map((observation) => ({
|
|
2039
|
-
...observation,
|
|
2040
|
-
})),
|
|
2041
|
-
nextSafeActions: [...check.nextSafeActions],
|
|
2042
|
-
...(check.semanticAssessment
|
|
2043
|
-
? {
|
|
2044
|
-
semanticAssessment: {
|
|
2045
|
-
...check.semanticAssessment,
|
|
2046
|
-
evidenceNodeIds: [
|
|
2047
|
-
...check.semanticAssessment.evidenceNodeIds,
|
|
2048
|
-
],
|
|
2049
|
-
},
|
|
2050
|
-
}
|
|
2051
|
-
: {}),
|
|
2052
|
-
})),
|
|
2053
|
-
behaviors: interactionVerification.behaviors.map((behavior) => ({
|
|
2054
|
-
...behavior,
|
|
2055
|
-
contradictions: [...behavior.contradictions],
|
|
2056
|
-
unknowns: [...behavior.unknowns],
|
|
2057
|
-
})),
|
|
2058
|
-
semanticVerifierMode,
|
|
2059
|
-
budgetExhaustedBehaviorIds: [
|
|
2060
|
-
...interactionVerification.budgetExhaustedBehaviorIds,
|
|
2061
|
-
],
|
|
2062
|
-
verifiedAt: interactionVerification.verifiedAt,
|
|
1905
|
+
status: interactionStatus(),
|
|
1906
|
+
behaviors: taskPlan?.behaviors?.length ? interactionSummary().behaviors : [],
|
|
1907
|
+
checks: freshInteractionChecks().map((check) => ({ ...check })),
|
|
1908
|
+
verifiedAt: interactionVerifiedAt,
|
|
2063
1909
|
},
|
|
2064
1910
|
}
|
|
2065
1911
|
: {}),
|
|
2066
1912
|
taskPlan: taskPlanSnapshot(),
|
|
2067
1913
|
requiredChecks: requiredChecks(),
|
|
2068
1914
|
completedChecks: requiredChecks().filter(verificationCheckCompleted),
|
|
2069
|
-
unavailableChecks:
|
|
2070
|
-
visualVerificationStatus() === 'unavailable' ? ['visual'] : [],
|
|
1915
|
+
unavailableChecks: unavailableChecks(),
|
|
2071
1916
|
missingChecks: missingVerificationChecks(),
|
|
2072
1917
|
lastMutationAt,
|
|
2073
1918
|
lastInspectionAt,
|
|
@@ -2089,49 +1934,58 @@ export function createFramerAgentToolRuntime({
|
|
|
2089
1934
|
};
|
|
2090
1935
|
};
|
|
2091
1936
|
|
|
2092
|
-
const
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
1937
|
+
const withUnavailableChecks = (completion) => {
|
|
1938
|
+
const unavailable = unavailableChecks();
|
|
1939
|
+
if (unavailable.length === 0) return completion;
|
|
1940
|
+
const unverifiedBehaviors = interactionSummary().behaviors
|
|
1941
|
+
.filter((behavior) => behavior.verdict === 'unknown')
|
|
1942
|
+
.map((behavior) => `${behavior.behaviorId} (${behavior.unknowns[0] || 'not confirmed'})`);
|
|
1943
|
+
return {
|
|
1944
|
+
...completion,
|
|
1945
|
+
checks: [
|
|
1946
|
+
...(Array.isArray(completion?.checks) ? completion.checks : []),
|
|
1947
|
+
...(unavailable.includes('visual')
|
|
1948
|
+
? [{
|
|
2099
1949
|
command: 'Visual verification',
|
|
2100
1950
|
status: 'skipped',
|
|
2101
1951
|
output:
|
|
2102
1952
|
'Screenshots of the final canvas could not be delivered to the model, so the visual result is unverified.',
|
|
2103
|
-
}
|
|
2104
|
-
],
|
|
2105
|
-
|
|
2106
|
-
|
|
1953
|
+
}]
|
|
1954
|
+
: []),
|
|
1955
|
+
...(unavailable.includes('interactions')
|
|
1956
|
+
? [{
|
|
1957
|
+
command: 'Interaction verification',
|
|
1958
|
+
status: 'skipped',
|
|
1959
|
+
output:
|
|
1960
|
+
`The live project did not expose evidence for: ${unverifiedBehaviors.join('; ')}. These behaviors are unverified.`,
|
|
1961
|
+
}]
|
|
1962
|
+
: []),
|
|
1963
|
+
],
|
|
1964
|
+
};
|
|
1965
|
+
};
|
|
2107
1966
|
|
|
2108
1967
|
const finalizeCompletion = (completion) => {
|
|
2109
1968
|
if (completion?.status !== 'completed' || mutationCalls === 0) {
|
|
2110
1969
|
return completion;
|
|
2111
1970
|
}
|
|
2112
|
-
if (verification().completed) return
|
|
1971
|
+
if (verification().completed) return withUnavailableChecks(completion);
|
|
2113
1972
|
const missing = missingVerificationChecks().map((check) => {
|
|
2114
1973
|
if (check === 'task-plan') return 'a harness task and verification plan';
|
|
2115
1974
|
if (check === 'structural') return 'a final structural read';
|
|
2116
1975
|
if (check === 'visual') return 'post-mutation screenshot verification';
|
|
2117
1976
|
return `post-mutation ${check} verification`;
|
|
2118
1977
|
});
|
|
2119
|
-
const interactionDetails =
|
|
2120
|
-
.filter((behavior) => behavior.verdict
|
|
2121
|
-
.map((behavior) =>
|
|
2122
|
-
|
|
2123
|
-
behavior.contradictions[0]
|
|
2124
|
-
|| behavior.unknowns[0]
|
|
2125
|
-
|| 'no conclusive live evidence';
|
|
2126
|
-
return `${behavior.behaviorId}=${behavior.verdict} (${detail})`;
|
|
2127
|
-
});
|
|
1978
|
+
const interactionDetails = (taskPlan?.behaviors?.length ? interactionSummary().behaviors : [])
|
|
1979
|
+
.filter((behavior) => behavior.verdict === 'contradicted' || !behavior.checked)
|
|
1980
|
+
.map((behavior) =>
|
|
1981
|
+
`${behavior.behaviorId}=${behavior.verdict === 'contradicted' ? behavior.contradictions[0] : 'not checked after the latest change'}`);
|
|
2128
1982
|
const reason = [
|
|
2129
1983
|
`The harness ended before completing ${missing.join(' and ')}.`,
|
|
2130
1984
|
...(interactionDetails.length > 0
|
|
2131
1985
|
? [`Interaction evidence: ${interactionDetails.join('; ')}.`]
|
|
2132
1986
|
: []),
|
|
2133
1987
|
].join(' ');
|
|
2134
|
-
const failed =
|
|
1988
|
+
const failed = withUnavailableChecks(completion);
|
|
2135
1989
|
return {
|
|
2136
1990
|
...failed,
|
|
2137
1991
|
status: 'failed',
|
|
@@ -2150,6 +2004,22 @@ export function createFramerAgentToolRuntime({
|
|
|
2150
2004
|
};
|
|
2151
2005
|
};
|
|
2152
2006
|
|
|
2007
|
+
// A model that reports failure without changing anything and without a
|
|
2008
|
+
// refused plan or edit gave up on its own. It gets one prompt to continue.
|
|
2009
|
+
const unblockedFailurePrompt = (completion) => {
|
|
2010
|
+
if (completion?.status !== 'failed' || mutationCalls > 0) return null;
|
|
2011
|
+
const lastGate = [...toolCalls]
|
|
2012
|
+
.reverse()
|
|
2013
|
+
.find((call) => call.category === 'plan' || call.category === 'mutation');
|
|
2014
|
+
if (lastGate?.status === 'failed') return null;
|
|
2015
|
+
return [
|
|
2016
|
+
'You returned failed without changing the Framer project, and no tool refused the work.',
|
|
2017
|
+
'Nothing in this run is blocking you and there is no time limit.',
|
|
2018
|
+
'Continue the same task from the current project state: plan it, apply the change, verify it, then return the required JSON outcome.',
|
|
2019
|
+
'Return failed or blocked only for a concrete blocker you can name.',
|
|
2020
|
+
].join(' ');
|
|
2021
|
+
};
|
|
2022
|
+
|
|
2153
2023
|
const verificationRepairPrompt = () => {
|
|
2154
2024
|
const current = verification();
|
|
2155
2025
|
if (current.completed || mutationCalls === 0) return null;
|
|
@@ -2165,27 +2035,33 @@ export function createFramerAgentToolRuntime({
|
|
|
2165
2035
|
missing.push(
|
|
2166
2036
|
'capture and inspect a screenshot of the affected canvas after the latest mutation',
|
|
2167
2037
|
);
|
|
2038
|
+
} else if (check === 'interactions') {
|
|
2039
|
+
const summary = interactionSummary();
|
|
2040
|
+
const unchecked = summary.behaviors
|
|
2041
|
+
.filter((behavior) => !behavior.checked)
|
|
2042
|
+
.map((behavior) => behavior.uncheckedTargets.length
|
|
2043
|
+
? `${behavior.behaviorId} on ${behavior.uncheckedTargets.join(', ')}`
|
|
2044
|
+
: behavior.behaviorId);
|
|
2045
|
+
const contradicted = summary.behaviors
|
|
2046
|
+
.filter((behavior) => behavior.verdict === 'contradicted')
|
|
2047
|
+
.map((behavior) => behavior.contradictions[0]);
|
|
2048
|
+
missing.push([
|
|
2049
|
+
unchecked.length
|
|
2050
|
+
? `call framer_verify_interactions for ${unchecked.join('; ')}, naming the Framer property that carries each behavior`
|
|
2051
|
+
: '',
|
|
2052
|
+
contradicted.length
|
|
2053
|
+
? `fix or explain these mismatches: ${contradicted.join('; ')}`
|
|
2054
|
+
: '',
|
|
2055
|
+
].filter(Boolean).join(' and '));
|
|
2168
2056
|
} else {
|
|
2169
|
-
|
|
2170
|
-
.map((behavior) =>
|
|
2171
|
-
`${behavior.behaviorId}:${behavior.verdict}`)
|
|
2172
|
-
.join(', ');
|
|
2173
|
-
const exhausted =
|
|
2174
|
-
interactionVerification.budgetExhaustedBehaviorIds.length > 0;
|
|
2175
|
-
missing.push(
|
|
2176
|
-
check === 'interactions'
|
|
2177
|
-
? exhausted
|
|
2178
|
-
? `report interaction verification as incomplete without further mutation or repeated verification (${interactionState || 'unknown'})`
|
|
2179
|
-
: `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'})`
|
|
2180
|
-
: `perform a focused read tagged with verifies:["${check}"] after the latest mutation`,
|
|
2181
|
-
);
|
|
2057
|
+
missing.push(`perform a focused read tagged with verifies:["${check}"] after the latest mutation`);
|
|
2182
2058
|
}
|
|
2183
2059
|
}
|
|
2184
2060
|
return [
|
|
2185
2061
|
'Continue the same Framer task from its current project state.',
|
|
2186
2062
|
'Do not redo successful work.',
|
|
2187
2063
|
`Before returning, ${missing.join(' and ')}.`,
|
|
2188
|
-
'
|
|
2064
|
+
'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.',
|
|
2189
2065
|
].join(' ');
|
|
2190
2066
|
};
|
|
2191
2067
|
|
|
@@ -2230,109 +2106,13 @@ export function createFramerAgentToolRuntime({
|
|
|
2230
2106
|
.update(`${name}\n${JSON.stringify(rawArguments)}`)
|
|
2231
2107
|
.digest('hex');
|
|
2232
2108
|
|
|
2233
|
-
const
|
|
2234
|
-
const commands = summarizeFramerChanges(changes).commands;
|
|
2235
|
-
if (
|
|
2236
|
-
commands.length === 0
|
|
2237
|
-
|| commands.some((command) =>
|
|
2238
|
-
command.operation !== 'set'
|
|
2239
|
-
|| !command.nodeId
|
|
2240
|
-
|| !representativeRepairNodeIds.has(command.nodeId))
|
|
2241
|
-
) {
|
|
2242
|
-
return [];
|
|
2243
|
-
}
|
|
2244
|
-
return uniqueStrings(commands.map((command) => command.nodeId));
|
|
2245
|
-
};
|
|
2246
|
-
|
|
2247
|
-
const requireTaskPlanForMutation = ({ changes = '' } = {}) => {
|
|
2109
|
+
const requireTaskPlanForMutation = () => {
|
|
2248
2110
|
if (!taskPlan) {
|
|
2249
2111
|
throw toolError(
|
|
2250
2112
|
'FRAMER_HARNESS_PLAN_REQUIRED',
|
|
2251
2113
|
'Inspect the live Framer target, then call framer_plan_task before making a mutation.',
|
|
2252
2114
|
);
|
|
2253
2115
|
}
|
|
2254
|
-
const repairTargets = representativeRepairTargets(changes);
|
|
2255
|
-
const isRepresentativeRepair = repairTargets.length > 0;
|
|
2256
|
-
const rejectedSelectedMechanisms = taskPlan.behaviors.filter((behavior) => {
|
|
2257
|
-
const selected = taskPlan.mechanisms.find((decision) =>
|
|
2258
|
-
decision.behaviorId === behavior.id);
|
|
2259
|
-
return selected && taskPlan.rejectedMechanisms.some((rejected) =>
|
|
2260
|
-
rejected.mechanism === selected.mechanism
|
|
2261
|
-
&& (
|
|
2262
|
-
rejected.behaviorId === behavior.id
|
|
2263
|
-
|| rejected.behaviorSignature === behavior.signature
|
|
2264
|
-
));
|
|
2265
|
-
});
|
|
2266
|
-
if (rejectedSelectedMechanisms.length > 0) {
|
|
2267
|
-
throw toolError(
|
|
2268
|
-
'FRAMER_BEHAVIOR_MECHANISM_REPLAN_REQUIRED',
|
|
2269
|
-
`Choose a different mechanism for ${rejectedSelectedMechanisms.map((behavior) => behavior.id).join(', ')} before making another mutation.`,
|
|
2270
|
-
);
|
|
2271
|
-
}
|
|
2272
|
-
const changedNodeIds = new Set(
|
|
2273
|
-
summarizeFramerChanges(changes).commands
|
|
2274
|
-
.flatMap((command) => [
|
|
2275
|
-
command.nodeId,
|
|
2276
|
-
command.sourceNodeId,
|
|
2277
|
-
])
|
|
2278
|
-
.filter(Boolean),
|
|
2279
|
-
);
|
|
2280
|
-
const rawChanges = String(changes || '');
|
|
2281
|
-
const pendingRepresentative = taskPlan.behaviors
|
|
2282
|
-
.filter((behavior) => behavior.expectedTargets > 1)
|
|
2283
|
-
.map((behavior) => ({
|
|
2284
|
-
behavior,
|
|
2285
|
-
decision: taskPlan.mechanisms.find((decision) =>
|
|
2286
|
-
decision.behaviorId === behavior.id),
|
|
2287
|
-
}))
|
|
2288
|
-
.filter(({ decision }) => decision?.representativeFirst)
|
|
2289
|
-
.filter(({ behavior }) =>
|
|
2290
|
-
!representativeVerifiedBehaviorIds.has(behavior.id));
|
|
2291
|
-
for (const { behavior, decision } of pendingRepresentative) {
|
|
2292
|
-
const touchedTargets = decision.targetNodeIds.filter((nodeId) =>
|
|
2293
|
-
rawChanges.includes(nodeId));
|
|
2294
|
-
const touchedEvidence = [...changedNodeIds].filter((nodeId) =>
|
|
2295
|
-
representativeEvidenceBehaviorByNode.get(nodeId) === behavior.id);
|
|
2296
|
-
if (
|
|
2297
|
-
touchedTargets.length > 1
|
|
2298
|
-
|| (
|
|
2299
|
-
touchedTargets.length === 1
|
|
2300
|
-
&& touchedTargets[0] !== decision.representativeTargetNodeId
|
|
2301
|
-
)
|
|
2302
|
-
) {
|
|
2303
|
-
throw toolError(
|
|
2304
|
-
'FRAMER_BEHAVIOR_REPRESENTATIVE_MUTATION_REQUIRED',
|
|
2305
|
-
`Implement only representative target ${decision.representativeTargetNodeId} for ${behavior.id} before changing the remaining targets.`,
|
|
2306
|
-
);
|
|
2307
|
-
}
|
|
2308
|
-
if (touchedTargets.length === 0 && touchedEvidence.length === 0) {
|
|
2309
|
-
continue;
|
|
2310
|
-
}
|
|
2311
|
-
if (!representativeMutatedBehaviorIds.has(behavior.id)) continue;
|
|
2312
|
-
const attempt = behaviorVerificationAttempts.get(behavior.id);
|
|
2313
|
-
const repairForBehavior =
|
|
2314
|
-
isRepresentativeRepair
|
|
2315
|
-
&& repairTargets.every((nodeId) =>
|
|
2316
|
-
representativeRepairBehaviorByNode.get(nodeId) === behavior.id);
|
|
2317
|
-
if (
|
|
2318
|
-
!repairForBehavior
|
|
2319
|
-
|| Number(attempt?.repairs || 0)
|
|
2320
|
-
>= MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
|
|
2321
|
-
) {
|
|
2322
|
-
throw toolError(
|
|
2323
|
-
'FRAMER_BEHAVIOR_REPRESENTATIVE_VERIFICATION_REQUIRED',
|
|
2324
|
-
`Verify representative target ${decision.representativeTargetNodeId} for ${behavior.id} before changing it again or batching the remaining targets.`,
|
|
2325
|
-
);
|
|
2326
|
-
}
|
|
2327
|
-
behaviorVerificationAttempts.set(behavior.id, {
|
|
2328
|
-
...attempt,
|
|
2329
|
-
repairs: Number(attempt?.repairs || 0) + 1,
|
|
2330
|
-
});
|
|
2331
|
-
}
|
|
2332
|
-
return pendingRepresentative
|
|
2333
|
-
.filter(({ decision }) =>
|
|
2334
|
-
rawChanges.includes(decision.representativeTargetNodeId))
|
|
2335
|
-
.map(({ behavior }) => behavior.id);
|
|
2336
2116
|
};
|
|
2337
2117
|
|
|
2338
2118
|
async function invokeUnsafe(name, rawArguments = {}) {
|
|
@@ -2424,7 +2204,40 @@ export function createFramerAgentToolRuntime({
|
|
|
2424
2204
|
const queries = Array.isArray(rawArguments.queries)
|
|
2425
2205
|
? uniqueStrings(rawArguments.queries, 8)
|
|
2426
2206
|
: [];
|
|
2427
|
-
|
|
2207
|
+
const guideIds = Array.isArray(rawArguments.guides)
|
|
2208
|
+
? uniqueStrings(rawArguments.guides, 6)
|
|
2209
|
+
: [];
|
|
2210
|
+
if (guideIds.length > 0) {
|
|
2211
|
+
const guides = guideIds.flatMap((id) => {
|
|
2212
|
+
const entry = guideCatalog.get(id);
|
|
2213
|
+
const loaded = entry
|
|
2214
|
+
? readGuidanceFile(
|
|
2215
|
+
entry.root,
|
|
2216
|
+
entry.path,
|
|
2217
|
+
MAX_GUIDANCE_FILE_BYTES,
|
|
2218
|
+
entry.section,
|
|
2219
|
+
)
|
|
2220
|
+
: null;
|
|
2221
|
+
if (!loaded) return [];
|
|
2222
|
+
if (!guidesRead.includes(id)) guidesRead.push(id);
|
|
2223
|
+
return [{ id, content: loaded.content }];
|
|
2224
|
+
});
|
|
2225
|
+
const unknown = guideIds.filter(
|
|
2226
|
+
(id) => !guides.some((guide) => guide.id === id),
|
|
2227
|
+
);
|
|
2228
|
+
result = {
|
|
2229
|
+
stdout: JSON.stringify({
|
|
2230
|
+
guides,
|
|
2231
|
+
...(unknown.length > 0
|
|
2232
|
+
? {
|
|
2233
|
+
unknownGuides: unknown,
|
|
2234
|
+
availableGuides: [...guideCatalog.keys()],
|
|
2235
|
+
}
|
|
2236
|
+
: {}),
|
|
2237
|
+
}),
|
|
2238
|
+
stderr: '',
|
|
2239
|
+
};
|
|
2240
|
+
} else if (queries.length > 0 && !instructionLookupUnavailable) {
|
|
2428
2241
|
try {
|
|
2429
2242
|
result = await executeCli(
|
|
2430
2243
|
['docs', ...queries],
|
|
@@ -2467,22 +2280,7 @@ export function createFramerAgentToolRuntime({
|
|
|
2467
2280
|
'Inspect the live Framer target before declaring the task and verification plan.',
|
|
2468
2281
|
);
|
|
2469
2282
|
}
|
|
2470
|
-
const nextPlan = normalizeTaskPlan(
|
|
2471
|
-
rawArguments,
|
|
2472
|
-
inheritedRejectedMechanisms,
|
|
2473
|
-
);
|
|
2474
|
-
const previousMechanisms = new Map(
|
|
2475
|
-
(taskPlan?.mechanisms || []).map((decision) => [
|
|
2476
|
-
decision.behaviorId,
|
|
2477
|
-
JSON.stringify({
|
|
2478
|
-
mechanism: decision.mechanism,
|
|
2479
|
-
targetNodeIds: decision.targetNodeIds,
|
|
2480
|
-
representativeTargetNodeId:
|
|
2481
|
-
decision.representativeTargetNodeId,
|
|
2482
|
-
}),
|
|
2483
|
-
]),
|
|
2484
|
-
);
|
|
2485
|
-
const behaviorPlan = mergeBehaviorPlans(taskPlan, nextPlan);
|
|
2283
|
+
const nextPlan = normalizeTaskPlan(rawArguments);
|
|
2486
2284
|
taskPlan = {
|
|
2487
2285
|
summary: nextPlan.summary,
|
|
2488
2286
|
domains: uniqueStrings([
|
|
@@ -2493,280 +2291,55 @@ export function createFramerAgentToolRuntime({
|
|
|
2493
2291
|
...(taskPlan?.verification || []),
|
|
2494
2292
|
...nextPlan.verification,
|
|
2495
2293
|
]),
|
|
2496
|
-
...
|
|
2294
|
+
...mergeBehaviorPlans(taskPlan, nextPlan),
|
|
2497
2295
|
};
|
|
2498
|
-
for (const decision of taskPlan.mechanisms) {
|
|
2499
|
-
const prior = previousMechanisms.get(decision.behaviorId);
|
|
2500
|
-
const current = JSON.stringify({
|
|
2501
|
-
mechanism: decision.mechanism,
|
|
2502
|
-
targetNodeIds: decision.targetNodeIds,
|
|
2503
|
-
representativeTargetNodeId:
|
|
2504
|
-
decision.representativeTargetNodeId,
|
|
2505
|
-
});
|
|
2506
|
-
if (prior && prior !== current) {
|
|
2507
|
-
representativeVerifiedBehaviorIds.delete(decision.behaviorId);
|
|
2508
|
-
representativeMutatedBehaviorIds.delete(decision.behaviorId);
|
|
2509
|
-
behaviorVerificationAttempts.delete(decision.behaviorId);
|
|
2510
|
-
}
|
|
2511
|
-
}
|
|
2512
2296
|
result = {
|
|
2513
|
-
stdout: JSON.stringify({
|
|
2514
|
-
plan: {
|
|
2515
|
-
summary: taskPlan.summary,
|
|
2516
|
-
domains: [...taskPlan.domains],
|
|
2517
|
-
verification: [...taskPlan.verification],
|
|
2518
|
-
behaviors: taskPlan.behaviors.map((behavior) => ({
|
|
2519
|
-
...behavior,
|
|
2520
|
-
expectedContent: [...behavior.expectedContent],
|
|
2521
|
-
})),
|
|
2522
|
-
mechanisms: taskPlan.mechanisms.map((decision) => ({
|
|
2523
|
-
...decision,
|
|
2524
|
-
})),
|
|
2525
|
-
compatibility: taskPlan.compatibility.map((entry) => ({
|
|
2526
|
-
...entry,
|
|
2527
|
-
requiredCapabilities: [...entry.requiredCapabilities],
|
|
2528
|
-
})),
|
|
2529
|
-
rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
|
|
2530
|
-
...entry,
|
|
2531
|
-
})),
|
|
2532
|
-
},
|
|
2533
|
-
instruction:
|
|
2534
|
-
'Implement one representative first when required, verify it, then complete coverage and gather every derived verification result after the final mutation.',
|
|
2535
|
-
}),
|
|
2297
|
+
stdout: JSON.stringify({ plan: taskPlanSnapshot() }),
|
|
2536
2298
|
stderr: '',
|
|
2537
2299
|
};
|
|
2538
2300
|
} else if (name === 'framer_verify_interactions') {
|
|
2539
|
-
if (mutationCalls === 0) {
|
|
2540
|
-
throw toolError(
|
|
2541
|
-
'FRAMER_HARNESS_INTERACTION_MUTATION_REQUIRED',
|
|
2542
|
-
'Verify interactions only after the relevant Framer mutation has succeeded.',
|
|
2543
|
-
);
|
|
2544
|
-
}
|
|
2545
2301
|
if (!taskPlan?.behaviors?.length) {
|
|
2546
2302
|
throw toolError(
|
|
2547
2303
|
'FRAMER_BEHAVIOR_CONTRACT_REQUIRED',
|
|
2548
|
-
'Declare
|
|
2549
|
-
);
|
|
2550
|
-
}
|
|
2551
|
-
const checks = normalizeBehaviorEvidence(
|
|
2552
|
-
rawArguments.checks,
|
|
2553
|
-
taskPlan,
|
|
2554
|
-
);
|
|
2555
|
-
const exhaustedBehaviorIds = uniqueStrings(
|
|
2556
|
-
checks
|
|
2557
|
-
.map((check) => check.behaviorId)
|
|
2558
|
-
.filter((behaviorId) =>
|
|
2559
|
-
behaviorVerificationAttempts.get(behaviorId)?.exhausted),
|
|
2560
|
-
);
|
|
2561
|
-
if (exhaustedBehaviorIds.length > 0) {
|
|
2562
|
-
throw toolError(
|
|
2563
|
-
'FRAMER_INTERACTION_VERIFICATION_BUDGET_EXHAUSTED',
|
|
2564
|
-
`Interaction verification is exhausted for ${exhaustedBehaviorIds.join(', ')}. Stop retrying unchanged evidence and report the work as partially verified.`,
|
|
2304
|
+
'Declare the behaviors to confirm in framer_plan_task before verifying them.',
|
|
2565
2305
|
);
|
|
2566
2306
|
}
|
|
2307
|
+
const checks = normalizeBehaviorEvidence(rawArguments.checks, taskPlan);
|
|
2567
2308
|
const pagePath = normalizePagePath(rawArguments.pagePath);
|
|
2568
|
-
const requestedIds = evidenceNodeIds(checks);
|
|
2569
|
-
const componentIds = uniqueStrings(
|
|
2570
|
-
checks.map((check) => check.componentNodeId),
|
|
2571
|
-
);
|
|
2572
2309
|
const liveResult = await execCode(
|
|
2573
2310
|
[
|
|
2574
|
-
`const requestedIds = ${JSON.stringify(
|
|
2575
|
-
|
|
2576
|
-
'
|
|
2577
|
-
'
|
|
2578
|
-
|
|
2579
|
-
'
|
|
2580
|
-
|
|
2581
|
-
' parentId: await framer.agent.getParentNode({ id },',
|
|
2582
|
-
` { pagePath: ${JSON.stringify(pagePath)} }).then((node) => node?.id ?? null).catch(() => null),`,
|
|
2583
|
-
' ancestorIds: await framer.agent.getAncestors({ id },',
|
|
2584
|
-
` { pagePath: ${JSON.stringify(pagePath)} }).then((nodes) => Array.isArray(nodes) ? nodes.map((node) => node?.id).filter(Boolean) : []).catch(() => []),`,
|
|
2585
|
-
' }))),',
|
|
2586
|
-
' componentIds.length > 0',
|
|
2587
|
-
' ? framer.agent.readComponentControls({ componentIds },',
|
|
2588
|
-
` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null)`,
|
|
2589
|
-
' : null,',
|
|
2590
|
-
']);',
|
|
2591
|
-
'console.log(JSON.stringify({ nodes, componentControls }, null, 2));',
|
|
2311
|
+
`const requestedIds = ${JSON.stringify(evidenceNodeIds(checks))};`,
|
|
2312
|
+
'const nodes = await Promise.all(requestedIds.map(async (id) => ({',
|
|
2313
|
+
' id,',
|
|
2314
|
+
' node: await framer.agent.serialize({ id, depth: 1 },',
|
|
2315
|
+
` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null),`,
|
|
2316
|
+
'})));',
|
|
2317
|
+
'console.log(JSON.stringify({ nodes }, null, 2));',
|
|
2592
2318
|
].join('\n'),
|
|
2593
2319
|
);
|
|
2594
2320
|
const parsed = parseStructuredOutput(liveResult.stdout);
|
|
2595
|
-
const
|
|
2321
|
+
const nodes = new Map(
|
|
2596
2322
|
(Array.isArray(parsed?.nodes) ? parsed.nodes : [])
|
|
2597
|
-
.filter((entry) => isRecord(entry) && typeof entry.id === 'string')
|
|
2598
|
-
.map((entry) => [entry.id,
|
|
2599
|
-
node: entry.node || null,
|
|
2600
|
-
parentId:
|
|
2601
|
-
typeof entry.parentId === 'string'
|
|
2602
|
-
? entry.parentId
|
|
2603
|
-
: null,
|
|
2604
|
-
ancestorIds: Array.isArray(entry.ancestorIds)
|
|
2605
|
-
? entry.ancestorIds
|
|
2606
|
-
: [],
|
|
2607
|
-
}]),
|
|
2323
|
+
.filter((entry) => isRecord(entry) && typeof entry.id === 'string' && entry.node)
|
|
2324
|
+
.map((entry) => [entry.id, entry.node]),
|
|
2608
2325
|
);
|
|
2609
|
-
const
|
|
2610
|
-
plan: taskPlan,
|
|
2611
|
-
checks,
|
|
2612
|
-
snapshots: nodesById,
|
|
2613
|
-
context: {
|
|
2614
|
-
lowLevelWriteCalls,
|
|
2615
|
-
authorizedInstanceContainerIds,
|
|
2616
|
-
componentControls: parsed?.componentControls || null,
|
|
2617
|
-
semanticVerifierMode,
|
|
2618
|
-
},
|
|
2619
|
-
});
|
|
2326
|
+
const results = verifyBehaviorChecks({ checks, nodes });
|
|
2620
2327
|
markInspection();
|
|
2621
2328
|
interactionVerificationCalls += 1;
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
for (const behaviorResult of verified.behaviors) {
|
|
2629
|
-
if (!checkedBehaviorIds.has(behaviorResult.behaviorId)) continue;
|
|
2630
|
-
const behaviorChecks = verified.checks.filter((check) =>
|
|
2631
|
-
check.behaviorId === behaviorResult.behaviorId);
|
|
2632
|
-
for (const check of behaviorChecks) {
|
|
2633
|
-
evidenceNodeIds([check]).forEach((nodeId) =>
|
|
2634
|
-
representativeEvidenceBehaviorByNode.set(
|
|
2635
|
-
nodeId,
|
|
2636
|
-
behaviorResult.behaviorId,
|
|
2637
|
-
));
|
|
2638
|
-
}
|
|
2639
|
-
const deterministicallyContradicted = behaviorChecks.some((check) =>
|
|
2640
|
-
check.deterministicVerdict === 'contradicted');
|
|
2641
|
-
const prior = behaviorVerificationAttempts.get(
|
|
2642
|
-
behaviorResult.behaviorId,
|
|
2643
|
-
) || {
|
|
2644
|
-
attempts: 0,
|
|
2645
|
-
unchangedEvidenceCount: 0,
|
|
2646
|
-
repairs: 0,
|
|
2647
|
-
fingerprint: null,
|
|
2648
|
-
exhausted: false,
|
|
2649
|
-
};
|
|
2650
|
-
if (behaviorResult.verdict === 'verified') {
|
|
2651
|
-
behaviorVerificationAttempts.delete(behaviorResult.behaviorId);
|
|
2652
|
-
Object.assign(behaviorResult, {
|
|
2653
|
-
verificationAttempts: prior.attempts + 1,
|
|
2654
|
-
verificationAttemptsRemaining: Math.max(
|
|
2655
|
-
0,
|
|
2656
|
-
MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR
|
|
2657
|
-
- prior.attempts
|
|
2658
|
-
- 1,
|
|
2659
|
-
),
|
|
2660
|
-
repairAttempts: prior.repairs,
|
|
2661
|
-
budgetExhausted: false,
|
|
2662
|
-
});
|
|
2663
|
-
continue;
|
|
2664
|
-
}
|
|
2665
|
-
const evidenceFingerprint = crypto
|
|
2666
|
-
.createHash('sha256')
|
|
2667
|
-
.update(JSON.stringify({
|
|
2668
|
-
mechanism: behaviorResult.mechanism,
|
|
2669
|
-
verdict: behaviorResult.verdict,
|
|
2670
|
-
contradictions: [...behaviorResult.contradictions].sort(),
|
|
2671
|
-
unknowns: [...behaviorResult.unknowns].sort(),
|
|
2672
|
-
checks: behaviorChecks.map((check) => ({
|
|
2673
|
-
verdict: check.verdict,
|
|
2674
|
-
nodeIds: evidenceNodeIds([check]).sort(),
|
|
2675
|
-
observations: check.observations,
|
|
2676
|
-
})),
|
|
2677
|
-
}))
|
|
2678
|
-
.digest('hex');
|
|
2679
|
-
const attempts = prior.attempts + 1;
|
|
2680
|
-
const unchangedEvidenceCount =
|
|
2681
|
-
prior.fingerprint === evidenceFingerprint
|
|
2682
|
-
? prior.unchangedEvidenceCount + 1
|
|
2683
|
-
: 1;
|
|
2684
|
-
const exhausted =
|
|
2685
|
-
attempts >= MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR
|
|
2686
|
-
|| unchangedEvidenceCount >= 2
|
|
2687
|
-
|| (
|
|
2688
|
-
behaviorResult.verdict === 'contradicted'
|
|
2689
|
-
&& prior.repairs >= MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
|
|
2690
|
-
);
|
|
2691
|
-
behaviorVerificationAttempts.set(behaviorResult.behaviorId, {
|
|
2692
|
-
attempts,
|
|
2693
|
-
unchangedEvidenceCount,
|
|
2694
|
-
repairs: prior.repairs,
|
|
2695
|
-
fingerprint: evidenceFingerprint,
|
|
2696
|
-
exhausted,
|
|
2697
|
-
});
|
|
2698
|
-
Object.assign(behaviorResult, {
|
|
2699
|
-
verificationAttempts: attempts,
|
|
2700
|
-
verificationAttemptsRemaining: Math.max(
|
|
2701
|
-
0,
|
|
2702
|
-
MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR - attempts,
|
|
2703
|
-
),
|
|
2704
|
-
repairAttempts: prior.repairs,
|
|
2705
|
-
budgetExhausted: exhausted,
|
|
2706
|
-
});
|
|
2707
|
-
for (const check of behaviorChecks) {
|
|
2708
|
-
check.repairAllowed =
|
|
2709
|
-
check.deterministicVerdict === 'contradicted'
|
|
2710
|
-
&& prior.repairs < MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
|
|
2711
|
-
&& !exhausted;
|
|
2712
|
-
if (!check.repairAllowed) continue;
|
|
2713
|
-
evidenceNodeIds([check]).forEach((nodeId) => {
|
|
2714
|
-
representativeRepairNodeIds.add(nodeId);
|
|
2715
|
-
representativeRepairBehaviorByNode.set(
|
|
2716
|
-
nodeId,
|
|
2717
|
-
behaviorResult.behaviorId,
|
|
2718
|
-
);
|
|
2719
|
-
});
|
|
2720
|
-
}
|
|
2721
|
-
if (
|
|
2722
|
-
!deterministicallyContradicted
|
|
2723
|
-
|| !exhausted
|
|
2724
|
-
|| !behaviorResult.mechanism
|
|
2725
|
-
) continue;
|
|
2726
|
-
const alreadyRejected = taskPlan.rejectedMechanisms.some((rejected) =>
|
|
2727
|
-
rejected.mechanism === behaviorResult.mechanism
|
|
2728
|
-
&& (
|
|
2729
|
-
rejected.behaviorId === behaviorResult.behaviorId
|
|
2730
|
-
|| rejected.behaviorSignature
|
|
2731
|
-
=== behaviorResult.behaviorSignature
|
|
2732
|
-
));
|
|
2733
|
-
if (alreadyRejected) continue;
|
|
2734
|
-
taskPlan = {
|
|
2735
|
-
...taskPlan,
|
|
2736
|
-
rejectedMechanisms: [
|
|
2737
|
-
...taskPlan.rejectedMechanisms,
|
|
2738
|
-
{
|
|
2739
|
-
behaviorId: behaviorResult.behaviorId,
|
|
2740
|
-
behaviorSignature: behaviorResult.behaviorSignature,
|
|
2741
|
-
mechanism: behaviorResult.mechanism,
|
|
2742
|
-
reason:
|
|
2743
|
-
`Repeated live evidence contradicted this mechanism: ${behaviorChecks
|
|
2744
|
-
.flatMap((check) => check.contradictions)
|
|
2745
|
-
.join('; ')
|
|
2746
|
-
.slice(0, 700)}`,
|
|
2747
|
-
source: 'verification_failure',
|
|
2748
|
-
},
|
|
2749
|
-
],
|
|
2750
|
-
};
|
|
2751
|
-
}
|
|
2752
|
-
const budgetExhaustedBehaviorIds = [...behaviorVerificationAttempts]
|
|
2753
|
-
.filter(([, attempt]) => attempt.exhausted)
|
|
2754
|
-
.map(([behaviorId]) => behaviorId);
|
|
2755
|
-
interactionVerification = {
|
|
2756
|
-
...verified,
|
|
2757
|
-
semanticVerifierMode,
|
|
2758
|
-
budgetExhaustedBehaviorIds,
|
|
2759
|
-
verifiedAt: new Date().toISOString(),
|
|
2760
|
-
};
|
|
2761
|
-
verified.representativeBehaviorIds.forEach((behaviorId) =>
|
|
2762
|
-
representativeVerifiedBehaviorIds.add(behaviorId));
|
|
2763
|
-
if (interactionVerification.completed) {
|
|
2764
|
-
verificationEvidence.set('interactions', operationSequence);
|
|
2765
|
-
} else {
|
|
2766
|
-
verificationEvidence.delete('interactions');
|
|
2329
|
+
interactionVerifiedAt = new Date().toISOString();
|
|
2330
|
+
for (const checkResult of results) {
|
|
2331
|
+
interactionCheckResults.set(
|
|
2332
|
+
`${checkResult.behaviorId}\n${checkResult.nodeId}\n${checkResult.property}`,
|
|
2333
|
+
{ sequence: operationSequence, result: checkResult },
|
|
2334
|
+
);
|
|
2767
2335
|
}
|
|
2336
|
+
const summary = interactionSummary();
|
|
2768
2337
|
result = {
|
|
2769
|
-
stdout: JSON.stringify(
|
|
2338
|
+
stdout: JSON.stringify({
|
|
2339
|
+
status: summary.status,
|
|
2340
|
+
checks: results,
|
|
2341
|
+
behaviors: summary.behaviors,
|
|
2342
|
+
}),
|
|
2770
2343
|
stderr: liveResult.stderr,
|
|
2771
2344
|
};
|
|
2772
2345
|
} else if (name === 'framer_read_project') {
|
|
@@ -3059,7 +2632,7 @@ export function createFramerAgentToolRuntime({
|
|
|
3059
2632
|
);
|
|
3060
2633
|
}
|
|
3061
2634
|
mutationCalls += 1;
|
|
3062
|
-
markMutation();
|
|
2635
|
+
markMutation({ code: true });
|
|
3063
2636
|
const mutationSequence = operationSequence;
|
|
3064
2637
|
markInspection({
|
|
3065
2638
|
verifies: parsed.typecheck?.passed === true ? ['code'] : [],
|
|
@@ -3096,8 +2669,7 @@ export function createFramerAgentToolRuntime({
|
|
|
3096
2669
|
'Framer Agent changes are required.',
|
|
3097
2670
|
);
|
|
3098
2671
|
}
|
|
3099
|
-
|
|
3100
|
-
requireTaskPlanForMutation({ changes });
|
|
2672
|
+
requireTaskPlanForMutation();
|
|
3101
2673
|
const pagePath = normalizePagePath(rawArguments.pagePath);
|
|
3102
2674
|
result = await execCode(
|
|
3103
2675
|
[
|
|
@@ -3108,8 +2680,6 @@ export function createFramerAgentToolRuntime({
|
|
|
3108
2680
|
mutationCalls += 1;
|
|
3109
2681
|
applyChangesCalls += 1;
|
|
3110
2682
|
markMutation();
|
|
3111
|
-
mutatedRepresentativeBehaviorIds.forEach((behaviorId) =>
|
|
3112
|
-
representativeMutatedBehaviorIds.add(behaviorId));
|
|
3113
2683
|
const parsedResult = parseStructuredOutput(result.stdout);
|
|
3114
2684
|
mutationReceipts.push({
|
|
3115
2685
|
sequence: operationSequence,
|
|
@@ -3183,6 +2753,9 @@ export function createFramerAgentToolRuntime({
|
|
|
3183
2753
|
name === 'framer_read_code_file'
|
|
3184
2754
|
? normalizedCodeFileReadResult(result)
|
|
3185
2755
|
: normalizedResult(result);
|
|
2756
|
+
// Result size is what this call adds to every later model turn.
|
|
2757
|
+
toolCall.resultBytes = Buffer.byteLength(JSON.stringify(normalized), 'utf8');
|
|
2758
|
+
if (modelImages.length > 0) toolCall.imageCount = modelImages.length;
|
|
3186
2759
|
if (name !== 'framer_read_project') return normalized;
|
|
3187
2760
|
const screenshotCount = screenshotQueries(rawArguments).length;
|
|
3188
2761
|
return {
|
|
@@ -3333,12 +2906,14 @@ export function createFramerAgentToolRuntime({
|
|
|
3333
2906
|
inspect: inspectFinalState,
|
|
3334
2907
|
finalizeCompletion,
|
|
3335
2908
|
verificationRepairPrompt,
|
|
2909
|
+
unblockedFailurePrompt,
|
|
3336
2910
|
sourceHash: () => null,
|
|
3337
2911
|
hasPendingChanges: () => false,
|
|
3338
2912
|
directWorkspace: true,
|
|
3339
2913
|
summary: () => ({
|
|
3340
2914
|
projectId,
|
|
3341
2915
|
projectUrl,
|
|
2916
|
+
unresolvedTargets: unresolvedTargets.map((target) => ({ ...target })),
|
|
3342
2917
|
mutationCalls,
|
|
3343
2918
|
applyChangesCalls,
|
|
3344
2919
|
lowLevelWriteCalls,
|
|
@@ -3392,10 +2967,6 @@ export function createFramerAgentToolRuntime({
|
|
|
3392
2967
|
lowLevelWriteDisabled,
|
|
3393
2968
|
lowLevelWriteBlockedCalls,
|
|
3394
2969
|
instructionFallbacks,
|
|
3395
|
-
interactionVerificationAttemptLimit:
|
|
3396
|
-
MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR,
|
|
3397
|
-
interactionRepairLimit:
|
|
3398
|
-
MAX_INTERACTION_REPAIRS_PER_BEHAVIOR,
|
|
3399
2970
|
},
|
|
3400
2971
|
branch: branchChange,
|
|
3401
2972
|
lastInspectionAt,
|
|
@@ -3422,6 +2993,7 @@ export function createFramerAgentToolRuntime({
|
|
|
3422
2993
|
projectContextHash: guidanceMetadata.projectContextHash,
|
|
3423
2994
|
files: [...guidanceMetadata.files],
|
|
3424
2995
|
bytes: guidanceMetadata.bytes,
|
|
2996
|
+
guidesRead: [...guidesRead],
|
|
3425
2997
|
},
|
|
3426
2998
|
doctrine: { ...promptMetadata },
|
|
3427
2999
|
}),
|