@gakim-digital/dexter-bridge 0.11.9 → 0.11.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/framerAgentTools.js +191 -118
- package/src/framerBehaviorContract.js +12 -0
- package/src/protocol.js +1 -1
package/package.json
CHANGED
package/src/framerAgentTools.js
CHANGED
|
@@ -23,6 +23,8 @@ const MAX_MODEL_RESULT_BYTES = 120 * 1024;
|
|
|
23
23
|
const MAX_SCREENSHOT_BYTES = 4 * 1024 * 1024;
|
|
24
24
|
const MAX_SCREENSHOTS_PER_CALL = 4;
|
|
25
25
|
const SCREENSHOT_FETCH_TIMEOUT_MS = 20_000;
|
|
26
|
+
const SESSION_SCREENSHOT_TIMEOUT_MS = 60_000;
|
|
27
|
+
const MAX_SCREENSHOT_FAILURES_REPORTED = 8;
|
|
26
28
|
const SCREENSHOT_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
|
|
27
29
|
const DEFAULT_COMMAND_TIMEOUT_MS = 5 * 60_000;
|
|
28
30
|
const SESSION_TIMEOUT_MS = 10 * 60_000;
|
|
@@ -155,6 +157,8 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
|
|
|
155
157
|
minItems: 1,
|
|
156
158
|
uniqueItems: true,
|
|
157
159
|
items: { type: 'string', enum: VERIFICATION_CHECKS },
|
|
160
|
+
description:
|
|
161
|
+
'The evidence you will produce before completion. Dexter enforces exactly this list and adds nothing to it. Include "visual" when the result must be judged by eye, such as layout or styling work or a build from a reference image, then inspect a screenshot after your final mutation.',
|
|
158
162
|
},
|
|
159
163
|
behaviors: {
|
|
160
164
|
type: 'array',
|
|
@@ -706,16 +710,9 @@ function normalizeTaskPlan(rawArguments, inheritedRejectedMechanisms = []) {
|
|
|
706
710
|
'Describe the understood task and declare at least one task domain.',
|
|
707
711
|
);
|
|
708
712
|
}
|
|
713
|
+
// The model's declared verification is authoritative; domains describe the
|
|
714
|
+
// work and never add checks on their own.
|
|
709
715
|
const verification = new Set(['structural', ...requestedVerification]);
|
|
710
|
-
if (
|
|
711
|
-
domains.some((domain) =>
|
|
712
|
-
['visual', 'interactions', 'responsive'].includes(domain))
|
|
713
|
-
) verification.add('visual');
|
|
714
|
-
if (domains.includes('links')) verification.add('links');
|
|
715
|
-
if (domains.includes('interactions')) verification.add('interactions');
|
|
716
|
-
if (domains.includes('responsive')) verification.add('responsive');
|
|
717
|
-
if (domains.includes('code')) verification.add('code');
|
|
718
|
-
if (domains.includes('data')) verification.add('data');
|
|
719
716
|
const behaviorPlan = normalizeBehaviorPlan(
|
|
720
717
|
rawArguments,
|
|
721
718
|
inheritedRejectedMechanisms,
|
|
@@ -906,13 +903,6 @@ function abortError() {
|
|
|
906
903
|
});
|
|
907
904
|
}
|
|
908
905
|
|
|
909
|
-
function screenshotQueryCount(rawArguments) {
|
|
910
|
-
return (Array.isArray(rawArguments?.queries) ? rawArguments.queries : [])
|
|
911
|
-
.filter((query) => query?.type === 'screenshot')
|
|
912
|
-
.slice(0, MAX_SCREENSHOTS_PER_CALL)
|
|
913
|
-
.length;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
906
|
function isFramerScreenshotUrl(value) {
|
|
917
907
|
try {
|
|
918
908
|
const url = new URL(String(value || ''));
|
|
@@ -924,32 +914,36 @@ function isFramerScreenshotUrl(value) {
|
|
|
924
914
|
}
|
|
925
915
|
}
|
|
926
916
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
const urls = (Array.isArray(parsed?.results) ? parsed.results : [])
|
|
931
|
-
.map((entry) => entry?.image_url)
|
|
932
|
-
.filter(isFramerScreenshotUrl)
|
|
917
|
+
function screenshotQueries(rawArguments) {
|
|
918
|
+
return (Array.isArray(rawArguments?.queries) ? rawArguments.queries : [])
|
|
919
|
+
.filter((query) => query?.type === 'screenshot')
|
|
933
920
|
.slice(0, MAX_SCREENSHOTS_PER_CALL);
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Screenshot results are URLs the model cannot open, so the bridge downloads
|
|
924
|
+
// them and hands the pixels to the model as image content. Failures carry a
|
|
925
|
+
// short reason so delivery problems are visible in run telemetry.
|
|
926
|
+
async function downloadScreenshotImage(url, fetchImpl) {
|
|
927
|
+
if (!isFramerScreenshotUrl(url)) return { error: 'untrusted_url' };
|
|
928
|
+
try {
|
|
929
|
+
const response = await fetchImpl(url, {
|
|
930
|
+
signal: AbortSignal.timeout(SCREENSHOT_FETCH_TIMEOUT_MS),
|
|
931
|
+
});
|
|
932
|
+
if (!response.ok) return { error: `http_${response.status}` };
|
|
933
|
+
const mimeType = String(response.headers.get('content-type') || '')
|
|
934
|
+
.split(';')[0]
|
|
935
|
+
.trim()
|
|
936
|
+
.toLowerCase();
|
|
937
|
+
if (!SCREENSHOT_MIME_TYPES.has(mimeType)) return { error: 'unsupported_type' };
|
|
938
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
939
|
+
if (!bytes.length) return { error: 'empty' };
|
|
940
|
+
if (bytes.length > MAX_SCREENSHOT_BYTES) return { error: 'too_large' };
|
|
941
|
+
return { image: { url, mimeType, data: bytes.toString('base64') } };
|
|
942
|
+
} catch (error) {
|
|
943
|
+
return {
|
|
944
|
+
error: String(error?.cause?.code || error?.name || 'fetch_failed').slice(0, 60),
|
|
945
|
+
};
|
|
951
946
|
}
|
|
952
|
-
return images;
|
|
953
947
|
}
|
|
954
948
|
|
|
955
949
|
function parseStructuredOutput(value) {
|
|
@@ -1239,8 +1233,12 @@ export function createFramerAgentToolRuntime({
|
|
|
1239
1233
|
const representativeEvidenceBehaviorByNode = new Map();
|
|
1240
1234
|
const behaviorVerificationAttempts = new Map();
|
|
1241
1235
|
const mutationReceipts = [];
|
|
1242
|
-
let screenshotBeforeMutation = false;
|
|
1243
1236
|
let lastScreenshotSequence = 0;
|
|
1237
|
+
let lastScreenshotAttemptSequence = 0;
|
|
1238
|
+
let screenshotsViaDownload = 0;
|
|
1239
|
+
let screenshotsViaSession = 0;
|
|
1240
|
+
let screenshotsUndelivered = 0;
|
|
1241
|
+
const screenshotFailures = [];
|
|
1244
1242
|
let taskPlan = null;
|
|
1245
1243
|
const verificationEvidence = new Map();
|
|
1246
1244
|
let guidanceMetadata = {
|
|
@@ -1664,6 +1662,78 @@ export function createFramerAgentToolRuntime({
|
|
|
1664
1662
|
}
|
|
1665
1663
|
};
|
|
1666
1664
|
|
|
1665
|
+
// Captures a node through the Framer session itself. This needs no access
|
|
1666
|
+
// to the screenshot CDN, so it works where that download is blocked.
|
|
1667
|
+
const captureSessionScreenshot = async (nodeId) => {
|
|
1668
|
+
for (const scale of [1, 0.5]) {
|
|
1669
|
+
try {
|
|
1670
|
+
const result = await execCode(
|
|
1671
|
+
[
|
|
1672
|
+
`const shot = await framer.screenshot(${JSON.stringify(nodeId)}, { format: 'jpeg', quality: 80, scale: ${scale} });`,
|
|
1673
|
+
'console.log(JSON.stringify({ mimeType: shot.mimeType, data: Buffer.from(shot.data).toString("base64") }));',
|
|
1674
|
+
].join('\n'),
|
|
1675
|
+
{ timeoutMs: SESSION_SCREENSHOT_TIMEOUT_MS },
|
|
1676
|
+
);
|
|
1677
|
+
const parsed = parseStructuredOutput(result.stdout);
|
|
1678
|
+
const mimeType = String(parsed?.mimeType || '').toLowerCase();
|
|
1679
|
+
const data = typeof parsed?.data === 'string' ? parsed.data : '';
|
|
1680
|
+
if (!SCREENSHOT_MIME_TYPES.has(mimeType)) return { error: 'unsupported_type' };
|
|
1681
|
+
const bytes = Buffer.from(data, 'base64').length;
|
|
1682
|
+
if (!bytes) return { error: 'empty' };
|
|
1683
|
+
if (bytes > MAX_SCREENSHOT_BYTES) return { error: 'too_large' };
|
|
1684
|
+
return { image: { url: null, mimeType, data } };
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
if (error?.code === 'FRAMER_AGENT_OUTPUT_TOO_LARGE' && scale === 1) continue;
|
|
1687
|
+
return { error: String(error?.code || 'capture_failed').slice(0, 60) };
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return { error: 'too_large' };
|
|
1691
|
+
};
|
|
1692
|
+
|
|
1693
|
+
const deliverScreenshots = async (queries, parsed) => {
|
|
1694
|
+
const results = (Array.isArray(parsed?.results) ? parsed.results : [])
|
|
1695
|
+
.filter((entry) => typeof entry?.image_url === 'string');
|
|
1696
|
+
const images = [];
|
|
1697
|
+
const failures = [];
|
|
1698
|
+
let viaDownload = 0;
|
|
1699
|
+
let viaSession = 0;
|
|
1700
|
+
let undelivered = 0;
|
|
1701
|
+
for (const [index, query] of queries.entries()) {
|
|
1702
|
+
const nodeId = typeof query.id === 'string' ? query.id.trim() : '';
|
|
1703
|
+
const entry =
|
|
1704
|
+
(nodeId && results.find((candidate) => candidate.id === nodeId))
|
|
1705
|
+
|| results[index];
|
|
1706
|
+
const downloaded = entry
|
|
1707
|
+
? await downloadScreenshotImage(entry.image_url, fetchImpl)
|
|
1708
|
+
: { error: 'no_image_url' };
|
|
1709
|
+
if (downloaded.image) {
|
|
1710
|
+
images.push(downloaded.image);
|
|
1711
|
+
viaDownload += 1;
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
const captured = nodeId
|
|
1715
|
+
? await captureSessionScreenshot(nodeId)
|
|
1716
|
+
: { error: 'no_node_id' };
|
|
1717
|
+
if (captured.image) {
|
|
1718
|
+
images.push(captured.image);
|
|
1719
|
+
viaSession += 1;
|
|
1720
|
+
failures.push({
|
|
1721
|
+
target: nodeId,
|
|
1722
|
+
download: downloaded.error,
|
|
1723
|
+
recoveredVia: 'session',
|
|
1724
|
+
});
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
undelivered += 1;
|
|
1728
|
+
failures.push({
|
|
1729
|
+
target: nodeId || 'url',
|
|
1730
|
+
download: downloaded.error,
|
|
1731
|
+
capture: captured.error,
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
return { images, viaDownload, viaSession, undelivered, failures };
|
|
1735
|
+
};
|
|
1736
|
+
|
|
1667
1737
|
const referenceAssetFiles = Array.isArray(assignment?.context?.referenceAssetFiles)
|
|
1668
1738
|
? assignment.context.referenceAssetFiles
|
|
1669
1739
|
: [];
|
|
@@ -1764,16 +1834,20 @@ export function createFramerAgentToolRuntime({
|
|
|
1764
1834
|
};
|
|
1765
1835
|
};
|
|
1766
1836
|
|
|
1767
|
-
const markInspection = ({
|
|
1837
|
+
const markInspection = ({
|
|
1838
|
+
visual = false,
|
|
1839
|
+
visualAttempted = false,
|
|
1840
|
+
verifies = [],
|
|
1841
|
+
} = {}) => {
|
|
1768
1842
|
operationSequence += 1;
|
|
1769
1843
|
lastInspectionSequence = operationSequence;
|
|
1770
1844
|
lastInspectionAt = new Date().toISOString();
|
|
1771
1845
|
verificationEvidence.set('structural', operationSequence);
|
|
1846
|
+
if (visualAttempted) lastScreenshotAttemptSequence = operationSequence;
|
|
1772
1847
|
if (visual) {
|
|
1773
1848
|
screenshotCalls += 1;
|
|
1774
1849
|
lastScreenshotSequence = operationSequence;
|
|
1775
1850
|
verificationEvidence.set('visual', operationSequence);
|
|
1776
|
-
if (lastMutationSequence === 0) screenshotBeforeMutation = true;
|
|
1777
1851
|
}
|
|
1778
1852
|
for (const check of normalizeVerificationValues(
|
|
1779
1853
|
verifies,
|
|
@@ -1824,34 +1898,30 @@ export function createFramerAgentToolRuntime({
|
|
|
1824
1898
|
const verificationCheckCompleted = (check) => {
|
|
1825
1899
|
if (mutationCalls === 0) return true;
|
|
1826
1900
|
if (check === 'structural') return synchronized();
|
|
1827
|
-
if (check === 'visual') {
|
|
1828
|
-
return (
|
|
1829
|
-
screenshotBeforeMutation
|
|
1830
|
-
&& lastScreenshotSequence > lastMutationSequence
|
|
1831
|
-
);
|
|
1832
|
-
}
|
|
1833
1901
|
return Number(verificationEvidence.get(check) || 0) > lastMutationSequence;
|
|
1834
1902
|
};
|
|
1835
1903
|
const visualVerificationRequired = () =>
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
);
|
|
1904
|
+
requiredVerificationChecks().includes('visual');
|
|
1905
|
+
// The model asked for the final canvas, but no screenshot could be handed to
|
|
1906
|
+
// it. That is reported as unverified rather than blamed on the model.
|
|
1907
|
+
const visualVerificationUnavailable = () =>
|
|
1908
|
+
mutationCalls > 0
|
|
1909
|
+
&& lastScreenshotAttemptSequence > lastMutationSequence
|
|
1910
|
+
&& lastScreenshotSequence <= lastMutationSequence;
|
|
1911
|
+
const visualVerificationStatus = () => {
|
|
1912
|
+
if (!visualVerificationRequired() || mutationCalls === 0) return 'not_required';
|
|
1913
|
+
if (verificationCheckCompleted('visual')) return 'seen';
|
|
1914
|
+
return visualVerificationUnavailable() ? 'unavailable' : 'missing';
|
|
1915
|
+
};
|
|
1916
|
+
const requiredChecks = () => [
|
|
1917
|
+
...new Set(['structural', ...requiredVerificationChecks()]),
|
|
1918
|
+
];
|
|
1845
1919
|
const missingVerificationChecks = () => {
|
|
1846
1920
|
if (mutationCalls === 0) return [];
|
|
1847
1921
|
const missing = [];
|
|
1848
1922
|
if (!taskPlan) missing.push('task-plan');
|
|
1849
|
-
const
|
|
1850
|
-
'
|
|
1851
|
-
...requiredVerificationChecks(),
|
|
1852
|
-
...(applyChangesCalls > 0 ? ['visual'] : []),
|
|
1853
|
-
]);
|
|
1854
|
-
for (const check of required) {
|
|
1923
|
+
for (const check of requiredChecks()) {
|
|
1924
|
+
if (check === 'visual' && visualVerificationUnavailable()) continue;
|
|
1855
1925
|
if (!verificationCheckCompleted(check)) missing.push(check);
|
|
1856
1926
|
}
|
|
1857
1927
|
return missing;
|
|
@@ -1862,8 +1932,18 @@ export function createFramerAgentToolRuntime({
|
|
|
1862
1932
|
completed: missingVerificationChecks().length === 0,
|
|
1863
1933
|
structuralCompleted: synchronized(),
|
|
1864
1934
|
visualRequired: visualVerificationRequired() && mutationCalls > 0,
|
|
1865
|
-
visualCompleted:
|
|
1935
|
+
visualCompleted: visualVerificationStatus() !== 'missing',
|
|
1936
|
+
visualStatus: visualVerificationStatus(),
|
|
1866
1937
|
screenshotCalls,
|
|
1938
|
+
// Every failed CDN download is listed; recoveredVia marks the ones the
|
|
1939
|
+
// session capture still delivered to the model.
|
|
1940
|
+
screenshotDelivery: {
|
|
1941
|
+
delivered: screenshotsViaDownload + screenshotsViaSession,
|
|
1942
|
+
viaDownload: screenshotsViaDownload,
|
|
1943
|
+
viaSession: screenshotsViaSession,
|
|
1944
|
+
undelivered: screenshotsUndelivered,
|
|
1945
|
+
failures: screenshotFailures.map((failure) => ({ ...failure })),
|
|
1946
|
+
},
|
|
1867
1947
|
...(requiredVerificationChecks().includes('interactions')
|
|
1868
1948
|
|| interactionVerificationCalls > 0
|
|
1869
1949
|
? {
|
|
@@ -1907,20 +1987,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1907
1987
|
}
|
|
1908
1988
|
: {}),
|
|
1909
1989
|
taskPlan: taskPlanSnapshot(),
|
|
1910
|
-
requiredChecks:
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
...(applyChangesCalls > 0 ? ['visual'] : []),
|
|
1915
|
-
]),
|
|
1916
|
-
],
|
|
1917
|
-
completedChecks: [
|
|
1918
|
-
...new Set([
|
|
1919
|
-
'structural',
|
|
1920
|
-
...requiredVerificationChecks(),
|
|
1921
|
-
...(applyChangesCalls > 0 ? ['visual'] : []),
|
|
1922
|
-
]),
|
|
1923
|
-
].filter(verificationCheckCompleted),
|
|
1990
|
+
requiredChecks: requiredChecks(),
|
|
1991
|
+
completedChecks: requiredChecks().filter(verificationCheckCompleted),
|
|
1992
|
+
unavailableChecks:
|
|
1993
|
+
visualVerificationStatus() === 'unavailable' ? ['visual'] : [],
|
|
1924
1994
|
missingChecks: missingVerificationChecks(),
|
|
1925
1995
|
lastMutationAt,
|
|
1926
1996
|
lastInspectionAt,
|
|
@@ -1942,18 +2012,31 @@ export function createFramerAgentToolRuntime({
|
|
|
1942
2012
|
};
|
|
1943
2013
|
};
|
|
1944
2014
|
|
|
2015
|
+
const withUnavailableVisualCheck = (completion) =>
|
|
2016
|
+
visualVerificationStatus() === 'unavailable'
|
|
2017
|
+
? {
|
|
2018
|
+
...completion,
|
|
2019
|
+
checks: [
|
|
2020
|
+
...(Array.isArray(completion?.checks) ? completion.checks : []),
|
|
2021
|
+
{
|
|
2022
|
+
command: 'Visual verification',
|
|
2023
|
+
status: 'skipped',
|
|
2024
|
+
output:
|
|
2025
|
+
'Screenshots of the final canvas could not be delivered to the model, so the visual result is unverified.',
|
|
2026
|
+
},
|
|
2027
|
+
],
|
|
2028
|
+
}
|
|
2029
|
+
: completion;
|
|
2030
|
+
|
|
1945
2031
|
const finalizeCompletion = (completion) => {
|
|
1946
|
-
if (
|
|
1947
|
-
completion?.status !== 'completed'
|
|
1948
|
-
|| mutationCalls === 0
|
|
1949
|
-
|| verification().completed
|
|
1950
|
-
) {
|
|
2032
|
+
if (completion?.status !== 'completed' || mutationCalls === 0) {
|
|
1951
2033
|
return completion;
|
|
1952
2034
|
}
|
|
2035
|
+
if (verification().completed) return withUnavailableVisualCheck(completion);
|
|
1953
2036
|
const missing = missingVerificationChecks().map((check) => {
|
|
1954
2037
|
if (check === 'task-plan') return 'a harness task and verification plan';
|
|
1955
2038
|
if (check === 'structural') return 'a final structural read';
|
|
1956
|
-
if (check === 'visual') return '
|
|
2039
|
+
if (check === 'visual') return 'post-mutation screenshot verification';
|
|
1957
2040
|
return `post-mutation ${check} verification`;
|
|
1958
2041
|
});
|
|
1959
2042
|
const interactionDetails = interactionVerification.behaviors
|
|
@@ -1971,14 +2054,15 @@ export function createFramerAgentToolRuntime({
|
|
|
1971
2054
|
? [`Interaction evidence: ${interactionDetails.join('; ')}.`]
|
|
1972
2055
|
: []),
|
|
1973
2056
|
].join(' ');
|
|
2057
|
+
const failed = withUnavailableVisualCheck(completion);
|
|
1974
2058
|
return {
|
|
1975
|
-
...
|
|
2059
|
+
...failed,
|
|
1976
2060
|
status: 'failed',
|
|
1977
2061
|
summary:
|
|
1978
2062
|
String(completion?.summary || '').trim()
|
|
1979
2063
|
|| 'The requested changes were applied.',
|
|
1980
2064
|
checks: [
|
|
1981
|
-
...(Array.isArray(
|
|
2065
|
+
...(Array.isArray(failed?.checks) ? failed.checks : []),
|
|
1982
2066
|
{
|
|
1983
2067
|
command: 'Harness-owned final verification',
|
|
1984
2068
|
status: 'failed',
|
|
@@ -2002,9 +2086,7 @@ export function createFramerAgentToolRuntime({
|
|
|
2002
2086
|
missing.push('perform a focused live read after the latest mutation');
|
|
2003
2087
|
} else if (check === 'visual') {
|
|
2004
2088
|
missing.push(
|
|
2005
|
-
|
|
2006
|
-
? 'capture and inspect the affected canvas after the latest mutation'
|
|
2007
|
-
: 'report that the required baseline screenshot was missed; do not claim visual verification',
|
|
2089
|
+
'capture and inspect a screenshot of the affected canvas after the latest mutation',
|
|
2008
2090
|
);
|
|
2009
2091
|
} else {
|
|
2010
2092
|
const interactionState = interactionVerification.behaviors
|
|
@@ -2084,10 +2166,7 @@ export function createFramerAgentToolRuntime({
|
|
|
2084
2166
|
return uniqueStrings(commands.map((command) => command.nodeId));
|
|
2085
2167
|
};
|
|
2086
2168
|
|
|
2087
|
-
const requireTaskPlanForMutation = ({
|
|
2088
|
-
canvas = false,
|
|
2089
|
-
changes = '',
|
|
2090
|
-
} = {}) => {
|
|
2169
|
+
const requireTaskPlanForMutation = ({ changes = '' } = {}) => {
|
|
2091
2170
|
if (!taskPlan) {
|
|
2092
2171
|
throw toolError(
|
|
2093
2172
|
'FRAMER_HARNESS_PLAN_REQUIRED',
|
|
@@ -2096,15 +2175,6 @@ export function createFramerAgentToolRuntime({
|
|
|
2096
2175
|
}
|
|
2097
2176
|
const repairTargets = representativeRepairTargets(changes);
|
|
2098
2177
|
const isRepresentativeRepair = repairTargets.length > 0;
|
|
2099
|
-
if (
|
|
2100
|
-
(canvas || taskPlan.verification.includes('visual'))
|
|
2101
|
-
&& !screenshotBeforeMutation
|
|
2102
|
-
) {
|
|
2103
|
-
throw toolError(
|
|
2104
|
-
'FRAMER_HARNESS_BASELINE_SCREENSHOT_REQUIRED',
|
|
2105
|
-
'Capture and inspect a screenshot of the affected canvas before the first visual mutation.',
|
|
2106
|
-
);
|
|
2107
|
-
}
|
|
2108
2178
|
const rejectedSelectedMechanisms = taskPlan.behaviors.filter((behavior) => {
|
|
2109
2179
|
const selected = taskPlan.mechanisms.find((decision) =>
|
|
2110
2180
|
decision.behaviorId === behavior.id);
|
|
@@ -2319,16 +2389,6 @@ export function createFramerAgentToolRuntime({
|
|
|
2319
2389
|
rawArguments,
|
|
2320
2390
|
inheritedRejectedMechanisms,
|
|
2321
2391
|
);
|
|
2322
|
-
if (
|
|
2323
|
-
mutationCalls > 0
|
|
2324
|
-
&& nextPlan.verification.includes('visual')
|
|
2325
|
-
&& !screenshotBeforeMutation
|
|
2326
|
-
) {
|
|
2327
|
-
throw toolError(
|
|
2328
|
-
'FRAMER_HARNESS_BASELINE_SCREENSHOT_REQUIRED',
|
|
2329
|
-
'Visual verification cannot be added after mutations when no baseline screenshot was captured.',
|
|
2330
|
-
);
|
|
2331
|
-
}
|
|
2332
2392
|
const previousMechanisms = new Map(
|
|
2333
2393
|
(taskPlan?.mechanisms || []).map((decision) => [
|
|
2334
2394
|
decision.behaviorId,
|
|
@@ -2644,12 +2704,25 @@ export function createFramerAgentToolRuntime({
|
|
|
2644
2704
|
'console.log(JSON.stringify(result, null, 2));',
|
|
2645
2705
|
].join('\n'),
|
|
2646
2706
|
);
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2707
|
+
const requestedScreenshots = screenshotQueries(rawArguments);
|
|
2708
|
+
const delivery = requestedScreenshots.length
|
|
2709
|
+
? await deliverScreenshots(
|
|
2710
|
+
requestedScreenshots,
|
|
2711
|
+
parseStructuredOutput(result.stdout),
|
|
2712
|
+
)
|
|
2713
|
+
: { images: [], viaDownload: 0, viaSession: 0, undelivered: 0, failures: [] };
|
|
2714
|
+
modelImages = delivery.images;
|
|
2715
|
+
screenshotsViaDownload += delivery.viaDownload;
|
|
2716
|
+
screenshotsViaSession += delivery.viaSession;
|
|
2717
|
+
screenshotsUndelivered += delivery.undelivered;
|
|
2718
|
+
screenshotFailures.push(...delivery.failures);
|
|
2719
|
+
screenshotFailures.splice(
|
|
2720
|
+
0,
|
|
2721
|
+
Math.max(0, screenshotFailures.length - MAX_SCREENSHOT_FAILURES_REPORTED),
|
|
2650
2722
|
);
|
|
2651
2723
|
markInspection({
|
|
2652
2724
|
visual: modelImages.length > 0,
|
|
2725
|
+
visualAttempted: requestedScreenshots.length > 0,
|
|
2653
2726
|
verifies: rawArguments.verifies,
|
|
2654
2727
|
});
|
|
2655
2728
|
} else if (name === 'framer_query_images') {
|
|
@@ -2694,7 +2767,7 @@ export function createFramerAgentToolRuntime({
|
|
|
2694
2767
|
);
|
|
2695
2768
|
}
|
|
2696
2769
|
const mutatedRepresentativeBehaviorIds =
|
|
2697
|
-
requireTaskPlanForMutation({
|
|
2770
|
+
requireTaskPlanForMutation({ changes });
|
|
2698
2771
|
const pagePath = normalizePagePath(rawArguments.pagePath);
|
|
2699
2772
|
result = await execCode(
|
|
2700
2773
|
[
|
|
@@ -2770,7 +2843,7 @@ export function createFramerAgentToolRuntime({
|
|
|
2770
2843
|
});
|
|
2771
2844
|
const normalized = normalizedResult(result);
|
|
2772
2845
|
if (name !== 'framer_read_project') return normalized;
|
|
2773
|
-
const screenshotCount =
|
|
2846
|
+
const screenshotCount = screenshotQueries(rawArguments).length;
|
|
2774
2847
|
return {
|
|
2775
2848
|
...normalized,
|
|
2776
2849
|
...(modelImages.length ? { modelImages } : {}),
|
|
@@ -1455,6 +1455,18 @@ export function summarizeFramerChanges(changes) {
|
|
|
1455
1455
|
sourceNodeId: variant[2],
|
|
1456
1456
|
};
|
|
1457
1457
|
}
|
|
1458
|
+
const removed = command.match(/^DEL\s+([^\s;]+)/i);
|
|
1459
|
+
if (removed) return { operation: 'delete', nodeId: removed[1] };
|
|
1460
|
+
const duplicated = command.match(/^DUPE\s+([^\s]+).*?\bnewId="([^"]+)"/i);
|
|
1461
|
+
if (duplicated) {
|
|
1462
|
+
return {
|
|
1463
|
+
operation: 'duplicate',
|
|
1464
|
+
nodeId: duplicated[2],
|
|
1465
|
+
sourceNodeId: duplicated[1],
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
const moved = command.match(/^MOVE\s+([^\s]+)/i);
|
|
1469
|
+
if (moved) return { operation: 'move', nodeId: moved[1] };
|
|
1458
1470
|
const operation = text(command.split(/\s+/)[0], 40).toLowerCase();
|
|
1459
1471
|
return { operation: operation || 'unknown' };
|
|
1460
1472
|
});
|
package/src/protocol.js
CHANGED
|
@@ -488,7 +488,7 @@ export function buildOutcomePrompt(outcome = {}, product, promptContext = {}) {
|
|
|
488
488
|
'Do not publish or deploy unless the assignment explicitly says publishing is authorized.',
|
|
489
489
|
'You own verification. Your framer_plan_task declaration determines the required evidence, and the harness enforces it before accepting completion.',
|
|
490
490
|
'framer_read_project screenshots are returned to you as images; judge the result from what you see in them. If a result carries screenshotWarning, you did not see that screenshot, so do not describe it or claim it as visual verification.',
|
|
491
|
-
'
|
|
491
|
+
'You decide which evidence the task needs. Declare visual verification in framer_plan_task when the result must be judged by eye, such as layout or styling work or a build from a reference image, and then inspect a screenshot after your final mutation. When building from a reference attachment, compare that final screenshot against the reference. A screenshot before editing is optional; take one when you need to preserve or compare the current look. Use verifies on final reads to establish link, responsive, code, or data checks when your plan requires them.',
|
|
492
492
|
'A generic read cannot verify interactions. framer_verify_interactions derives success requirements from the behavior contract and chosen mechanism; supply canonical evidence node IDs rather than defining your own success test. It returns verified, contradicted, or unknown. Unknown means the available representation could not prove the behavior; it is not a defect and must not trigger mutation. A grounded semanticAssessment may resolve unknown evidence when enabled, but it cannot override a deterministic contradiction or authorize writes. Native links are verified from href, URL, route, destination, or component-control evidence. Native effects are verified from reflected hoverEffect, tapEffect, or appearEffect properties and are structural evidence, not runtime playback.',
|
|
493
493
|
'For repeated behavior, declare every targetNodeId and one representativeTargetNodeId in the mechanism plan. Implement and verify only that representative before fan-out, then verify complete target coverage after the final mutation. Only a contradicted result permits one evidence-scoped repair. Repeated unchanged evidence or exhausted verification budget must end as partial rather than starting another loop.',
|
|
494
494
|
'Do not reuse a mechanism recorded as rejected by the user or repeated contradictory live evidence. Unknown evidence never rejects a mechanism.',
|