@gakim-digital/dexter-bridge 0.11.14 → 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 +2 -1
- package/src/agent.js +20 -9
- package/src/config.js +1 -3
- package/src/framerAgentTools.js +329 -777
- package/src/framerBehaviorContract.js +124 -1326
- package/src/protocol.js +3 -5
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
|
},
|
|
@@ -646,6 +440,50 @@ function readGuidanceFile(projectRoot, relativePath, remainingBytes, section) {
|
|
|
646
440
|
};
|
|
647
441
|
}
|
|
648
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
|
+
|
|
649
487
|
function markdownSection(markdown, heading) {
|
|
650
488
|
const lines = markdown.split('\n');
|
|
651
489
|
const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
|
@@ -779,7 +617,7 @@ function normalizeVerificationValues(values, allowed) {
|
|
|
779
617
|
.filter((value) => allowed.includes(value));
|
|
780
618
|
}
|
|
781
619
|
|
|
782
|
-
function normalizeTaskPlan(rawArguments
|
|
620
|
+
function normalizeTaskPlan(rawArguments) {
|
|
783
621
|
const summary = String(rawArguments.summary || '').trim().slice(0, 1000);
|
|
784
622
|
const domains = normalizeVerificationValues(
|
|
785
623
|
rawArguments.domains,
|
|
@@ -798,19 +636,7 @@ function normalizeTaskPlan(rawArguments, inheritedRejectedMechanisms = []) {
|
|
|
798
636
|
// The model's declared verification is authoritative; domains describe the
|
|
799
637
|
// work and never add checks on their own.
|
|
800
638
|
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
|
-
}
|
|
639
|
+
const behaviorPlan = normalizeBehaviorPlan(rawArguments);
|
|
814
640
|
if (behaviorPlan.behaviors.length > 0) {
|
|
815
641
|
if (!domains.includes('interactions')) domains.push('interactions');
|
|
816
642
|
verification.add('interactions');
|
|
@@ -936,6 +762,28 @@ function authoritativeNodeIdsFromAssignment(assignment) {
|
|
|
936
762
|
]);
|
|
937
763
|
}
|
|
938
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
|
+
|
|
939
787
|
function validateCode(value, { write = false, allowPublishing = false } = {}) {
|
|
940
788
|
const code = String(value || '').trim();
|
|
941
789
|
if (!code) {
|
|
@@ -944,7 +792,7 @@ function validateCode(value, { write = false, allowPublishing = false } = {}) {
|
|
|
944
792
|
'Framer Agent code is required.',
|
|
945
793
|
);
|
|
946
794
|
}
|
|
947
|
-
if (UNSAFE_EXEC_PATTERN.test(code)) {
|
|
795
|
+
if (UNSAFE_EXEC_PATTERN.test(codeOutsideLiterals(code))) {
|
|
948
796
|
throw toolError(
|
|
949
797
|
'FRAMER_AGENT_CODE_UNSAFE',
|
|
950
798
|
'Framer Agent code may use only the provided framer, state, and console globals.',
|
|
@@ -1246,32 +1094,9 @@ export function createFramerAgentToolRuntime({
|
|
|
1246
1094
|
);
|
|
1247
1095
|
}
|
|
1248
1096
|
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
1097
|
const activePagePath = normalizePagePath(
|
|
1260
1098
|
assignment?.context?.activePagePath,
|
|
1261
1099
|
);
|
|
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
1100
|
const authoritativeNodeIds =
|
|
1276
1101
|
authoritativeNodeIdsFromAssignment(assignment);
|
|
1277
1102
|
let sessionId = null;
|
|
@@ -1303,20 +1128,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1303
1128
|
let lastMutationAt = null;
|
|
1304
1129
|
let screenshotCalls = 0;
|
|
1305
1130
|
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();
|
|
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;
|
|
1320
1135
|
const mutationReceipts = [];
|
|
1321
1136
|
let lastScreenshotSequence = 0;
|
|
1322
1137
|
let lastScreenshotAttemptSequence = 0;
|
|
@@ -1326,6 +1141,8 @@ export function createFramerAgentToolRuntime({
|
|
|
1326
1141
|
const screenshotFailures = [];
|
|
1327
1142
|
let taskPlan = null;
|
|
1328
1143
|
const verificationEvidence = new Map();
|
|
1144
|
+
let guideCatalog = new Map();
|
|
1145
|
+
const guidesRead = [];
|
|
1329
1146
|
let guidanceMetadata = {
|
|
1330
1147
|
loaded: false,
|
|
1331
1148
|
contentHash: null,
|
|
@@ -1334,6 +1151,7 @@ export function createFramerAgentToolRuntime({
|
|
|
1334
1151
|
bytes: 0,
|
|
1335
1152
|
};
|
|
1336
1153
|
let promptMetadata = {
|
|
1154
|
+
promptBytes: 0,
|
|
1337
1155
|
doctrineLoaded: false,
|
|
1338
1156
|
doctrineVersion: null,
|
|
1339
1157
|
};
|
|
@@ -1343,6 +1161,7 @@ export function createFramerAgentToolRuntime({
|
|
|
1343
1161
|
const framerAgentVersion = installedFramerAgentVersion();
|
|
1344
1162
|
let operationSequence = 0;
|
|
1345
1163
|
let lastMutationSequence = 0;
|
|
1164
|
+
let lastCodeMutationSequence = 0;
|
|
1346
1165
|
let lastInspectionSequence = 0;
|
|
1347
1166
|
const abortController = new AbortController();
|
|
1348
1167
|
|
|
@@ -1649,6 +1468,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1649
1468
|
if (!fs.existsSync(projectRoot)) {
|
|
1650
1469
|
return { content: '', ...guidanceMetadata };
|
|
1651
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.
|
|
1652
1475
|
const requestedFiles = [
|
|
1653
1476
|
{ root: skillRoot, path: 'SKILL.md', label: 'SKILL.md' },
|
|
1654
1477
|
{ root: projectRoot, path: 'index.md', label: 'project/index.md' },
|
|
@@ -1657,24 +1480,19 @@ export function createFramerAgentToolRuntime({
|
|
|
1657
1480
|
path: 'project-inventory.md',
|
|
1658
1481
|
label: 'project/project-inventory.md',
|
|
1659
1482
|
},
|
|
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
1483
|
{ root: projectRoot, path: 'prompt/overview.md' },
|
|
1484
|
+
{ root: projectRoot, path: 'prompt/core-principles.md' },
|
|
1666
1485
|
{ root: projectRoot, path: 'prompt/guardrails.md' },
|
|
1667
1486
|
{ 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
1487
|
{ root: projectRoot, path: 'prompt/design-rules.md' },
|
|
1677
|
-
{ root: projectRoot, path: 'prompt/
|
|
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
|
+
})),
|
|
1678
1496
|
];
|
|
1679
1497
|
const files = [];
|
|
1680
1498
|
let remainingBytes = MAX_GUIDANCE_BYTES;
|
|
@@ -1693,9 +1511,19 @@ export function createFramerAgentToolRuntime({
|
|
|
1693
1511
|
remainingBytes -= Buffer.byteLength(loaded.content, 'utf8');
|
|
1694
1512
|
if (remainingBytes <= 0) break;
|
|
1695
1513
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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');
|
|
1699
1527
|
const inventory = files.find(
|
|
1700
1528
|
(file) => file.path.endsWith('project-inventory.md'),
|
|
1701
1529
|
)?.content || '';
|
|
@@ -1716,8 +1544,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1716
1544
|
const configurePromptContext = ({
|
|
1717
1545
|
doctrineLoaded = false,
|
|
1718
1546
|
doctrineVersion = null,
|
|
1547
|
+
promptBytes = 0,
|
|
1719
1548
|
} = {}) => {
|
|
1720
1549
|
promptMetadata = {
|
|
1550
|
+
promptBytes: Math.max(0, Number(promptBytes) || 0),
|
|
1721
1551
|
doctrineLoaded: doctrineLoaded === true,
|
|
1722
1552
|
doctrineVersion:
|
|
1723
1553
|
typeof doctrineVersion === 'string' && doctrineVersion.trim()
|
|
@@ -1858,9 +1688,14 @@ export function createFramerAgentToolRuntime({
|
|
|
1858
1688
|
return result;
|
|
1859
1689
|
};
|
|
1860
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 = [];
|
|
1861
1696
|
const validateAuthoritativeNodeIds = async () => {
|
|
1862
1697
|
await ensureSession();
|
|
1863
|
-
if (authoritativeNodeIds.length === 0) return;
|
|
1698
|
+
if (authoritativeNodeIds.length === 0) return { unresolvedTargets };
|
|
1864
1699
|
if (nodeValidationPromise) return nodeValidationPromise;
|
|
1865
1700
|
nodeValidationPromise = (async () => {
|
|
1866
1701
|
reportActivity({
|
|
@@ -1872,9 +1707,13 @@ export function createFramerAgentToolRuntime({
|
|
|
1872
1707
|
[
|
|
1873
1708
|
`const requestedIds = ${JSON.stringify(authoritativeNodeIds)};`,
|
|
1874
1709
|
'const resolved = await Promise.all(requestedIds.map(async (requestedId) => {',
|
|
1875
|
-
'
|
|
1876
|
-
|
|
1877
|
-
|
|
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
|
+
' }',
|
|
1878
1717
|
'}));',
|
|
1879
1718
|
'console.log(JSON.stringify({ resolved }));',
|
|
1880
1719
|
].join('\n'),
|
|
@@ -1884,26 +1723,30 @@ export function createFramerAgentToolRuntime({
|
|
|
1884
1723
|
const resolved = Array.isArray(parsed?.resolved)
|
|
1885
1724
|
? parsed.resolved
|
|
1886
1725
|
: [];
|
|
1887
|
-
|
|
1726
|
+
unresolvedTargets = authoritativeNodeIds.flatMap((requestedId) => {
|
|
1888
1727
|
const match = resolved.find(
|
|
1889
1728
|
(entry) =>
|
|
1890
1729
|
isRecord(entry)
|
|
1891
1730
|
&& entry.requestedId === requestedId,
|
|
1892
1731
|
);
|
|
1893
|
-
|
|
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
|
+
}];
|
|
1894
1741
|
});
|
|
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
1742
|
reportActivity({
|
|
1903
1743
|
kind: 'status_update',
|
|
1904
1744
|
source: 'framer-agent',
|
|
1905
|
-
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.',
|
|
1906
1748
|
});
|
|
1749
|
+
return { unresolvedTargets };
|
|
1907
1750
|
})().catch((error) => {
|
|
1908
1751
|
nodeValidationPromise = null;
|
|
1909
1752
|
throw error;
|
|
@@ -1954,9 +1797,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1954
1797
|
}
|
|
1955
1798
|
};
|
|
1956
1799
|
|
|
1957
|
-
const markMutation = () => {
|
|
1800
|
+
const markMutation = ({ code = false } = {}) => {
|
|
1958
1801
|
operationSequence += 1;
|
|
1959
1802
|
lastMutationSequence = operationSequence;
|
|
1803
|
+
if (code) lastCodeMutationSequence = operationSequence;
|
|
1960
1804
|
lastMutationAt = new Date().toISOString();
|
|
1961
1805
|
};
|
|
1962
1806
|
|
|
@@ -1972,30 +1816,35 @@ export function createFramerAgentToolRuntime({
|
|
|
1972
1816
|
? {
|
|
1973
1817
|
behaviors: taskPlan.behaviors.map((behavior) => ({
|
|
1974
1818
|
...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],
|
|
1819
|
+
targetNodeIds: [...behavior.targetNodeIds],
|
|
1988
1820
|
})),
|
|
1989
1821
|
}
|
|
1990
1822
|
: {}),
|
|
1991
1823
|
};
|
|
1992
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
|
+
};
|
|
1993
1836
|
const synchronized = () =>
|
|
1994
1837
|
mutationCalls === 0 || lastInspectionSequence > lastMutationSequence;
|
|
1995
1838
|
const verificationCheckCompleted = (check) => {
|
|
1996
1839
|
if (mutationCalls === 0) return true;
|
|
1997
1840
|
if (check === 'structural') return synchronized();
|
|
1998
|
-
|
|
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;
|
|
1999
1848
|
};
|
|
2000
1849
|
const visualVerificationRequired = () =>
|
|
2001
1850
|
requiredVerificationChecks().includes('visual');
|
|
@@ -2013,12 +1862,19 @@ export function createFramerAgentToolRuntime({
|
|
|
2013
1862
|
const requiredChecks = () => [
|
|
2014
1863
|
...new Set(['structural', ...requiredVerificationChecks()]),
|
|
2015
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
|
+
];
|
|
2016
1871
|
const missingVerificationChecks = () => {
|
|
2017
1872
|
if (mutationCalls === 0) return [];
|
|
2018
1873
|
const missing = [];
|
|
2019
1874
|
if (!taskPlan) missing.push('task-plan');
|
|
1875
|
+
const unavailable = unavailableChecks();
|
|
2020
1876
|
for (const check of requiredChecks()) {
|
|
2021
|
-
if (check
|
|
1877
|
+
if (unavailable.includes(check)) continue;
|
|
2022
1878
|
if (!verificationCheckCompleted(check)) missing.push(check);
|
|
2023
1879
|
}
|
|
2024
1880
|
return missing;
|
|
@@ -2046,48 +1902,17 @@ export function createFramerAgentToolRuntime({
|
|
|
2046
1902
|
? {
|
|
2047
1903
|
interactionVerificationCalls,
|
|
2048
1904
|
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,
|
|
1905
|
+
status: interactionStatus(),
|
|
1906
|
+
behaviors: taskPlan?.behaviors?.length ? interactionSummary().behaviors : [],
|
|
1907
|
+
checks: freshInteractionChecks().map((check) => ({ ...check })),
|
|
1908
|
+
verifiedAt: interactionVerifiedAt,
|
|
2083
1909
|
},
|
|
2084
1910
|
}
|
|
2085
1911
|
: {}),
|
|
2086
1912
|
taskPlan: taskPlanSnapshot(),
|
|
2087
1913
|
requiredChecks: requiredChecks(),
|
|
2088
1914
|
completedChecks: requiredChecks().filter(verificationCheckCompleted),
|
|
2089
|
-
unavailableChecks:
|
|
2090
|
-
visualVerificationStatus() === 'unavailable' ? ['visual'] : [],
|
|
1915
|
+
unavailableChecks: unavailableChecks(),
|
|
2091
1916
|
missingChecks: missingVerificationChecks(),
|
|
2092
1917
|
lastMutationAt,
|
|
2093
1918
|
lastInspectionAt,
|
|
@@ -2109,49 +1934,58 @@ export function createFramerAgentToolRuntime({
|
|
|
2109
1934
|
};
|
|
2110
1935
|
};
|
|
2111
1936
|
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
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
|
+
? [{
|
|
2119
1949
|
command: 'Visual verification',
|
|
2120
1950
|
status: 'skipped',
|
|
2121
1951
|
output:
|
|
2122
1952
|
'Screenshots of the final canvas could not be delivered to the model, so the visual result is unverified.',
|
|
2123
|
-
}
|
|
2124
|
-
],
|
|
2125
|
-
|
|
2126
|
-
|
|
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
|
+
};
|
|
2127
1966
|
|
|
2128
1967
|
const finalizeCompletion = (completion) => {
|
|
2129
1968
|
if (completion?.status !== 'completed' || mutationCalls === 0) {
|
|
2130
1969
|
return completion;
|
|
2131
1970
|
}
|
|
2132
|
-
if (verification().completed) return
|
|
1971
|
+
if (verification().completed) return withUnavailableChecks(completion);
|
|
2133
1972
|
const missing = missingVerificationChecks().map((check) => {
|
|
2134
1973
|
if (check === 'task-plan') return 'a harness task and verification plan';
|
|
2135
1974
|
if (check === 'structural') return 'a final structural read';
|
|
2136
1975
|
if (check === 'visual') return 'post-mutation screenshot verification';
|
|
2137
1976
|
return `post-mutation ${check} verification`;
|
|
2138
1977
|
});
|
|
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
|
-
});
|
|
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'}`);
|
|
2148
1982
|
const reason = [
|
|
2149
1983
|
`The harness ended before completing ${missing.join(' and ')}.`,
|
|
2150
1984
|
...(interactionDetails.length > 0
|
|
2151
1985
|
? [`Interaction evidence: ${interactionDetails.join('; ')}.`]
|
|
2152
1986
|
: []),
|
|
2153
1987
|
].join(' ');
|
|
2154
|
-
const failed =
|
|
1988
|
+
const failed = withUnavailableChecks(completion);
|
|
2155
1989
|
return {
|
|
2156
1990
|
...failed,
|
|
2157
1991
|
status: 'failed',
|
|
@@ -2170,6 +2004,22 @@ export function createFramerAgentToolRuntime({
|
|
|
2170
2004
|
};
|
|
2171
2005
|
};
|
|
2172
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
|
+
|
|
2173
2023
|
const verificationRepairPrompt = () => {
|
|
2174
2024
|
const current = verification();
|
|
2175
2025
|
if (current.completed || mutationCalls === 0) return null;
|
|
@@ -2185,27 +2035,33 @@ export function createFramerAgentToolRuntime({
|
|
|
2185
2035
|
missing.push(
|
|
2186
2036
|
'capture and inspect a screenshot of the affected canvas after the latest mutation',
|
|
2187
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 '));
|
|
2188
2056
|
} 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
|
-
);
|
|
2057
|
+
missing.push(`perform a focused read tagged with verifies:["${check}"] after the latest mutation`);
|
|
2202
2058
|
}
|
|
2203
2059
|
}
|
|
2204
2060
|
return [
|
|
2205
2061
|
'Continue the same Framer task from its current project state.',
|
|
2206
2062
|
'Do not redo successful work.',
|
|
2207
2063
|
`Before returning, ${missing.join(' and ')}.`,
|
|
2208
|
-
'
|
|
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.',
|
|
2209
2065
|
].join(' ');
|
|
2210
2066
|
};
|
|
2211
2067
|
|
|
@@ -2250,109 +2106,13 @@ export function createFramerAgentToolRuntime({
|
|
|
2250
2106
|
.update(`${name}\n${JSON.stringify(rawArguments)}`)
|
|
2251
2107
|
.digest('hex');
|
|
2252
2108
|
|
|
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 = '' } = {}) => {
|
|
2109
|
+
const requireTaskPlanForMutation = () => {
|
|
2268
2110
|
if (!taskPlan) {
|
|
2269
2111
|
throw toolError(
|
|
2270
2112
|
'FRAMER_HARNESS_PLAN_REQUIRED',
|
|
2271
2113
|
'Inspect the live Framer target, then call framer_plan_task before making a mutation.',
|
|
2272
2114
|
);
|
|
2273
2115
|
}
|
|
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
2116
|
};
|
|
2357
2117
|
|
|
2358
2118
|
async function invokeUnsafe(name, rawArguments = {}) {
|
|
@@ -2444,7 +2204,40 @@ export function createFramerAgentToolRuntime({
|
|
|
2444
2204
|
const queries = Array.isArray(rawArguments.queries)
|
|
2445
2205
|
? uniqueStrings(rawArguments.queries, 8)
|
|
2446
2206
|
: [];
|
|
2447
|
-
|
|
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) {
|
|
2448
2241
|
try {
|
|
2449
2242
|
result = await executeCli(
|
|
2450
2243
|
['docs', ...queries],
|
|
@@ -2487,22 +2280,7 @@ export function createFramerAgentToolRuntime({
|
|
|
2487
2280
|
'Inspect the live Framer target before declaring the task and verification plan.',
|
|
2488
2281
|
);
|
|
2489
2282
|
}
|
|
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);
|
|
2283
|
+
const nextPlan = normalizeTaskPlan(rawArguments);
|
|
2506
2284
|
taskPlan = {
|
|
2507
2285
|
summary: nextPlan.summary,
|
|
2508
2286
|
domains: uniqueStrings([
|
|
@@ -2513,280 +2291,55 @@ export function createFramerAgentToolRuntime({
|
|
|
2513
2291
|
...(taskPlan?.verification || []),
|
|
2514
2292
|
...nextPlan.verification,
|
|
2515
2293
|
]),
|
|
2516
|
-
...
|
|
2294
|
+
...mergeBehaviorPlans(taskPlan, nextPlan),
|
|
2517
2295
|
};
|
|
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
2296
|
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
|
-
}),
|
|
2297
|
+
stdout: JSON.stringify({ plan: taskPlanSnapshot() }),
|
|
2556
2298
|
stderr: '',
|
|
2557
2299
|
};
|
|
2558
2300
|
} 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
2301
|
if (!taskPlan?.behaviors?.length) {
|
|
2566
2302
|
throw toolError(
|
|
2567
2303
|
'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.`,
|
|
2304
|
+
'Declare the behaviors to confirm in framer_plan_task before verifying them.',
|
|
2585
2305
|
);
|
|
2586
2306
|
}
|
|
2307
|
+
const checks = normalizeBehaviorEvidence(rawArguments.checks, taskPlan);
|
|
2587
2308
|
const pagePath = normalizePagePath(rawArguments.pagePath);
|
|
2588
|
-
const requestedIds = evidenceNodeIds(checks);
|
|
2589
|
-
const componentIds = uniqueStrings(
|
|
2590
|
-
checks.map((check) => check.componentNodeId),
|
|
2591
|
-
);
|
|
2592
2309
|
const liveResult = await execCode(
|
|
2593
2310
|
[
|
|
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));',
|
|
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));',
|
|
2612
2318
|
].join('\n'),
|
|
2613
2319
|
);
|
|
2614
2320
|
const parsed = parseStructuredOutput(liveResult.stdout);
|
|
2615
|
-
const
|
|
2321
|
+
const nodes = new Map(
|
|
2616
2322
|
(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
|
-
}]),
|
|
2323
|
+
.filter((entry) => isRecord(entry) && typeof entry.id === 'string' && entry.node)
|
|
2324
|
+
.map((entry) => [entry.id, entry.node]),
|
|
2628
2325
|
);
|
|
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
|
-
});
|
|
2326
|
+
const results = verifyBehaviorChecks({ checks, nodes });
|
|
2640
2327
|
markInspection();
|
|
2641
2328
|
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');
|
|
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
|
+
);
|
|
2787
2335
|
}
|
|
2336
|
+
const summary = interactionSummary();
|
|
2788
2337
|
result = {
|
|
2789
|
-
stdout: JSON.stringify(
|
|
2338
|
+
stdout: JSON.stringify({
|
|
2339
|
+
status: summary.status,
|
|
2340
|
+
checks: results,
|
|
2341
|
+
behaviors: summary.behaviors,
|
|
2342
|
+
}),
|
|
2790
2343
|
stderr: liveResult.stderr,
|
|
2791
2344
|
};
|
|
2792
2345
|
} else if (name === 'framer_read_project') {
|
|
@@ -3079,7 +2632,7 @@ export function createFramerAgentToolRuntime({
|
|
|
3079
2632
|
);
|
|
3080
2633
|
}
|
|
3081
2634
|
mutationCalls += 1;
|
|
3082
|
-
markMutation();
|
|
2635
|
+
markMutation({ code: true });
|
|
3083
2636
|
const mutationSequence = operationSequence;
|
|
3084
2637
|
markInspection({
|
|
3085
2638
|
verifies: parsed.typecheck?.passed === true ? ['code'] : [],
|
|
@@ -3116,8 +2669,7 @@ export function createFramerAgentToolRuntime({
|
|
|
3116
2669
|
'Framer Agent changes are required.',
|
|
3117
2670
|
);
|
|
3118
2671
|
}
|
|
3119
|
-
|
|
3120
|
-
requireTaskPlanForMutation({ changes });
|
|
2672
|
+
requireTaskPlanForMutation();
|
|
3121
2673
|
const pagePath = normalizePagePath(rawArguments.pagePath);
|
|
3122
2674
|
result = await execCode(
|
|
3123
2675
|
[
|
|
@@ -3128,8 +2680,6 @@ export function createFramerAgentToolRuntime({
|
|
|
3128
2680
|
mutationCalls += 1;
|
|
3129
2681
|
applyChangesCalls += 1;
|
|
3130
2682
|
markMutation();
|
|
3131
|
-
mutatedRepresentativeBehaviorIds.forEach((behaviorId) =>
|
|
3132
|
-
representativeMutatedBehaviorIds.add(behaviorId));
|
|
3133
2683
|
const parsedResult = parseStructuredOutput(result.stdout);
|
|
3134
2684
|
mutationReceipts.push({
|
|
3135
2685
|
sequence: operationSequence,
|
|
@@ -3203,6 +2753,9 @@ export function createFramerAgentToolRuntime({
|
|
|
3203
2753
|
name === 'framer_read_code_file'
|
|
3204
2754
|
? normalizedCodeFileReadResult(result)
|
|
3205
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;
|
|
3206
2759
|
if (name !== 'framer_read_project') return normalized;
|
|
3207
2760
|
const screenshotCount = screenshotQueries(rawArguments).length;
|
|
3208
2761
|
return {
|
|
@@ -3353,12 +2906,14 @@ export function createFramerAgentToolRuntime({
|
|
|
3353
2906
|
inspect: inspectFinalState,
|
|
3354
2907
|
finalizeCompletion,
|
|
3355
2908
|
verificationRepairPrompt,
|
|
2909
|
+
unblockedFailurePrompt,
|
|
3356
2910
|
sourceHash: () => null,
|
|
3357
2911
|
hasPendingChanges: () => false,
|
|
3358
2912
|
directWorkspace: true,
|
|
3359
2913
|
summary: () => ({
|
|
3360
2914
|
projectId,
|
|
3361
2915
|
projectUrl,
|
|
2916
|
+
unresolvedTargets: unresolvedTargets.map((target) => ({ ...target })),
|
|
3362
2917
|
mutationCalls,
|
|
3363
2918
|
applyChangesCalls,
|
|
3364
2919
|
lowLevelWriteCalls,
|
|
@@ -3412,10 +2967,6 @@ export function createFramerAgentToolRuntime({
|
|
|
3412
2967
|
lowLevelWriteDisabled,
|
|
3413
2968
|
lowLevelWriteBlockedCalls,
|
|
3414
2969
|
instructionFallbacks,
|
|
3415
|
-
interactionVerificationAttemptLimit:
|
|
3416
|
-
MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR,
|
|
3417
|
-
interactionRepairLimit:
|
|
3418
|
-
MAX_INTERACTION_REPAIRS_PER_BEHAVIOR,
|
|
3419
2970
|
},
|
|
3420
2971
|
branch: branchChange,
|
|
3421
2972
|
lastInspectionAt,
|
|
@@ -3442,6 +2993,7 @@ export function createFramerAgentToolRuntime({
|
|
|
3442
2993
|
projectContextHash: guidanceMetadata.projectContextHash,
|
|
3443
2994
|
files: [...guidanceMetadata.files],
|
|
3444
2995
|
bytes: guidanceMetadata.bytes,
|
|
2996
|
+
guidesRead: [...guidesRead],
|
|
3445
2997
|
},
|
|
3446
2998
|
doctrine: { ...promptMetadata },
|
|
3447
2999
|
}),
|