@gakim-digital/dexter-bridge 0.13.9 → 0.13.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/agent.js +46 -25
- package/src/framerErrorFixes.js +76 -0
- package/src/framerTools.js +38 -9
- package/src/protocol.js +1 -0
- package/src/providers/codexAppServer.js +12 -2
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -98,6 +98,7 @@ export function harnessMcpProcessEnvironment(
|
|
|
98
98
|
const companionModelSessions = new Map();
|
|
99
99
|
const sharedProviderAdapters = new Map();
|
|
100
100
|
const MAX_COMPANION_MODEL_SESSIONS = 64;
|
|
101
|
+
const FRAMER_CONTINUATION_NOTE = 'The user sent a new message in this conversation. Your instructions and the Framer guidance from earlier in this session still apply, and so does what you learned: what you changed, and what Framer rejected and why. Do not repeat an approach Framer already rejected; use what its error told you. The project inventory and assignment below are current. The user may have changed the project since your last reads, so read layers again before you edit them.';
|
|
101
102
|
|
|
102
103
|
function enabledFlag(value, fallback = false) {
|
|
103
104
|
const raw = String(value ?? '')
|
|
@@ -2432,10 +2433,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2432
2433
|
const webflowOutcome = run?.outcome?.kind === 'webflow-site';
|
|
2433
2434
|
if (webflowOutcome) assertWebflowHarnessAgent(definition.id);
|
|
2434
2435
|
const designOutcome = framerAgentOutcome || webflowOutcome;
|
|
2435
|
-
//
|
|
2436
|
-
//
|
|
2437
|
-
//
|
|
2438
|
-
const sessionKey =
|
|
2436
|
+
// A Framer conversation keeps one provider session: Dexter resumes it on
|
|
2437
|
+
// each message, so the model keeps what it tried, what Framer rejected, and
|
|
2438
|
+
// why. Webflow messages still start a fresh session.
|
|
2439
|
+
const sessionKey = webflowOutcome
|
|
2439
2440
|
? run?.runId || companionSessionKey(run)
|
|
2440
2441
|
: companionSessionKey(run);
|
|
2441
2442
|
const infrastructureAbortController = new AbortController();
|
|
@@ -2657,7 +2658,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2657
2658
|
}
|
|
2658
2659
|
};
|
|
2659
2660
|
const controlMonitor = monitorControl();
|
|
2660
|
-
const rememberedSession =
|
|
2661
|
+
const rememberedSession = webflowOutcome
|
|
2661
2662
|
? null
|
|
2662
2663
|
: rememberedCompanionSession(sessionKey) ||
|
|
2663
2664
|
(run?.outcome?.session?.providerSessionId
|
|
@@ -2859,6 +2860,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2859
2860
|
const callStartedAt = Date.now();
|
|
2860
2861
|
const usageAccumulator = createCompanionUsageAccumulator(definition.id);
|
|
2861
2862
|
let providerAttempts = 0;
|
|
2863
|
+
let sessionResumed = false;
|
|
2862
2864
|
let result;
|
|
2863
2865
|
try {
|
|
2864
2866
|
const preflight = designOutcome
|
|
@@ -2882,32 +2884,42 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2882
2884
|
// them from the cache instead of writing them again.
|
|
2883
2885
|
let systemPromptFile = null;
|
|
2884
2886
|
let systemPromptBytes = 0;
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
) {
|
|
2887
|
+
// A resumed Framer session already holds the instructions and guidance,
|
|
2888
|
+
// so it gets only this message's task.
|
|
2889
|
+
let continuationPrompt = null;
|
|
2890
|
+
if (designOutcome) {
|
|
2890
2891
|
const { instructions, task } = webflowOutcome
|
|
2891
2892
|
? buildWebflowOutcomePrompt(executionOutcome, product, promptContext)
|
|
2892
2893
|
: buildFramerOutcomePrompt(executionOutcome, product, promptContext);
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2894
|
+
if (definition.id === 'claude-code' && !options.providerAdapter?.runOutcome) {
|
|
2895
|
+
const systemPrompt = `${(webflowOutcome ? claudeWebflowOutcomeSystemPrompt : claudeOutcomeSystemPrompt)(product)}\n\n${instructions}`;
|
|
2896
|
+
systemPromptFile = writeClaudeSystemPromptFile(systemPrompt);
|
|
2897
|
+
systemPromptBytes = Buffer.byteLength(systemPrompt, 'utf8');
|
|
2898
|
+
prompt = task;
|
|
2899
|
+
} else {
|
|
2900
|
+
prompt = buildOutcomePrompt(executionOutcome, product, promptContext);
|
|
2901
|
+
}
|
|
2902
|
+
if (framerAgentOutcome) continuationPrompt = `${FRAMER_CONTINUATION_NOTE}\n\n${task}`;
|
|
2897
2903
|
} else {
|
|
2898
2904
|
prompt = buildOutcomePrompt(executionOutcome, product, promptContext);
|
|
2899
2905
|
}
|
|
2900
2906
|
const unresolvedTargets = preflight?.unresolvedTargets || [];
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2907
|
+
const withRunNotes = (text) => {
|
|
2908
|
+
let result = text;
|
|
2909
|
+
if (unresolvedTargets.length > 0) {
|
|
2910
|
+
result = [
|
|
2911
|
+
result,
|
|
2912
|
+
'',
|
|
2913
|
+
`These selected layer IDs do not resolve to native nodes on the active page: ${unresolvedTargets.map((target) => target.id).join(', ')}. The user still means those layers. Locate them by name and position in the provided context and the live tree, treat the matching native nodes as the selection, and ask only if no layer clearly matches.`,
|
|
2914
|
+
].join('\n');
|
|
2915
|
+
}
|
|
2916
|
+
if (materializedNativeSkills) {
|
|
2917
|
+
result = claudeNativeSkillPrompt(result, materializedNativeSkills);
|
|
2918
|
+
}
|
|
2919
|
+
return result;
|
|
2920
|
+
};
|
|
2921
|
+
prompt = withRunNotes(prompt);
|
|
2922
|
+
if (continuationPrompt) continuationPrompt = withRunNotes(continuationPrompt);
|
|
2911
2923
|
harnessTools.configurePromptContext?.({
|
|
2912
2924
|
doctrineLoaded: Boolean(run?.doctrine),
|
|
2913
2925
|
doctrineVersion: run?.doctrineVersion,
|
|
@@ -2954,6 +2966,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2954
2966
|
turnPrompt,
|
|
2955
2967
|
resumeSessionIdOverride,
|
|
2956
2968
|
step = 0,
|
|
2969
|
+
continuation = null,
|
|
2957
2970
|
) => {
|
|
2958
2971
|
// User attachments ride on the first turn as native image input, exactly
|
|
2959
2972
|
// as if pasted into the agent; follow-up turns already have them in context.
|
|
@@ -2964,6 +2977,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2964
2977
|
sessionId: sessionKey,
|
|
2965
2978
|
resumeSessionId: resumeSessionIdOverride,
|
|
2966
2979
|
prompt: turnPrompt,
|
|
2980
|
+
...(continuation ? { continuationPrompt: continuation } : {}),
|
|
2967
2981
|
model: adapterInvocationModel(run, selectedModel, definition.id),
|
|
2968
2982
|
outputSchema: schema,
|
|
2969
2983
|
responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
@@ -2984,6 +2998,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2984
2998
|
onProgress: progress,
|
|
2985
2999
|
});
|
|
2986
3000
|
providerAttempts += providerAttemptCount(providerResult);
|
|
3001
|
+
if (step === 0) sessionResumed = providerResult?.sessionContinued === true;
|
|
2987
3002
|
return providerResult;
|
|
2988
3003
|
}
|
|
2989
3004
|
const runtime =
|
|
@@ -3001,7 +3016,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
3001
3016
|
let processResult;
|
|
3002
3017
|
try {
|
|
3003
3018
|
providerAttempts += 1;
|
|
3004
|
-
processResult = await callLocalJsonAgent(definition.id, turnPrompt, {
|
|
3019
|
+
processResult = await callLocalJsonAgent(definition.id, resumeSessionId && continuation ? continuation : turnPrompt, {
|
|
3005
3020
|
...options,
|
|
3006
3021
|
model: selectedModelId,
|
|
3007
3022
|
timeoutMs: outcomeInactivityTimeoutMs,
|
|
@@ -3024,8 +3039,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
3024
3039
|
...(mcpConfig ? { mcpConfig } : {}),
|
|
3025
3040
|
onProgress: progress,
|
|
3026
3041
|
});
|
|
3042
|
+
if (step === 0) sessionResumed = Boolean(resumeSessionId);
|
|
3027
3043
|
} catch (error) {
|
|
3028
3044
|
if (!resumeSessionId || !resumeFailure(error)) throw error;
|
|
3045
|
+
if (step === 0) sessionResumed = false;
|
|
3029
3046
|
providerAttempts += 1;
|
|
3030
3047
|
processResult = await callLocalJsonAgent(definition.id, turnPrompt, {
|
|
3031
3048
|
...options,
|
|
@@ -3060,6 +3077,8 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
3060
3077
|
const providerExecution = executeProviderOutcome(
|
|
3061
3078
|
prompt,
|
|
3062
3079
|
rememberedSession?.providerSessionId,
|
|
3080
|
+
0,
|
|
3081
|
+
continuationPrompt,
|
|
3063
3082
|
);
|
|
3064
3083
|
const infrastructureFailure = harnessTools
|
|
3065
3084
|
.waitForFatalInfrastructure()
|
|
@@ -3274,6 +3293,8 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
3274
3293
|
: normalized.status,
|
|
3275
3294
|
durationMs: Date.now() - callStartedAt,
|
|
3276
3295
|
providerAttemptCount: Math.max(1, providerAttempts),
|
|
3296
|
+
resumed: sessionResumed,
|
|
3297
|
+
resumeSessionAvailable: Boolean(rememberedSession),
|
|
3277
3298
|
...usageSnapshot.tokenUsage,
|
|
3278
3299
|
estimatedCostUSD: usageSnapshot.estimatedCostUSD,
|
|
3279
3300
|
},
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// How to work around properties Framer rejects. Framer's message says what is
|
|
2
|
+
// wrong; each fix says what to change instead, so the model does not retry the
|
|
3
|
+
// same write or give up. Add an entry when production runs show a rejection
|
|
4
|
+
// that keeps coming back, and test it in framerErrorFixes.test.js.
|
|
5
|
+
//
|
|
6
|
+
// The first matching fix wins for an error.
|
|
7
|
+
export const FRAMER_ERROR_FIXES = [
|
|
8
|
+
{
|
|
9
|
+
id: 'custom-cursor-needs-ancestor',
|
|
10
|
+
matches: (error) => /customCursor/i.test(error.property) && /ancestor to provide a custom cursor/i.test(error.message),
|
|
11
|
+
fix: 'Framer applies a custom cursor variant only to a layer whose ancestor provides the custom cursor component. Set the cursor component on an ancestor first, such as the breakpoint or the root of the shared component the layer sits in, then set the variant on the layer; or set it on a layer that already has a cursor ancestor. If the project has no cursor component, say so instead of retrying. Read guides/Cursors/part-1.md and its later parts with framer_instructions if you are unsure.',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'cursor-on-text-node',
|
|
15
|
+
matches: (error) => /^(customCursor|cursor)/i.test(error.property) && /virtual (rich-text|textBlock) node/i.test(error.message),
|
|
16
|
+
fix: 'A cursor cannot be set on a text node. Set it on the frame or link that contains the text.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'aspect-ratio-needs-sizing',
|
|
20
|
+
matches: (error) => error.property === 'aspectRatio' && /auto or fit-image sizing/i.test(error.message),
|
|
21
|
+
fix: 'aspectRatio applies only when the layer\'s width and height are not auto or fit-image. In the same patch, give the layer a fixed or fill width and height as the node-properties guidance describes, then set aspectRatio. For images in a CMS collection list, change the image layer inside the repeated item, not each copy. Take a screenshot afterwards to check the crop.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'text-style-preset-controls',
|
|
25
|
+
matches: (error) => /preset-controlled text properties|text style preset is set/i.test(error.message),
|
|
26
|
+
fix: 'This text uses a text style preset, which controls its font, size, weight, and spacing. Either change the preset itself, which changes every text that uses it, apply a different preset, or remove the preset from this layer before setting those properties. If changing a shared preset would affect text the user did not mention, say so.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'text-style-on-runs',
|
|
30
|
+
matches: (error) => /Expected text in the same patch or styling on inserted TextRun children/i.test(error.message)
|
|
31
|
+
|| /to virtual (rich-text|textBlock) node/i.test(error.message),
|
|
32
|
+
fix: 'Text styling belongs on the text runs, not on the text layer\'s frame. Set it in the same patch that sets the text, or style the TextRun children as the tree API guidance describes.',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'layout-template-controls',
|
|
36
|
+
matches: (error) => /layout-template-controlled properties/i.test(error.message),
|
|
37
|
+
fix: 'This page uses a layout template, which controls the breakpoint\'s layout and stack. Change the layout template, which affects every page that uses it, or change the content inside the page instead.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'unsupported-property',
|
|
41
|
+
matches: (error) => /is not a supported property/i.test(error.message),
|
|
42
|
+
fix: 'Framer has no property by that name. Find the right one in the preloaded node-properties guidance instead of guessing: for example, bold is a font weight on the text runs, and hover styles are variants or interactions.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'font-unavailable',
|
|
46
|
+
matches: (error) => /font/i.test(`${error.property} ${error.message}`)
|
|
47
|
+
&& /not (loaded|available|found|installed)|unknown font|unavailable/i.test(error.message),
|
|
48
|
+
fix: 'A font Framer rejected is not available to this project. Follow the preloaded project APIs guidance: resolve the family through api.fonts before setting it, or use a family the project already uses.',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function normalized(error) {
|
|
53
|
+
return {
|
|
54
|
+
property: String(error?.property || ''),
|
|
55
|
+
message: String(error?.message || ''),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function framerErrorFix(error) {
|
|
60
|
+
const value = normalized(error);
|
|
61
|
+
return FRAMER_ERROR_FIXES.find((entry) => entry.matches(value)) || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// One line per distinct fix, so a batch of the same rejection reads once.
|
|
65
|
+
export function framerErrorFixHint(errors = []) {
|
|
66
|
+
const fixes = [];
|
|
67
|
+
for (const error of errors) {
|
|
68
|
+
const entry = framerErrorFix(error);
|
|
69
|
+
if (entry && !fixes.includes(entry)) fixes.push(entry);
|
|
70
|
+
}
|
|
71
|
+
if (!fixes.length) return null;
|
|
72
|
+
return [
|
|
73
|
+
...fixes.map((entry) => entry.fix),
|
|
74
|
+
'Change what Framer\'s message asks for in your next framer_edit; do not send the same property again unchanged.',
|
|
75
|
+
].join(' ');
|
|
76
|
+
}
|
package/src/framerTools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import jsTokens from 'js-tokens';
|
|
3
|
+
import { framerErrorFixHint } from './framerErrorFixes.js';
|
|
3
4
|
import { createFramerMcpClient } from './framerMcpClient.js';
|
|
4
5
|
import { withUserMessages } from './userMessages.js';
|
|
5
6
|
import {
|
|
@@ -50,7 +51,6 @@ const MAX_RESULT_CHARS = 6_000;
|
|
|
50
51
|
// after this long is told to start building.
|
|
51
52
|
const FIRST_EDIT_NUDGE_MS = 60_000;
|
|
52
53
|
const FIRST_EDIT_NUDGE = 'If this request changes the canvas, nothing is on it yet and you know enough to start: make your first framer_edit now with a small first section, such as the navigation and the hero headline, then build the rest two sections per edit. Read more only when a specific detail blocks the next edit. If the user asked only for an answer or an analysis, keep to that.';
|
|
53
|
-
const FONT_ERROR_HINT = 'A font Framer rejected is not available to this project. Follow the preloaded project APIs guidance: resolve the family through api.fonts before setting it, or use a family the project already uses. Fix it in your next framer_edit, not at the end.';
|
|
54
54
|
const ACTIVE_PAGE_OUTLINE_DEPTH = 2;
|
|
55
55
|
// Framer returns the previous layout for about 50ms after an edit.
|
|
56
56
|
const LAYOUT_SETTLE_MS = 150;
|
|
@@ -254,6 +254,21 @@ function codeOutsideLiterals(code) {
|
|
|
254
254
|
return visible;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// An edit program as it can be kept in run logs: code, layer ids, and short
|
|
258
|
+
// values stay; longer strings, which are usually the site's copy, do not.
|
|
259
|
+
// Kept as lines so no single log string is long enough to be dropped.
|
|
260
|
+
const LOGGED_LITERAL_CHARS = 40;
|
|
261
|
+
export function loggedProgram(code) {
|
|
262
|
+
let text = '';
|
|
263
|
+
for (const token of jsTokens(String(code || ''))) {
|
|
264
|
+
const literal = LITERAL_TOKEN_TYPES.has(token.type);
|
|
265
|
+
text += literal && token.value.length > LOGGED_LITERAL_CHARS + 2
|
|
266
|
+
? `"[omitted ${token.value.length - 2} chars]"`
|
|
267
|
+
: token.value;
|
|
268
|
+
}
|
|
269
|
+
return text.split('\n').slice(0, 400).map((line) => line.slice(0, 1_000));
|
|
270
|
+
}
|
|
271
|
+
|
|
257
272
|
// Text blocks inside a layer have virtual ids such as "v:dD8FHq_BL:0"; they
|
|
258
273
|
// belong to the layer named in the middle.
|
|
259
274
|
function layerIdOf(id) {
|
|
@@ -385,10 +400,6 @@ export function fontFamiliesIn(value, found = new Set()) {
|
|
|
385
400
|
return [...found].slice(0, 20);
|
|
386
401
|
}
|
|
387
402
|
|
|
388
|
-
function isFontError(error) {
|
|
389
|
-
return /font/i.test(`${error?.property || ''} ${error?.message || ''}`);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
403
|
function boundedResult(value) {
|
|
393
404
|
if (value === undefined) return undefined;
|
|
394
405
|
let text;
|
|
@@ -1224,6 +1235,8 @@ export function createFramerAgentToolRuntime({
|
|
|
1224
1235
|
let lastMutationAt = 0;
|
|
1225
1236
|
let lastReadAfterMutationAt = 0;
|
|
1226
1237
|
let lastVisualAt = 0;
|
|
1238
|
+
// Tool calls whose result handed the model at least one image.
|
|
1239
|
+
let screenshotCalls = 0;
|
|
1227
1240
|
let firstToolAt = null;
|
|
1228
1241
|
let guidance = { content: '', inventory: '', loaded: false, files: [] };
|
|
1229
1242
|
const mutationReceipts = [];
|
|
@@ -1528,6 +1541,10 @@ export function createFramerAgentToolRuntime({
|
|
|
1528
1541
|
return {
|
|
1529
1542
|
visualRequired: required.has('visual'),
|
|
1530
1543
|
visualCompleted: required.has('visual') && mutationCalls > 0 && lastVisualAt >= lastMutationAt,
|
|
1544
|
+
// The model saw the result after its last change, whether or not its
|
|
1545
|
+
// plan declared visual verification.
|
|
1546
|
+
visualAfterLastChange: mutationCalls > 0 && lastVisualAt >= lastMutationAt,
|
|
1547
|
+
screenshotCalls,
|
|
1531
1548
|
completed: missingChecks.length === 0,
|
|
1532
1549
|
missingChecks,
|
|
1533
1550
|
};
|
|
@@ -1628,6 +1645,13 @@ export function createFramerAgentToolRuntime({
|
|
|
1628
1645
|
await snapshotUnchanged();
|
|
1629
1646
|
const result = await client.exec(wrapProgram(program, { allowContentDeletion }));
|
|
1630
1647
|
const applied = applyReceipt(result, screenshotNodeIds);
|
|
1648
|
+
// An edit Framer rejected, and the edit after it, keep their program
|
|
1649
|
+
// in the run log, so a later fix shows what worked instead.
|
|
1650
|
+
const logged = mutationReceipts.at(-1) === applied.receipt;
|
|
1651
|
+
const previousReceipt = mutationReceipts.at(logged ? -2 : -1);
|
|
1652
|
+
if (logged && (applied.receipt.propertyErrors.length || previousReceipt?.propertyErrors?.length)) {
|
|
1653
|
+
applied.receipt.program = loggedProgram(program);
|
|
1654
|
+
}
|
|
1631
1655
|
requirements = renameRequirementNodes(requirements, applied.receipt.renamedIds);
|
|
1632
1656
|
const refused = Array.isArray(applied.output?.result?.dexterRefused) ? applied.output.result.dexterRefused : [];
|
|
1633
1657
|
if (refused.length) {
|
|
@@ -1661,6 +1685,7 @@ export function createFramerAgentToolRuntime({
|
|
|
1661
1685
|
}
|
|
1662
1686
|
if (applied.images.length) lastVisualAt = Date.now();
|
|
1663
1687
|
const due = evidenceStillDue();
|
|
1688
|
+
const propertyErrorHint = framerErrorFixHint(applied.receipt.propertyErrors);
|
|
1664
1689
|
const inner = applied.output.result && typeof applied.output.result === 'object'
|
|
1665
1690
|
? applied.output.result
|
|
1666
1691
|
: {};
|
|
@@ -1694,7 +1719,7 @@ export function createFramerAgentToolRuntime({
|
|
|
1694
1719
|
}
|
|
1695
1720
|
: {}),
|
|
1696
1721
|
propertyErrors: applied.receipt.propertyErrors,
|
|
1697
|
-
...(
|
|
1722
|
+
...(propertyErrorHint ? { propertyErrorHint } : {}),
|
|
1698
1723
|
...(applied.receipt.linterWarnings.length ? { linterWarnings: applied.receipt.linterWarnings } : {}),
|
|
1699
1724
|
codeFiles: applied.receipt.codeFiles,
|
|
1700
1725
|
...(applied.receipt.branchChange ? { branchChange: applied.receipt.branchChange } : {}),
|
|
@@ -1811,6 +1836,7 @@ export function createFramerAgentToolRuntime({
|
|
|
1811
1836
|
const { modelImages, ...rest } = payload;
|
|
1812
1837
|
const ready = await modelReadyImages(modelImages);
|
|
1813
1838
|
payload = ready.length ? { ...rest, screenshotsAttached: ready.length, modelImages: ready } : rest;
|
|
1839
|
+
if (ready.length) screenshotCalls += 1;
|
|
1814
1840
|
}
|
|
1815
1841
|
reportActivity({
|
|
1816
1842
|
kind: 'tool_active',
|
|
@@ -1971,6 +1997,8 @@ export function createFramerAgentToolRuntime({
|
|
|
1971
1997
|
for (const error of propertyErrors) weighedPropertyErrors.add(propertyErrorKey(error));
|
|
1972
1998
|
if (propertyErrors.length) {
|
|
1973
1999
|
facts.push(`Framer rejected these properties, so they are not set: ${propertyErrors.map((error) => `${error.nodeId}.${error.property}: ${error.message}`).join('; ')}.`);
|
|
2000
|
+
const hint = framerErrorFixHint(propertyErrors);
|
|
2001
|
+
if (hint) facts.push(hint);
|
|
1974
2002
|
}
|
|
1975
2003
|
if (pendingRepair?.claimed?.length) {
|
|
1976
2004
|
facts.push(`Your changedNodeIds lists layers no edit touched: ${pendingRepair.claimed.join(', ')}.`);
|
|
@@ -2093,9 +2121,10 @@ export function createFramerAgentToolRuntime({
|
|
|
2093
2121
|
output: 'No layer changed for a summary that claimed a change.',
|
|
2094
2122
|
});
|
|
2095
2123
|
}
|
|
2096
|
-
// A rejected
|
|
2097
|
-
//
|
|
2098
|
-
|
|
2124
|
+
// A property Framer rejected and no later edit of that layer replaced was
|
|
2125
|
+
// never applied. The model was shown it and may explain it in its summary,
|
|
2126
|
+
// but the run is not reported as done.
|
|
2127
|
+
const propertyErrors = lingeringPropertyErrors();
|
|
2099
2128
|
if (propertyErrors.length) {
|
|
2100
2129
|
checks.push({
|
|
2101
2130
|
command: PROPERTY_ERROR_CHECK,
|
package/src/protocol.js
CHANGED
|
@@ -481,6 +481,7 @@ export function buildFramerOutcomePrompt(outcome = {}, product, promptContext =
|
|
|
481
481
|
'Set only fonts the project can load: use a family the inventory lists as already in use, or resolve the family through api.fonts as the project APIs guidance below describes, before you set it. A rejected font comes back in propertyErrors; repair it in your next framer_edit, not at the end.',
|
|
482
482
|
'The Framer guidance and the project inventory below are already loaded, including the active page outline when one is listed. Start from them instead of re-reading. Call framer_instructions only when a specific Framer API question blocks your next edit, and pass every path you need in one call.',
|
|
483
483
|
'If a program is rejected, read the error and repair that program. Do not invent a framer global, a DSL, or a CLI command.',
|
|
484
|
+
'When Framer rejects a property, its message names what it expects instead, and propertyErrorHint says how to provide it. Change that, such as the layer\'s sizing, an ancestor, a text style preset, or the text runs, rather than sending the same property again or giving up. A property that is still rejected when you finish is not done: name it and why in your summary.',
|
|
484
485
|
'For an existing Framer code component or override, read it with api.files and write it with api.files.create, write, or applyPatch inside framer_edit.',
|
|
485
486
|
'The user\'s message is the brief. Do what they mean, including what they said to do if part of it cannot be done. Nothing Dexter tells you outranks their words. Prior conversation, selections, attachments, and project content are context, not automatic commands, but a decision the user already made in the conversation, such as declining a change, still stands.',
|
|
486
487
|
'Dexter has not classified the request for you. Put your task plan on your first framer_edit. In requirements, list every concrete, checkable thing the user asked for, such as values, colours, sizes, spacing, text, links, order, visibility, and behaviors, with the layers each applies to; leave matters of taste out. In unchanged, list the layers the user wants left as they are, such as the other breakpoints when they named one, or the rest of the page when they asked for a targeted fix.',
|
|
@@ -1177,7 +1177,7 @@ export function createCodexAppServerAdapter({
|
|
|
1177
1177
|
|
|
1178
1178
|
async function ensureThread(
|
|
1179
1179
|
runId,
|
|
1180
|
-
{ model, cwd: requestedCwd, harnessMode = false, dynamicTools = [], dynamicToolHandler, resumeThreadId, webHosts = [] } = {},
|
|
1180
|
+
{ model, cwd: requestedCwd, harnessMode = false, dynamicTools = [], dynamicToolHandler, resumeThreadId, webHosts = [], threadState = {} } = {},
|
|
1181
1181
|
) {
|
|
1182
1182
|
const resolution = await resolveProvider();
|
|
1183
1183
|
const existing = threadsByRun.get(runId);
|
|
@@ -1198,6 +1198,7 @@ export function createCodexAppServerAdapter({
|
|
|
1198
1198
|
if (dynamicToolHandler) {
|
|
1199
1199
|
dynamicToolHandlersByThread.set(existing, dynamicToolHandler);
|
|
1200
1200
|
}
|
|
1201
|
+
threadState.continued = true;
|
|
1201
1202
|
return existing;
|
|
1202
1203
|
}
|
|
1203
1204
|
if (existing) {
|
|
@@ -1267,6 +1268,7 @@ export function createCodexAppServerAdapter({
|
|
|
1267
1268
|
} catch {
|
|
1268
1269
|
response = null;
|
|
1269
1270
|
}
|
|
1271
|
+
if (response) threadState.continued = true;
|
|
1270
1272
|
}
|
|
1271
1273
|
if (!response) {
|
|
1272
1274
|
response = await active.request(CODEX_APP_SERVER_METHODS.threadStart, params, {
|
|
@@ -1326,11 +1328,11 @@ export function createCodexAppServerAdapter({
|
|
|
1326
1328
|
cwd: requestedCwd,
|
|
1327
1329
|
harnessTools,
|
|
1328
1330
|
resumeSessionId,
|
|
1331
|
+
continuationPrompt,
|
|
1329
1332
|
imageFiles = [],
|
|
1330
1333
|
webHosts = [],
|
|
1331
1334
|
} = {}) {
|
|
1332
1335
|
const transportSchema = outputSchema ? codexStructuredOutputSchema(outputSchema, { responseContract }) : null;
|
|
1333
|
-
const transportPrompt = transportSchema ? codexStructuredOutputPrompt(prompt, responseContract) : prompt;
|
|
1334
1336
|
await resolveProvider();
|
|
1335
1337
|
await initialize();
|
|
1336
1338
|
const active = ensureClient();
|
|
@@ -1339,6 +1341,7 @@ export function createCodexAppServerAdapter({
|
|
|
1339
1341
|
const materializedNativeSkills = await prepareNativeSkills(threadKey, nativeSkills);
|
|
1340
1342
|
const designHarness =
|
|
1341
1343
|
harnessMode && Boolean(designHarnessPlatform(harnessTools?.definitions || []));
|
|
1344
|
+
const threadState = {};
|
|
1342
1345
|
const threadId = await ensureThread(threadKey, {
|
|
1343
1346
|
model,
|
|
1344
1347
|
cwd: requestedCwd,
|
|
@@ -1347,7 +1350,12 @@ export function createCodexAppServerAdapter({
|
|
|
1347
1350
|
dynamicToolHandler: harnessMode ? harnessTools?.invoke : undefined,
|
|
1348
1351
|
resumeThreadId: harnessMode ? resumeSessionId : undefined,
|
|
1349
1352
|
webHosts: harnessMode ? webHosts : [],
|
|
1353
|
+
threadState,
|
|
1350
1354
|
});
|
|
1355
|
+
// A thread that already holds the conversation gets only what is new;
|
|
1356
|
+
// a fresh thread needs the whole prompt.
|
|
1357
|
+
const turnPrompt = threadState.continued && continuationPrompt ? continuationPrompt : prompt;
|
|
1358
|
+
const transportPrompt = transportSchema ? codexStructuredOutputPrompt(turnPrompt, responseContract) : turnPrompt;
|
|
1351
1359
|
const priorSkillDigest = invokedSkillDigestsByThread.get(threadId);
|
|
1352
1360
|
const nativeSkillsReused =
|
|
1353
1361
|
Boolean(materializedNativeSkills) && priorSkillDigest === materializedNativeSkills.digest;
|
|
@@ -1575,6 +1583,7 @@ export function createCodexAppServerAdapter({
|
|
|
1575
1583
|
return {
|
|
1576
1584
|
...result,
|
|
1577
1585
|
providerAttemptCount,
|
|
1586
|
+
sessionContinued: Boolean(threadState.continued),
|
|
1578
1587
|
...(materializedNativeSkills
|
|
1579
1588
|
? {
|
|
1580
1589
|
nativeSkillLifecycle: nativeSkillLifecycle(materializedNativeSkills, 'skill-input', nativeSkillsReused),
|
|
@@ -1613,6 +1622,7 @@ export function createCodexAppServerAdapter({
|
|
|
1613
1622
|
return {
|
|
1614
1623
|
...result,
|
|
1615
1624
|
providerAttemptCount,
|
|
1625
|
+
sessionContinued: Boolean(threadState.continued),
|
|
1616
1626
|
retryReason: 'transport_correction',
|
|
1617
1627
|
...(materializedNativeSkills
|
|
1618
1628
|
? {
|