@gakim-digital/dexter-bridge 0.11.4 → 0.11.6

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.
@@ -24,6 +24,8 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 5 * 60_000;
24
24
  const SESSION_TIMEOUT_MS = 10 * 60_000;
25
25
  const MAX_UNSAFE_WRITE_FAILURES = 2;
26
26
  const MAX_LOW_LEVEL_WRITE_FAILURES = 3;
27
+ const MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR = 3;
28
+ const MAX_INTERACTION_REPAIRS_PER_BEHAVIOR = 1;
27
29
  const MAX_GUIDANCE_BYTES = 180 * 1024;
28
30
  const MAX_GUIDANCE_FILE_BYTES = 48 * 1024;
29
31
  const PROJECT_ID_PATTERN = /^[A-Za-z0-9]{20}$/;
@@ -189,6 +191,15 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
189
191
  type: 'array',
190
192
  maxItems: 20,
191
193
  items: { type: 'string', minLength: 1, maxLength: 500 },
194
+ description:
195
+ 'Optional exact literal text that must exist in the interaction target. Do not put behavioral descriptions or prose assertions here.',
196
+ },
197
+ expectedDestinations: {
198
+ type: 'array',
199
+ maxItems: 20,
200
+ items: { type: 'string', minLength: 1, maxLength: 500 },
201
+ description:
202
+ 'Required for navigation behavior. Use exact routes or URLs such as /contact.',
192
203
  },
193
204
  initialState: {
194
205
  type: 'string',
@@ -237,6 +248,25 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
237
248
  maxLength: 800,
238
249
  },
239
250
  representativeFirst: { type: 'boolean', default: true },
251
+ targetNodeIds: {
252
+ type: 'array',
253
+ maxItems: 50,
254
+ uniqueItems: true,
255
+ items: {
256
+ type: 'string',
257
+ minLength: 1,
258
+ maxLength: 200,
259
+ },
260
+ description:
261
+ 'All intended target node IDs. Required when expectedTargets is greater than one.',
262
+ },
263
+ representativeTargetNodeId: {
264
+ type: 'string',
265
+ minLength: 1,
266
+ maxLength: 200,
267
+ description:
268
+ 'The single target to implement and verify before fan-out.',
269
+ },
240
270
  },
241
271
  required: ['behaviorId', 'mechanism', 'rationale'],
242
272
  additionalProperties: false,
@@ -278,7 +308,7 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
278
308
  {
279
309
  name: 'framer_verify_interactions',
280
310
  description:
281
- 'Verify the observable behavior contract from the live Framer tree. The runtime derives the required action and invariants from the planned behavior and mechanism; supply canonical evidence node IDs. For native_effect, use hoverEffect, tapEffect, or appearEffect as the event value. This is the only tool that can satisfy interaction verification.',
311
+ 'Verify the observable behavior contract from live Framer evidence. Results are verified, contradicted, or unknown. Unknown evidence never proves failure and must not trigger mutation. Native links and component-control navigation are verified from destinations, not onTap. You may add a grounded semanticAssessment; it cannot override a deterministic contradiction or authorize writes.',
282
312
  inputSchema: {
283
313
  type: 'object',
284
314
  properties: {
@@ -310,6 +340,11 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
310
340
  minLength: 1,
311
341
  maxLength: 80,
312
342
  },
343
+ expectedDestination: {
344
+ type: 'string',
345
+ minLength: 1,
346
+ maxLength: 500,
347
+ },
313
348
  targetNodeId: {
314
349
  type: 'string',
315
350
  minLength: 1,
@@ -350,12 +385,34 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
350
385
  minLength: 1,
351
386
  maxLength: 200,
352
387
  },
388
+ semanticAssessment: {
389
+ type: 'object',
390
+ properties: {
391
+ verdict: {
392
+ type: 'string',
393
+ enum: ['verified', 'contradicted', 'unknown'],
394
+ },
395
+ rationale: {
396
+ type: 'string',
397
+ minLength: 10,
398
+ maxLength: 1200,
399
+ },
400
+ evidenceNodeIds: {
401
+ type: 'array',
402
+ maxItems: 50,
403
+ uniqueItems: true,
404
+ items: {
405
+ type: 'string',
406
+ minLength: 1,
407
+ maxLength: 200,
408
+ },
409
+ },
410
+ },
411
+ required: ['verdict', 'rationale', 'evidenceNodeIds'],
412
+ additionalProperties: false,
413
+ },
353
414
  },
354
- required: [
355
- 'behaviorId',
356
- 'triggerNodeId',
357
- 'event',
358
- ],
415
+ required: ['behaviorId'],
359
416
  additionalProperties: false,
360
417
  },
361
418
  },
@@ -1053,6 +1110,19 @@ export function createFramerAgentToolRuntime({
1053
1110
  const activePagePath = normalizePagePath(
1054
1111
  assignment?.context?.activePagePath,
1055
1112
  );
1113
+ const semanticVerifierMode =
1114
+ ['off', 'shadow', 'enforce'].includes(
1115
+ String(
1116
+ assignment?.context?.harnessAuthority?.semanticVerifierMode
1117
+ || env.DEXTER_FRAMER_SEMANTIC_VERIFIER_MODE
1118
+ || '',
1119
+ ).trim(),
1120
+ )
1121
+ ? String(
1122
+ assignment?.context?.harnessAuthority?.semanticVerifierMode
1123
+ || env.DEXTER_FRAMER_SEMANTIC_VERIFIER_MODE,
1124
+ ).trim()
1125
+ : 'enforce';
1056
1126
  const authoritativeNodeIds =
1057
1127
  authoritativeNodeIdsFromAssignment(assignment);
1058
1128
  let sessionId = null;
@@ -1086,14 +1156,18 @@ export function createFramerAgentToolRuntime({
1086
1156
  let interactionVerificationCalls = 0;
1087
1157
  let interactionVerification = {
1088
1158
  completed: false,
1089
- verificationLevel: 'unverified',
1159
+ verificationLevel: 'unknown',
1090
1160
  checks: [],
1091
1161
  behaviors: [],
1162
+ budgetExhaustedBehaviorIds: [],
1092
1163
  verifiedAt: null,
1093
1164
  };
1094
1165
  const representativeVerifiedBehaviorIds = new Set();
1166
+ const representativeMutatedBehaviorIds = new Set();
1095
1167
  const representativeRepairNodeIds = new Set();
1096
- const repeatedBehaviorVerificationFailures = new Map();
1168
+ const representativeRepairBehaviorByNode = new Map();
1169
+ const representativeEvidenceBehaviorByNode = new Map();
1170
+ const behaviorVerificationAttempts = new Map();
1097
1171
  const mutationReceipts = [];
1098
1172
  let screenshotBeforeMutation = false;
1099
1173
  let lastScreenshotSequence = 0;
@@ -1588,9 +1662,11 @@ export function createFramerAgentToolRuntime({
1588
1662
  behaviors: taskPlan.behaviors.map((behavior) => ({
1589
1663
  ...behavior,
1590
1664
  expectedContent: [...behavior.expectedContent],
1665
+ expectedDestinations: [...behavior.expectedDestinations],
1591
1666
  })),
1592
1667
  mechanisms: taskPlan.mechanisms.map((decision) => ({
1593
1668
  ...decision,
1669
+ targetNodeIds: [...decision.targetNodeIds],
1594
1670
  })),
1595
1671
  rejectedMechanisms: taskPlan.rejectedMechanisms.map((entry) => ({
1596
1672
  ...entry,
@@ -1657,15 +1733,35 @@ export function createFramerAgentToolRuntime({
1657
1733
  verificationLevel:
1658
1734
  verificationCheckCompleted('interactions')
1659
1735
  ? interactionVerification.verificationLevel
1660
- : 'unverified',
1736
+ : 'unknown',
1661
1737
  checks: interactionVerification.checks.map((check) => ({
1662
1738
  ...check,
1663
- failures: [...check.failures],
1739
+ contradictions: [...check.contradictions],
1740
+ unknowns: [...check.unknowns],
1741
+ observations: check.observations.map((observation) => ({
1742
+ ...observation,
1743
+ })),
1744
+ nextSafeActions: [...check.nextSafeActions],
1745
+ ...(check.semanticAssessment
1746
+ ? {
1747
+ semanticAssessment: {
1748
+ ...check.semanticAssessment,
1749
+ evidenceNodeIds: [
1750
+ ...check.semanticAssessment.evidenceNodeIds,
1751
+ ],
1752
+ },
1753
+ }
1754
+ : {}),
1664
1755
  })),
1665
1756
  behaviors: interactionVerification.behaviors.map((behavior) => ({
1666
1757
  ...behavior,
1667
- failures: [...behavior.failures],
1758
+ contradictions: [...behavior.contradictions],
1759
+ unknowns: [...behavior.unknowns],
1668
1760
  })),
1761
+ semanticVerifierMode,
1762
+ budgetExhaustedBehaviorIds: [
1763
+ ...interactionVerification.budgetExhaustedBehaviorIds,
1764
+ ],
1669
1765
  verifiedAt: interactionVerification.verifiedAt,
1670
1766
  },
1671
1767
  }
@@ -1720,12 +1816,27 @@ export function createFramerAgentToolRuntime({
1720
1816
  if (check === 'visual') return 'before-and-after screenshot verification';
1721
1817
  return `post-mutation ${check} verification`;
1722
1818
  });
1723
- const reason =
1724
- `The harness ended before completing ${missing.join(' and ')}.`;
1819
+ const interactionDetails = interactionVerification.behaviors
1820
+ .filter((behavior) => behavior.verdict !== 'verified')
1821
+ .map((behavior) => {
1822
+ const detail =
1823
+ behavior.contradictions[0]
1824
+ || behavior.unknowns[0]
1825
+ || 'no conclusive live evidence';
1826
+ return `${behavior.behaviorId}=${behavior.verdict} (${detail})`;
1827
+ });
1828
+ const reason = [
1829
+ `The harness ended before completing ${missing.join(' and ')}.`,
1830
+ ...(interactionDetails.length > 0
1831
+ ? [`Interaction evidence: ${interactionDetails.join('; ')}.`]
1832
+ : []),
1833
+ ].join(' ');
1725
1834
  return {
1726
1835
  ...completion,
1727
1836
  status: 'failed',
1728
- summary: `${String(completion?.summary || '').trim()} ${reason}`.trim(),
1837
+ summary:
1838
+ String(completion?.summary || '').trim()
1839
+ || 'The requested changes were applied.',
1729
1840
  checks: [
1730
1841
  ...(Array.isArray(completion?.checks) ? completion.checks : []),
1731
1842
  {
@@ -1756,9 +1867,17 @@ export function createFramerAgentToolRuntime({
1756
1867
  : 'report that the required baseline screenshot was missed; do not claim visual verification',
1757
1868
  );
1758
1869
  } else {
1870
+ const interactionState = interactionVerification.behaviors
1871
+ .map((behavior) =>
1872
+ `${behavior.behaviorId}:${behavior.verdict}`)
1873
+ .join(', ');
1874
+ const exhausted =
1875
+ interactionVerification.budgetExhaustedBehaviorIds.length > 0;
1759
1876
  missing.push(
1760
1877
  check === 'interactions'
1761
- ? 'call framer_verify_interactions with canonical evidence node IDs for every declared behavior and chosen mechanism'
1878
+ ? exhausted
1879
+ ? `report interaction verification as incomplete without further mutation or repeated verification (${interactionState || 'unknown'})`
1880
+ : `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'})`
1762
1881
  : `perform a focused read tagged with verifies:["${check}"] after the latest mutation`,
1763
1882
  );
1764
1883
  }
@@ -1767,7 +1886,7 @@ export function createFramerAgentToolRuntime({
1767
1886
  'Continue the same Framer task from its current project state.',
1768
1887
  'Do not redo successful work.',
1769
1888
  `Before returning, ${missing.join(' and ')}.`,
1770
- 'Repair only concrete problems you observe, then return the required JSON outcome.',
1889
+ 'Never mutate merely because evidence is unknown. Repair one observed contradiction at most once, then return the required JSON outcome.',
1771
1890
  ].join(' ');
1772
1891
  };
1773
1892
 
@@ -1861,22 +1980,70 @@ export function createFramerAgentToolRuntime({
1861
1980
  `Choose a different mechanism for ${rejectedSelectedMechanisms.map((behavior) => behavior.id).join(', ')} before making another mutation.`,
1862
1981
  );
1863
1982
  }
1864
- if (mutationCalls > 0) {
1865
- const pendingRepresentative = taskPlan.behaviors
1866
- .filter((behavior) => behavior.expectedTargets > 1)
1867
- .filter((behavior) =>
1868
- taskPlan.mechanisms.find((decision) =>
1869
- decision.behaviorId === behavior.id
1870
- && decision.representativeFirst))
1871
- .filter((behavior) =>
1872
- !representativeVerifiedBehaviorIds.has(behavior.id));
1873
- if (pendingRepresentative.length > 0 && !isRepresentativeRepair) {
1983
+ const changedNodeIds = new Set(
1984
+ summarizeFramerChanges(changes).commands
1985
+ .flatMap((command) => [
1986
+ command.nodeId,
1987
+ command.sourceNodeId,
1988
+ ])
1989
+ .filter(Boolean),
1990
+ );
1991
+ const rawChanges = String(changes || '');
1992
+ const pendingRepresentative = taskPlan.behaviors
1993
+ .filter((behavior) => behavior.expectedTargets > 1)
1994
+ .map((behavior) => ({
1995
+ behavior,
1996
+ decision: taskPlan.mechanisms.find((decision) =>
1997
+ decision.behaviorId === behavior.id),
1998
+ }))
1999
+ .filter(({ decision }) => decision?.representativeFirst)
2000
+ .filter(({ behavior }) =>
2001
+ !representativeVerifiedBehaviorIds.has(behavior.id));
2002
+ for (const { behavior, decision } of pendingRepresentative) {
2003
+ const touchedTargets = decision.targetNodeIds.filter((nodeId) =>
2004
+ rawChanges.includes(nodeId));
2005
+ const touchedEvidence = [...changedNodeIds].filter((nodeId) =>
2006
+ representativeEvidenceBehaviorByNode.get(nodeId) === behavior.id);
2007
+ if (
2008
+ touchedTargets.length > 1
2009
+ || (
2010
+ touchedTargets.length === 1
2011
+ && touchedTargets[0] !== decision.representativeTargetNodeId
2012
+ )
2013
+ ) {
2014
+ throw toolError(
2015
+ 'FRAMER_BEHAVIOR_REPRESENTATIVE_MUTATION_REQUIRED',
2016
+ `Implement only representative target ${decision.representativeTargetNodeId} for ${behavior.id} before changing the remaining targets.`,
2017
+ );
2018
+ }
2019
+ if (touchedTargets.length === 0 && touchedEvidence.length === 0) {
2020
+ continue;
2021
+ }
2022
+ if (!representativeMutatedBehaviorIds.has(behavior.id)) continue;
2023
+ const attempt = behaviorVerificationAttempts.get(behavior.id);
2024
+ const repairForBehavior =
2025
+ isRepresentativeRepair
2026
+ && repairTargets.every((nodeId) =>
2027
+ representativeRepairBehaviorByNode.get(nodeId) === behavior.id);
2028
+ if (
2029
+ !repairForBehavior
2030
+ || Number(attempt?.repairs || 0)
2031
+ >= MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
2032
+ ) {
1874
2033
  throw toolError(
1875
2034
  'FRAMER_BEHAVIOR_REPRESENTATIVE_VERIFICATION_REQUIRED',
1876
- `Verify one representative implementation for ${pendingRepresentative.map((behavior) => behavior.id).join(', ')} before batching further mutations. A failed check may be repaired with SET changes limited to its evidence nodes.`,
2035
+ `Verify representative target ${decision.representativeTargetNodeId} for ${behavior.id} before changing it again or batching the remaining targets.`,
1877
2036
  );
1878
2037
  }
2038
+ behaviorVerificationAttempts.set(behavior.id, {
2039
+ ...attempt,
2040
+ repairs: Number(attempt?.repairs || 0) + 1,
2041
+ });
1879
2042
  }
2043
+ return pendingRepresentative
2044
+ .filter(({ decision }) =>
2045
+ rawChanges.includes(decision.representativeTargetNodeId))
2046
+ .map(({ behavior }) => behavior.id);
1880
2047
  };
1881
2048
 
1882
2049
  async function invokeUnsafe(name, rawArguments = {}) {
@@ -2023,7 +2190,12 @@ export function createFramerAgentToolRuntime({
2023
2190
  const previousMechanisms = new Map(
2024
2191
  (taskPlan?.mechanisms || []).map((decision) => [
2025
2192
  decision.behaviorId,
2026
- decision.mechanism,
2193
+ JSON.stringify({
2194
+ mechanism: decision.mechanism,
2195
+ targetNodeIds: decision.targetNodeIds,
2196
+ representativeTargetNodeId:
2197
+ decision.representativeTargetNodeId,
2198
+ }),
2027
2199
  ]),
2028
2200
  );
2029
2201
  const behaviorPlan = mergeBehaviorPlans(taskPlan, nextPlan);
@@ -2041,8 +2213,16 @@ export function createFramerAgentToolRuntime({
2041
2213
  };
2042
2214
  for (const decision of taskPlan.mechanisms) {
2043
2215
  const prior = previousMechanisms.get(decision.behaviorId);
2044
- if (prior && prior !== decision.mechanism) {
2216
+ const current = JSON.stringify({
2217
+ mechanism: decision.mechanism,
2218
+ targetNodeIds: decision.targetNodeIds,
2219
+ representativeTargetNodeId:
2220
+ decision.representativeTargetNodeId,
2221
+ });
2222
+ if (prior && prior !== current) {
2045
2223
  representativeVerifiedBehaviorIds.delete(decision.behaviorId);
2224
+ representativeMutatedBehaviorIds.delete(decision.behaviorId);
2225
+ behaviorVerificationAttempts.delete(decision.behaviorId);
2046
2226
  }
2047
2227
  }
2048
2228
  result = {
@@ -2088,21 +2268,43 @@ export function createFramerAgentToolRuntime({
2088
2268
  rawArguments.checks,
2089
2269
  taskPlan,
2090
2270
  );
2271
+ const exhaustedBehaviorIds = uniqueStrings(
2272
+ checks
2273
+ .map((check) => check.behaviorId)
2274
+ .filter((behaviorId) =>
2275
+ behaviorVerificationAttempts.get(behaviorId)?.exhausted),
2276
+ );
2277
+ if (exhaustedBehaviorIds.length > 0) {
2278
+ throw toolError(
2279
+ 'FRAMER_INTERACTION_VERIFICATION_BUDGET_EXHAUSTED',
2280
+ `Interaction verification is exhausted for ${exhaustedBehaviorIds.join(', ')}. Stop retrying unchanged evidence and report the work as partially verified.`,
2281
+ );
2282
+ }
2091
2283
  const pagePath = normalizePagePath(rawArguments.pagePath);
2092
2284
  const requestedIds = evidenceNodeIds(checks);
2285
+ const componentIds = uniqueStrings(
2286
+ checks.map((check) => check.componentNodeId),
2287
+ );
2093
2288
  const liveResult = await execCode(
2094
2289
  [
2095
2290
  `const requestedIds = ${JSON.stringify(requestedIds)};`,
2096
- 'const nodes = await Promise.all(requestedIds.map(async (id) => ({',
2097
- ' id,',
2098
- ' node: await framer.agent.serialize({ id, depth: 8 },',
2099
- ` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null),`,
2100
- ' parentId: await framer.agent.getParentNode({ id },',
2101
- ` { pagePath: ${JSON.stringify(pagePath)} }).then((node) => node?.id ?? null).catch(() => null),`,
2102
- ' ancestorIds: await framer.agent.getAncestors({ id },',
2103
- ` { pagePath: ${JSON.stringify(pagePath)} }).then((nodes) => Array.isArray(nodes) ? nodes.map((node) => node?.id).filter(Boolean) : []).catch(() => []),`,
2104
- '})));',
2105
- 'console.log(JSON.stringify({ nodes }, null, 2));',
2291
+ `const componentIds = ${JSON.stringify(componentIds)};`,
2292
+ 'const [nodes, componentControls] = await Promise.all([',
2293
+ ' Promise.all(requestedIds.map(async (id) => ({',
2294
+ ' id,',
2295
+ ' node: await framer.agent.serialize({ id, depth: 8 },',
2296
+ ` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null),`,
2297
+ ' parentId: await framer.agent.getParentNode({ id },',
2298
+ ` { pagePath: ${JSON.stringify(pagePath)} }).then((node) => node?.id ?? null).catch(() => null),`,
2299
+ ' ancestorIds: await framer.agent.getAncestors({ id },',
2300
+ ` { pagePath: ${JSON.stringify(pagePath)} }).then((nodes) => Array.isArray(nodes) ? nodes.map((node) => node?.id).filter(Boolean) : []).catch(() => []),`,
2301
+ ' }))),',
2302
+ ' componentIds.length > 0',
2303
+ ' ? framer.agent.readComponentControls({ componentIds },',
2304
+ ` { pagePath: ${JSON.stringify(pagePath)} }).catch(() => null)`,
2305
+ ' : null,',
2306
+ ']);',
2307
+ 'console.log(JSON.stringify({ nodes, componentControls }, null, 2));',
2106
2308
  ].join('\n'),
2107
2309
  );
2108
2310
  const parsed = parseStructuredOutput(liveResult.stdout);
@@ -2127,52 +2329,115 @@ export function createFramerAgentToolRuntime({
2127
2329
  context: {
2128
2330
  lowLevelWriteCalls,
2129
2331
  authorizedInstanceContainerIds,
2332
+ componentControls: parsed?.componentControls || null,
2333
+ semanticVerifierMode,
2130
2334
  },
2131
2335
  });
2132
2336
  markInspection();
2133
2337
  interactionVerificationCalls += 1;
2134
- interactionVerification = {
2135
- ...verified,
2136
- verifiedAt: new Date().toISOString(),
2137
- };
2138
2338
  representativeRepairNodeIds.clear();
2139
- for (const check of verified.checks.filter((item) => !item.passed)) {
2140
- evidenceNodeIds([check]).forEach((nodeId) =>
2141
- representativeRepairNodeIds.add(nodeId));
2142
- }
2339
+ representativeRepairBehaviorByNode.clear();
2340
+ representativeEvidenceBehaviorByNode.clear();
2341
+ const checkedBehaviorIds = new Set(
2342
+ checks.map((check) => check.behaviorId),
2343
+ );
2143
2344
  for (const behaviorResult of verified.behaviors) {
2144
- const failedChecks = verified.checks.filter((check) =>
2145
- check.behaviorId === behaviorResult.behaviorId && !check.passed);
2146
- if (failedChecks.length === 0) {
2147
- repeatedBehaviorVerificationFailures.delete(
2148
- behaviorResult.behaviorId,
2149
- );
2345
+ if (!checkedBehaviorIds.has(behaviorResult.behaviorId)) continue;
2346
+ const behaviorChecks = verified.checks.filter((check) =>
2347
+ check.behaviorId === behaviorResult.behaviorId);
2348
+ for (const check of behaviorChecks) {
2349
+ evidenceNodeIds([check]).forEach((nodeId) =>
2350
+ representativeEvidenceBehaviorByNode.set(
2351
+ nodeId,
2352
+ behaviorResult.behaviorId,
2353
+ ));
2354
+ }
2355
+ const deterministicallyContradicted = behaviorChecks.some((check) =>
2356
+ check.deterministicVerdict === 'contradicted');
2357
+ const prior = behaviorVerificationAttempts.get(
2358
+ behaviorResult.behaviorId,
2359
+ ) || {
2360
+ attempts: 0,
2361
+ unchangedEvidenceCount: 0,
2362
+ repairs: 0,
2363
+ fingerprint: null,
2364
+ exhausted: false,
2365
+ };
2366
+ if (behaviorResult.verdict === 'verified') {
2367
+ behaviorVerificationAttempts.delete(behaviorResult.behaviorId);
2368
+ Object.assign(behaviorResult, {
2369
+ verificationAttempts: prior.attempts + 1,
2370
+ verificationAttemptsRemaining: Math.max(
2371
+ 0,
2372
+ MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR
2373
+ - prior.attempts
2374
+ - 1,
2375
+ ),
2376
+ repairAttempts: prior.repairs,
2377
+ budgetExhausted: false,
2378
+ });
2150
2379
  continue;
2151
2380
  }
2152
- const failureFingerprint = crypto
2381
+ const evidenceFingerprint = crypto
2153
2382
  .createHash('sha256')
2154
2383
  .update(JSON.stringify({
2155
2384
  mechanism: behaviorResult.mechanism,
2156
- failures: failedChecks
2157
- .flatMap((check) => check.failures)
2158
- .sort(),
2385
+ verdict: behaviorResult.verdict,
2386
+ contradictions: [...behaviorResult.contradictions].sort(),
2387
+ unknowns: [...behaviorResult.unknowns].sort(),
2388
+ checks: behaviorChecks.map((check) => ({
2389
+ verdict: check.verdict,
2390
+ nodeIds: evidenceNodeIds([check]).sort(),
2391
+ observations: check.observations,
2392
+ })),
2159
2393
  }))
2160
2394
  .digest('hex');
2161
- const prior = repeatedBehaviorVerificationFailures.get(
2162
- behaviorResult.behaviorId,
2163
- );
2164
- const count =
2165
- prior?.fingerprint === failureFingerprint
2166
- ? prior.count + 1
2395
+ const attempts = prior.attempts + 1;
2396
+ const unchangedEvidenceCount =
2397
+ prior.fingerprint === evidenceFingerprint
2398
+ ? prior.unchangedEvidenceCount + 1
2167
2399
  : 1;
2168
- repeatedBehaviorVerificationFailures.set(
2169
- behaviorResult.behaviorId,
2170
- { fingerprint: failureFingerprint, count },
2171
- );
2400
+ const exhausted =
2401
+ attempts >= MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR
2402
+ || unchangedEvidenceCount >= 2
2403
+ || (
2404
+ behaviorResult.verdict === 'contradicted'
2405
+ && prior.repairs >= MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
2406
+ );
2407
+ behaviorVerificationAttempts.set(behaviorResult.behaviorId, {
2408
+ attempts,
2409
+ unchangedEvidenceCount,
2410
+ repairs: prior.repairs,
2411
+ fingerprint: evidenceFingerprint,
2412
+ exhausted,
2413
+ });
2414
+ Object.assign(behaviorResult, {
2415
+ verificationAttempts: attempts,
2416
+ verificationAttemptsRemaining: Math.max(
2417
+ 0,
2418
+ MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR - attempts,
2419
+ ),
2420
+ repairAttempts: prior.repairs,
2421
+ budgetExhausted: exhausted,
2422
+ });
2423
+ for (const check of behaviorChecks) {
2424
+ check.repairAllowed =
2425
+ check.deterministicVerdict === 'contradicted'
2426
+ && prior.repairs < MAX_INTERACTION_REPAIRS_PER_BEHAVIOR
2427
+ && !exhausted;
2428
+ if (!check.repairAllowed) continue;
2429
+ evidenceNodeIds([check]).forEach((nodeId) => {
2430
+ representativeRepairNodeIds.add(nodeId);
2431
+ representativeRepairBehaviorByNode.set(
2432
+ nodeId,
2433
+ behaviorResult.behaviorId,
2434
+ );
2435
+ });
2436
+ }
2172
2437
  if (
2173
- count < 2
2438
+ !deterministicallyContradicted
2439
+ || !exhausted
2174
2440
  || !behaviorResult.mechanism
2175
- || behaviorResult.mechanism === 'native_effect'
2176
2441
  ) continue;
2177
2442
  const alreadyRejected = taskPlan.rejectedMechanisms.some((rejected) =>
2178
2443
  rejected.mechanism === behaviorResult.mechanism
@@ -2191,8 +2456,8 @@ export function createFramerAgentToolRuntime({
2191
2456
  behaviorSignature: behaviorResult.behaviorSignature,
2192
2457
  mechanism: behaviorResult.mechanism,
2193
2458
  reason:
2194
- `Repeated structural verification failed: ${failedChecks
2195
- .flatMap((check) => check.failures)
2459
+ `Repeated live evidence contradicted this mechanism: ${behaviorChecks
2460
+ .flatMap((check) => check.contradictions)
2196
2461
  .join('; ')
2197
2462
  .slice(0, 700)}`,
2198
2463
  source: 'verification_failure',
@@ -2200,6 +2465,15 @@ export function createFramerAgentToolRuntime({
2200
2465
  ],
2201
2466
  };
2202
2467
  }
2468
+ const budgetExhaustedBehaviorIds = [...behaviorVerificationAttempts]
2469
+ .filter(([, attempt]) => attempt.exhausted)
2470
+ .map(([behaviorId]) => behaviorId);
2471
+ interactionVerification = {
2472
+ ...verified,
2473
+ semanticVerifierMode,
2474
+ budgetExhaustedBehaviorIds,
2475
+ verifiedAt: new Date().toISOString(),
2476
+ };
2203
2477
  verified.representativeBehaviorIds.forEach((behaviorId) =>
2204
2478
  representativeVerifiedBehaviorIds.add(behaviorId));
2205
2479
  if (interactionVerification.completed) {
@@ -2271,7 +2545,8 @@ export function createFramerAgentToolRuntime({
2271
2545
  'Framer Agent changes are required.',
2272
2546
  );
2273
2547
  }
2274
- requireTaskPlanForMutation({ canvas: true, changes });
2548
+ const mutatedRepresentativeBehaviorIds =
2549
+ requireTaskPlanForMutation({ canvas: true, changes });
2275
2550
  const pagePath = normalizePagePath(rawArguments.pagePath);
2276
2551
  result = await execCode(
2277
2552
  [
@@ -2282,6 +2557,8 @@ export function createFramerAgentToolRuntime({
2282
2557
  mutationCalls += 1;
2283
2558
  applyChangesCalls += 1;
2284
2559
  markMutation();
2560
+ mutatedRepresentativeBehaviorIds.forEach((behaviorId) =>
2561
+ representativeMutatedBehaviorIds.add(behaviorId));
2285
2562
  const parsedResult = parseStructuredOutput(result.stdout);
2286
2563
  mutationReceipts.push({
2287
2564
  sequence: operationSequence,
@@ -2541,6 +2818,10 @@ export function createFramerAgentToolRuntime({
2541
2818
  lowLevelWriteDisabled,
2542
2819
  lowLevelWriteBlockedCalls,
2543
2820
  instructionFallbacks,
2821
+ interactionVerificationAttemptLimit:
2822
+ MAX_INTERACTION_VERIFICATION_ATTEMPTS_PER_BEHAVIOR,
2823
+ interactionRepairLimit:
2824
+ MAX_INTERACTION_REPAIRS_PER_BEHAVIOR,
2544
2825
  },
2545
2826
  branch: branchChange,
2546
2827
  lastInspectionAt,