@gakim-digital/dexter-bridge 0.11.0 → 0.11.2
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 +30 -17
- package/package.json +1 -1
- package/src/agent.js +265 -44
- package/src/api.js +34 -0
- package/src/config.js +5 -0
- package/src/framerAgentTools.js +1643 -192
- package/src/framerBehaviorContract.js +962 -0
- package/src/harnessMcpServer.js +91 -189
- package/src/logger.js +1 -0
- package/src/protocol.js +63 -14
- package/src/providers/codexAppServer.js +14 -6
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ progress, cancellation, and result interface.
|
|
|
39
39
|
The Framer harness receives only these project-scoped tools:
|
|
40
40
|
|
|
41
41
|
- `framer_instructions` and `framer_context`
|
|
42
|
-
- `framer_read_project` and `framer_apply_changes`
|
|
42
|
+
- `framer_read_project`, `framer_verify_interactions`, and `framer_apply_changes`
|
|
43
43
|
- restricted `framer_read` and `framer_write`
|
|
44
44
|
- `progress_update`
|
|
45
45
|
|
|
@@ -47,21 +47,34 @@ Native shell, filesystem, web, and unrelated-project access are disabled for
|
|
|
47
47
|
Framer outcome runs. Publishing is denied unless a future assignment explicitly
|
|
48
48
|
authorizes it.
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
Bridges advertise `framer-interaction-verification-v1`,
|
|
51
|
+
`framer-behavior-contract-v1`, and `framer-mechanism-verification-v1` only
|
|
52
|
+
when interaction work is declared as observable behavior, checked against the
|
|
53
|
+
selected Framer mechanism before mutation, and verified from live canonical
|
|
54
|
+
Framer nodes afterward. Repeated behavior is verified on one representative
|
|
55
|
+
target before the remaining targets are changed.
|
|
56
|
+
|
|
57
|
+
Project connection is handled inside Dexter and completes before the model
|
|
58
|
+
starts:
|
|
59
|
+
|
|
60
|
+
- The plugin reads the exact active project or branch URL directly from Framer.
|
|
61
|
+
- The user adds a Framer project API key once in Dexter. The API verifies it
|
|
62
|
+
through Framer's documented `connect(projectId, apiKey)` plus
|
|
63
|
+
`getProjectInfo()` flow, then encrypts and stores it only after that succeeds.
|
|
64
|
+
- The active hosted runtime redeems that verified credential only for the
|
|
65
|
+
matching run and device, then installs it with Framer’s official
|
|
66
|
+
`project auth` command against the authoritative 20-character project ID.
|
|
67
|
+
- The runtime opens an official Framer Agent session, reads the active branch
|
|
68
|
+
back from Framer, and confirms the authoritative project ID again.
|
|
69
|
+
- Rejected credentials are revoked. Replacing a key creates a new credential
|
|
70
|
+
version, so a cached runtime session cannot reuse the replacement.
|
|
71
|
+
- Hosted worker images bundle the Framer skills and seed them into each
|
|
72
|
+
runtime's private writable home before processing runs.
|
|
60
73
|
|
|
61
74
|
The user never runs `@framer/agent setup`, `project auth`, or `session new`.
|
|
62
75
|
The raw project credential is scoped to the matching active run and paired
|
|
63
76
|
device, is excluded from run payloads and logs, and is never sent to the Framer
|
|
64
|
-
plugin.
|
|
77
|
+
plugin after the one-time authenticated submission.
|
|
65
78
|
|
|
66
79
|
The bridge reports provider token usage on terminal events, including
|
|
67
80
|
interrupted and cancelled runs. Claude-reported
|
|
@@ -291,10 +304,10 @@ Recommended release env:
|
|
|
291
304
|
|
|
292
305
|
```bash
|
|
293
306
|
FRAMER_COMPANION_DOWNLOAD_URL=https://instawebai.com/dexter-bridge
|
|
294
|
-
NEXT_PUBLIC_DEXTER_BRIDGE_VERSION=0.11.
|
|
307
|
+
NEXT_PUBLIC_DEXTER_BRIDGE_VERSION=0.11.1
|
|
295
308
|
NEXT_PUBLIC_DEXTER_BRIDGE_DOWNLOAD_BASE_URL=https://instawebai.com/downloads/dexter-bridge
|
|
296
|
-
NEXT_PUBLIC_DEXTER_BRIDGE_MAC_ARM64_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.
|
|
297
|
-
NEXT_PUBLIC_DEXTER_BRIDGE_MAC_X64_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.
|
|
298
|
-
NEXT_PUBLIC_DEXTER_BRIDGE_MAC_ZIP_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.
|
|
299
|
-
NEXT_PUBLIC_DEXTER_BRIDGE_WIN_X64_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.
|
|
309
|
+
NEXT_PUBLIC_DEXTER_BRIDGE_MAC_ARM64_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.1-mac-arm64.dmg
|
|
310
|
+
NEXT_PUBLIC_DEXTER_BRIDGE_MAC_X64_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.1-mac-x64.dmg
|
|
311
|
+
NEXT_PUBLIC_DEXTER_BRIDGE_MAC_ZIP_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.1-mac-universal.zip
|
|
312
|
+
NEXT_PUBLIC_DEXTER_BRIDGE_WIN_X64_URL=https://instawebai.com/downloads/dexter-bridge/Dexter-Bridge-0.11.1-win-x64.exe
|
|
300
313
|
```
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -4,7 +4,11 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import crossSpawn from 'cross-spawn';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getFramerProjectAuthorization,
|
|
9
|
+
postRunEvent,
|
|
10
|
+
reportFramerProjectAuthorizationVerification,
|
|
11
|
+
} from './api.js';
|
|
8
12
|
import {
|
|
9
13
|
createCompanionUsageAccumulator,
|
|
10
14
|
createClaudeProgressParser,
|
|
@@ -60,15 +64,15 @@ const HARNESS_MCP_SERVER_PATH = fileURLToPath(
|
|
|
60
64
|
export function harnessMcpProcessEnvironment(
|
|
61
65
|
gateway,
|
|
62
66
|
versions = process.versions,
|
|
63
|
-
|
|
67
|
+
toolDefinitions,
|
|
64
68
|
) {
|
|
65
69
|
return {
|
|
66
70
|
INSTAWEB_HARNESS_GATEWAY_URL: gateway.url,
|
|
67
71
|
INSTAWEB_HARNESS_GATEWAY_TOKEN: gateway.token,
|
|
68
|
-
...(Array.isArray(
|
|
72
|
+
...(Array.isArray(toolDefinitions)
|
|
69
73
|
? {
|
|
70
|
-
|
|
71
|
-
JSON.stringify(
|
|
74
|
+
INSTAWEB_HARNESS_TOOL_DEFINITIONS:
|
|
75
|
+
JSON.stringify(toolDefinitions),
|
|
72
76
|
}
|
|
73
77
|
: {}),
|
|
74
78
|
...(versions?.electron ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
|
|
@@ -92,8 +96,8 @@ function deltaModelSessionsEnabled(env = process.env) {
|
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
function companionSessionKey(run) {
|
|
95
|
-
return run?.outcome?.session?.
|
|
96
|
-
|| run?.outcome?.session?.
|
|
99
|
+
return run?.outcome?.session?.sessionId
|
|
100
|
+
|| run?.outcome?.session?.goalSessionId
|
|
97
101
|
|| run?.modelTurn?.session?.goalSessionId
|
|
98
102
|
|| run?.modelTurn?.session?.sessionId
|
|
99
103
|
|| run?.turnId
|
|
@@ -318,7 +322,8 @@ function claudeOutcomeSystemPrompt(product) {
|
|
|
318
322
|
'Inspect the project, perform the requested work, verify the result, and return the structured outcome.',
|
|
319
323
|
'Do not access local credentials, unrelated files, other projects, account settings, or billing.',
|
|
320
324
|
'Do not publish unless the assignment explicitly authorizes publishing.',
|
|
321
|
-
'Call progress_update before the first inspection or edit and at meaningful phase changes.',
|
|
325
|
+
'Call progress_update before the first inspection or edit and at meaningful phase changes. Use one or two natural first-person sentences that name the specific page area, component, or behavior and the concrete change you are about to make or are making now; optionally say what you will check next. Avoid generic updates such as "working on the next step", "thinking", "loading", or "making changes".',
|
|
326
|
+
'If framer_write is rejected by the safety boundary, do not retry JavaScript variants. Switch to framer_apply_changes, inspect the relevant state, or report the concrete blocker.',
|
|
322
327
|
`Return only the structured outcome result requested by ${name}.`,
|
|
323
328
|
].join(' ');
|
|
324
329
|
}
|
|
@@ -1317,6 +1322,13 @@ function adapterInvocationModel(run, selectedModel, agent) {
|
|
|
1317
1322
|
if (typeof runtimeInvocation === 'string' && runtimeInvocation.trim()) {
|
|
1318
1323
|
return runtimeInvocation.trim();
|
|
1319
1324
|
}
|
|
1325
|
+
if (
|
|
1326
|
+
run?.companion?.modelPolicy?.owner === 'harness'
|
|
1327
|
+
&& typeof selectedModel?.invocationName === 'string'
|
|
1328
|
+
&& selectedModel.invocationName.trim()
|
|
1329
|
+
) {
|
|
1330
|
+
return selectedModel.invocationName.trim();
|
|
1331
|
+
}
|
|
1320
1332
|
const explicit = run?.companion?.model?.invocationName;
|
|
1321
1333
|
if (typeof explicit === 'string' && explicit.trim()) return explicit.trim();
|
|
1322
1334
|
if (selectedModel?.invocationName) return selectedModel.invocationName;
|
|
@@ -1326,12 +1338,63 @@ function adapterInvocationModel(run, selectedModel, agent) {
|
|
|
1326
1338
|
return raw.startsWith(prefix) ? raw.slice(prefix.length) : raw || undefined;
|
|
1327
1339
|
}
|
|
1328
1340
|
|
|
1341
|
+
function harnessOutcomeModel(run, definition) {
|
|
1342
|
+
const requestedModel =
|
|
1343
|
+
run?.runtimeProfile?.model?.id
|
|
1344
|
+
|| run?.companion?.model?.id
|
|
1345
|
+
|| run?.model;
|
|
1346
|
+
const requested =
|
|
1347
|
+
run?.runtimeProfile?.model
|
|
1348
|
+
|| companionModelDefinition(requestedModel, definition.id);
|
|
1349
|
+
if (run?.companion?.modelPolicy?.owner !== 'harness') return requested;
|
|
1350
|
+
const candidates = (
|
|
1351
|
+
Array.isArray(run?.companion?.models)
|
|
1352
|
+
? run.companion.models
|
|
1353
|
+
: []
|
|
1354
|
+
).filter((model) =>
|
|
1355
|
+
model
|
|
1356
|
+
&& typeof model === 'object'
|
|
1357
|
+
&& String(model.agent || definition.id) === definition.id);
|
|
1358
|
+
if (!candidates.length) return requested;
|
|
1359
|
+
const mode = String(run.companion.modelPolicy.mode || 'auto');
|
|
1360
|
+
if (mode === 'advanced') {
|
|
1361
|
+
return candidates.find((model) => model.id === requested.id) || requested;
|
|
1362
|
+
}
|
|
1363
|
+
if (mode === 'auto') {
|
|
1364
|
+
return (
|
|
1365
|
+
candidates.find((model) => model.isDefault === true)
|
|
1366
|
+
|| candidates.find((model) => model.id === requested.id)
|
|
1367
|
+
|| requested
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
const tier = mode === 'fast' ? 'fast' : 'director';
|
|
1371
|
+
const costRank = { '$': 1, '$$': 2, '$$$': 3 };
|
|
1372
|
+
return (
|
|
1373
|
+
candidates.find((model) => model.routingTier === tier)
|
|
1374
|
+
|| [...candidates].sort((left, right) => {
|
|
1375
|
+
const leftRank = costRank[left.costTier] || 2;
|
|
1376
|
+
const rightRank = costRank[right.costTier] || 2;
|
|
1377
|
+
return mode === 'fast'
|
|
1378
|
+
? leftRank - rightRank
|
|
1379
|
+
: rightRank - leftRank;
|
|
1380
|
+
})[0]
|
|
1381
|
+
|| requested
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1329
1385
|
function controlRequestsCancellation(response) {
|
|
1330
1386
|
return Boolean(
|
|
1331
1387
|
response?.control?.cancelRequested || response?.status === 'cancelled' || response?.status === 'expired',
|
|
1332
1388
|
);
|
|
1333
1389
|
}
|
|
1334
1390
|
|
|
1391
|
+
function controlCancellationSource(response) {
|
|
1392
|
+
const source = String(response?.control?.cancelSource || '').trim();
|
|
1393
|
+
if (source) return source;
|
|
1394
|
+
if (response?.status === 'expired') return 'deadline';
|
|
1395
|
+
return 'unknown';
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1335
1398
|
function boundedDurationMs(value, fallback, minimum = 10, maximum = 60 * 60 * 1000) {
|
|
1336
1399
|
const parsed = Number(value);
|
|
1337
1400
|
if (!Number.isFinite(parsed)) return fallback;
|
|
@@ -1357,11 +1420,31 @@ function waitForControl(delayMs, signal) {
|
|
|
1357
1420
|
}
|
|
1358
1421
|
|
|
1359
1422
|
class CompanionRunCancelledError extends Error {
|
|
1360
|
-
constructor(
|
|
1423
|
+
constructor(
|
|
1424
|
+
message = 'The local bridge model turn was cancelled.',
|
|
1425
|
+
cancellationSource = 'unknown',
|
|
1426
|
+
) {
|
|
1361
1427
|
super(message);
|
|
1362
1428
|
this.name = 'CompanionRunCancelledError';
|
|
1363
1429
|
this.code = 'RUN_CANCELLED';
|
|
1430
|
+
this.cancellationSource = cancellationSource;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function cancellationMessage(error) {
|
|
1435
|
+
const framer = error?.framer || error?.companionUsage?.framer;
|
|
1436
|
+
const mutationCalls = Number(framer?.mutationCalls) || 0;
|
|
1437
|
+
if (mutationCalls <= 0) {
|
|
1438
|
+
return 'Stopped before Dexter completed a Framer change.';
|
|
1364
1439
|
}
|
|
1440
|
+
const verificationCompleted = framer?.verification?.completed === true;
|
|
1441
|
+
return [
|
|
1442
|
+
`Stopped after ${mutationCalls} successful Framer change ${mutationCalls === 1 ? 'batch' : 'batches'}.`,
|
|
1443
|
+
'Those changes remain in Framer.',
|
|
1444
|
+
verificationCompleted
|
|
1445
|
+
? 'The harness verified the latest project state before the run stopped.'
|
|
1446
|
+
: 'The harness did not verify the final project state before the run stopped.',
|
|
1447
|
+
].join(' ');
|
|
1365
1448
|
}
|
|
1366
1449
|
|
|
1367
1450
|
async function callProviderAdapter(
|
|
@@ -2093,13 +2176,14 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2093
2176
|
|| run?.companion?.model?.id
|
|
2094
2177
|
|| run?.model
|
|
2095
2178
|
|| options.selectedModel;
|
|
2096
|
-
const selectedModel =
|
|
2097
|
-
run?.runtimeProfile?.model
|
|
2098
|
-
|| companionModelDefinition(requestedModel, definition.id);
|
|
2179
|
+
const selectedModel = harnessOutcomeModel(run, definition);
|
|
2099
2180
|
const selectedModelId =
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2181
|
+
selectedModel?.id
|
|
2182
|
+
|| (
|
|
2183
|
+
typeof requestedModel === 'string' && requestedModel.trim()
|
|
2184
|
+
? requestedModel.trim()
|
|
2185
|
+
: definition.model
|
|
2186
|
+
);
|
|
2103
2187
|
const outcomeMaxDurationMs = boundedDurationMs(
|
|
2104
2188
|
run?.outcome?.maxDurationMs
|
|
2105
2189
|
?? options.maxDurationMs
|
|
@@ -2120,6 +2204,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2120
2204
|
const controlMonitorAbort = new AbortController();
|
|
2121
2205
|
let finished = false;
|
|
2122
2206
|
let cancelRequested = false;
|
|
2207
|
+
let cancellationSource = 'unknown';
|
|
2123
2208
|
let cancelAction = null;
|
|
2124
2209
|
let rejectCancellation;
|
|
2125
2210
|
let latestTokenUsage = normalizeCompanionTokenUsage();
|
|
@@ -2128,7 +2213,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2128
2213
|
rejectCancellation = reject;
|
|
2129
2214
|
});
|
|
2130
2215
|
const cancellationError = () => {
|
|
2131
|
-
const error = new CompanionRunCancelledError(
|
|
2216
|
+
const error = new CompanionRunCancelledError(
|
|
2217
|
+
'The local bridge model turn was cancelled.',
|
|
2218
|
+
cancellationSource,
|
|
2219
|
+
);
|
|
2132
2220
|
error.companionUsage = {
|
|
2133
2221
|
tokenUsage: latestTokenUsage,
|
|
2134
2222
|
usageAvailable: latestTokenUsage.totalTokens > 0,
|
|
@@ -2137,7 +2225,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2137
2225
|
};
|
|
2138
2226
|
return error;
|
|
2139
2227
|
};
|
|
2140
|
-
const requestCancel = () => {
|
|
2228
|
+
const requestCancel = (source = 'unknown') => {
|
|
2229
|
+
if (cancellationSource === 'unknown' && source) {
|
|
2230
|
+
cancellationSource = source;
|
|
2231
|
+
}
|
|
2141
2232
|
if (cancelRequested) return cancelAction;
|
|
2142
2233
|
cancelRequested = true;
|
|
2143
2234
|
infrastructureAbortController.abort();
|
|
@@ -2153,7 +2244,9 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2153
2244
|
.then(async () => {
|
|
2154
2245
|
if (finished) return;
|
|
2155
2246
|
const response = await send('activity', payload);
|
|
2156
|
-
if (controlRequestsCancellation(response))
|
|
2247
|
+
if (controlRequestsCancellation(response)) {
|
|
2248
|
+
await requestCancel(controlCancellationSource(response));
|
|
2249
|
+
}
|
|
2157
2250
|
})
|
|
2158
2251
|
.catch((error) => {
|
|
2159
2252
|
if (!finished) {
|
|
@@ -2185,7 +2278,9 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2185
2278
|
Boolean(event.message || event.name) ||
|
|
2186
2279
|
event.kind === 'turn_started';
|
|
2187
2280
|
if (!meaningful) return;
|
|
2188
|
-
const userFacingUpdate =
|
|
2281
|
+
const userFacingUpdate =
|
|
2282
|
+
event.kind === 'status_update'
|
|
2283
|
+
&& event.source === 'harness';
|
|
2189
2284
|
void postActivity({
|
|
2190
2285
|
stage: 'outcome',
|
|
2191
2286
|
outcomeId: run?.outcome?.outcomeId,
|
|
@@ -2226,7 +2321,9 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2226
2321
|
occurredAt: new Date().toISOString(),
|
|
2227
2322
|
message: `${definition.label} is still working.`,
|
|
2228
2323
|
});
|
|
2229
|
-
if (controlRequestsCancellation(response))
|
|
2324
|
+
if (controlRequestsCancellation(response)) {
|
|
2325
|
+
await requestCancel(controlCancellationSource(response));
|
|
2326
|
+
}
|
|
2230
2327
|
} catch (error) {
|
|
2231
2328
|
if (!controlMonitorAbort.signal.aborted && !cancelRequested) {
|
|
2232
2329
|
options.trace?.warn?.('outcome_control_poll_failed', {
|
|
@@ -2291,7 +2388,29 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2291
2388
|
allowInsecureHttp: options.allowInsecureHttp,
|
|
2292
2389
|
})
|
|
2293
2390
|
: undefined,
|
|
2294
|
-
|
|
2391
|
+
verifyProjectAuthorization:
|
|
2392
|
+
options.apiBaseUrl && options.deviceToken
|
|
2393
|
+
? ({
|
|
2394
|
+
status,
|
|
2395
|
+
framerProjectId,
|
|
2396
|
+
framerProjectUrl,
|
|
2397
|
+
credentialVersion,
|
|
2398
|
+
errorCode,
|
|
2399
|
+
signal,
|
|
2400
|
+
}) =>
|
|
2401
|
+
reportFramerProjectAuthorizationVerification(options.apiBaseUrl, {
|
|
2402
|
+
deviceToken: options.deviceToken,
|
|
2403
|
+
runId: run.runId,
|
|
2404
|
+
status,
|
|
2405
|
+
framerProjectId,
|
|
2406
|
+
framerProjectUrl,
|
|
2407
|
+
credentialVersion,
|
|
2408
|
+
errorCode,
|
|
2409
|
+
fetchImpl: options.fetchImpl,
|
|
2410
|
+
signal,
|
|
2411
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
2412
|
+
})
|
|
2413
|
+
: undefined,
|
|
2295
2414
|
})
|
|
2296
2415
|
: createHarnessToolRuntime({
|
|
2297
2416
|
assignment: run?.outcome,
|
|
@@ -2300,10 +2419,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2300
2419
|
trace: options.trace,
|
|
2301
2420
|
onActivity: progress,
|
|
2302
2421
|
});
|
|
2303
|
-
let prompt =
|
|
2304
|
-
if (materializedNativeSkills) {
|
|
2305
|
-
prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
|
|
2306
|
-
}
|
|
2422
|
+
let prompt = '';
|
|
2307
2423
|
const schema = outcomeOutputSchema();
|
|
2308
2424
|
assertOutcomeResponseContract(
|
|
2309
2425
|
STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
@@ -2317,6 +2433,24 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2317
2433
|
if (framerAgentOutcome) {
|
|
2318
2434
|
await Promise.race([harnessTools.preflight(), cancellation]);
|
|
2319
2435
|
}
|
|
2436
|
+
const framerGuidance = framerAgentOutcome
|
|
2437
|
+
? await Promise.race([
|
|
2438
|
+
harnessTools.loadProjectGuidance(),
|
|
2439
|
+
cancellation,
|
|
2440
|
+
])
|
|
2441
|
+
: null;
|
|
2442
|
+
harnessTools.configurePromptContext?.({
|
|
2443
|
+
doctrineLoaded: Boolean(run?.doctrine),
|
|
2444
|
+
doctrineVersion: run?.doctrineVersion,
|
|
2445
|
+
});
|
|
2446
|
+
prompt = buildOutcomePrompt(run.outcome, product, {
|
|
2447
|
+
doctrine: run?.doctrine,
|
|
2448
|
+
doctrineVersion: run?.doctrineVersion,
|
|
2449
|
+
framerGuidance: framerGuidance?.content,
|
|
2450
|
+
});
|
|
2451
|
+
if (materializedNativeSkills) {
|
|
2452
|
+
prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
|
|
2453
|
+
}
|
|
2320
2454
|
const gateway =
|
|
2321
2455
|
definition.id === 'codex' ? null : await harnessTools.startGateway();
|
|
2322
2456
|
const mcpServer = gateway
|
|
@@ -2327,7 +2461,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2327
2461
|
environment: harnessMcpProcessEnvironment(
|
|
2328
2462
|
gateway,
|
|
2329
2463
|
process.versions,
|
|
2330
|
-
harnessTools.
|
|
2464
|
+
harnessTools.definitions,
|
|
2331
2465
|
),
|
|
2332
2466
|
}
|
|
2333
2467
|
: null;
|
|
@@ -2349,15 +2483,22 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2349
2483
|
});
|
|
2350
2484
|
if (controlRequestsCancellation(statusResponse)) {
|
|
2351
2485
|
await options.providerAdapter?.cancel?.(sessionKey);
|
|
2352
|
-
throw new CompanionRunCancelledError(
|
|
2486
|
+
throw new CompanionRunCancelledError(
|
|
2487
|
+
undefined,
|
|
2488
|
+
controlCancellationSource(statusResponse),
|
|
2489
|
+
);
|
|
2353
2490
|
}
|
|
2354
|
-
const
|
|
2491
|
+
const executeProviderOutcome = async (
|
|
2492
|
+
turnPrompt,
|
|
2493
|
+
resumeSessionIdOverride,
|
|
2494
|
+
step = 0,
|
|
2495
|
+
) => {
|
|
2355
2496
|
if (options.providerAdapter?.runOutcome) {
|
|
2356
2497
|
const providerResult = await options.providerAdapter.runOutcome({
|
|
2357
2498
|
runId: run.runId,
|
|
2358
2499
|
sessionId: sessionKey,
|
|
2359
|
-
resumeSessionId:
|
|
2360
|
-
prompt,
|
|
2500
|
+
resumeSessionId: resumeSessionIdOverride,
|
|
2501
|
+
prompt: turnPrompt,
|
|
2361
2502
|
model: adapterInvocationModel(run, selectedModel, definition.id),
|
|
2362
2503
|
outputSchema: schema,
|
|
2363
2504
|
responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
@@ -2387,12 +2528,12 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2387
2528
|
}
|
|
2388
2529
|
const resumeSessionId =
|
|
2389
2530
|
agentSessionResumeEnabled(definition, options.env)
|
|
2390
|
-
?
|
|
2531
|
+
? resumeSessionIdOverride
|
|
2391
2532
|
: undefined;
|
|
2392
2533
|
let processResult;
|
|
2393
2534
|
try {
|
|
2394
2535
|
providerAttempts += 1;
|
|
2395
|
-
processResult = await callLocalJsonAgent(definition.id,
|
|
2536
|
+
processResult = await callLocalJsonAgent(definition.id, turnPrompt, {
|
|
2396
2537
|
...options,
|
|
2397
2538
|
model: selectedModelId,
|
|
2398
2539
|
timeoutMs: outcomeInactivityTimeoutMs,
|
|
@@ -2401,7 +2542,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2401
2542
|
outputSchema: schema,
|
|
2402
2543
|
responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
2403
2544
|
product,
|
|
2404
|
-
step
|
|
2545
|
+
step,
|
|
2405
2546
|
maxDurationMs: outcomeMaxDurationMs,
|
|
2406
2547
|
cwd: materialized.directory,
|
|
2407
2548
|
harnessMode: true,
|
|
@@ -2415,7 +2556,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2415
2556
|
} catch (error) {
|
|
2416
2557
|
if (!resumeSessionId || !resumeFailure(error)) throw error;
|
|
2417
2558
|
providerAttempts += 1;
|
|
2418
|
-
processResult = await callLocalJsonAgent(definition.id,
|
|
2559
|
+
processResult = await callLocalJsonAgent(definition.id, turnPrompt, {
|
|
2419
2560
|
...options,
|
|
2420
2561
|
model: selectedModelId,
|
|
2421
2562
|
timeoutMs: outcomeInactivityTimeoutMs,
|
|
@@ -2423,7 +2564,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2423
2564
|
outputSchema: schema,
|
|
2424
2565
|
responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
2425
2566
|
product,
|
|
2426
|
-
step
|
|
2567
|
+
step,
|
|
2427
2568
|
maxDurationMs: outcomeMaxDurationMs,
|
|
2428
2569
|
cwd: materialized.directory,
|
|
2429
2570
|
harnessMode: true,
|
|
@@ -2441,7 +2582,11 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2441
2582
|
text: parsedOutput.resultText,
|
|
2442
2583
|
sessionId: parsedOutput.sessionId,
|
|
2443
2584
|
};
|
|
2444
|
-
}
|
|
2585
|
+
};
|
|
2586
|
+
const providerExecution = executeProviderOutcome(
|
|
2587
|
+
prompt,
|
|
2588
|
+
rememberedSession?.providerSessionId,
|
|
2589
|
+
);
|
|
2445
2590
|
const infrastructureFailure = harnessTools
|
|
2446
2591
|
.waitForFatalInfrastructure()
|
|
2447
2592
|
.then(async (error) => {
|
|
@@ -2466,7 +2611,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2466
2611
|
if (cancelRequested) throw cancellationError();
|
|
2467
2612
|
throw error;
|
|
2468
2613
|
}
|
|
2469
|
-
|
|
2614
|
+
let providerSessionId = result?.threadId || result?.sessionId;
|
|
2470
2615
|
if (providerSessionId || options.providerAdapter) {
|
|
2471
2616
|
rememberCompanionSession(sessionKey, {
|
|
2472
2617
|
providerSessionId:
|
|
@@ -2478,7 +2623,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2478
2623
|
});
|
|
2479
2624
|
}
|
|
2480
2625
|
usageAccumulator.add(result);
|
|
2481
|
-
|
|
2626
|
+
let rawCompletion = normalizeOutcomeCompletion(
|
|
2482
2627
|
extractJsonObject(result?.text || result?.resultText || ''),
|
|
2483
2628
|
run?.outcome?.outcomeId,
|
|
2484
2629
|
);
|
|
@@ -2486,11 +2631,68 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2486
2631
|
if (harnessTools.directWorkspace) {
|
|
2487
2632
|
synchronization = await harnessTools.inspect();
|
|
2488
2633
|
} else if (
|
|
2489
|
-
|
|
2634
|
+
rawCompletion.status === 'completed'
|
|
2490
2635
|
|| harnessTools.hasPendingChanges()
|
|
2491
2636
|
) {
|
|
2492
2637
|
synchronization = await harnessTools.synchronize();
|
|
2493
2638
|
}
|
|
2639
|
+
const repairPrompt =
|
|
2640
|
+
rawCompletion.status === 'completed'
|
|
2641
|
+
&& typeof harnessTools.verificationRepairPrompt === 'function'
|
|
2642
|
+
? harnessTools.verificationRepairPrompt()
|
|
2643
|
+
: null;
|
|
2644
|
+
if (
|
|
2645
|
+
repairPrompt
|
|
2646
|
+
&& Date.now() - callStartedAt < outcomeMaxDurationMs - 5_000
|
|
2647
|
+
) {
|
|
2648
|
+
progress({
|
|
2649
|
+
kind: 'status_update',
|
|
2650
|
+
source: 'harness',
|
|
2651
|
+
message:
|
|
2652
|
+
'I’m completing the final Framer verification before I finish.',
|
|
2653
|
+
});
|
|
2654
|
+
const repairResult = await Promise.race([
|
|
2655
|
+
executeProviderOutcome(
|
|
2656
|
+
repairPrompt,
|
|
2657
|
+
providerSessionId
|
|
2658
|
+
|| rememberedSession?.providerSessionId
|
|
2659
|
+
|| sessionKey,
|
|
2660
|
+
1,
|
|
2661
|
+
),
|
|
2662
|
+
infrastructureFailure,
|
|
2663
|
+
cancellation,
|
|
2664
|
+
]);
|
|
2665
|
+
usageAccumulator.add(repairResult);
|
|
2666
|
+
result = repairResult;
|
|
2667
|
+
providerSessionId =
|
|
2668
|
+
repairResult?.threadId
|
|
2669
|
+
|| repairResult?.sessionId
|
|
2670
|
+
|| providerSessionId;
|
|
2671
|
+
rawCompletion = normalizeOutcomeCompletion(
|
|
2672
|
+
extractJsonObject(
|
|
2673
|
+
repairResult?.text || repairResult?.resultText || '',
|
|
2674
|
+
),
|
|
2675
|
+
run?.outcome?.outcomeId,
|
|
2676
|
+
);
|
|
2677
|
+
synchronization = harnessTools.directWorkspace
|
|
2678
|
+
? await harnessTools.inspect()
|
|
2679
|
+
: await harnessTools.synchronize();
|
|
2680
|
+
await Promise.race([activityChain, cancellation]);
|
|
2681
|
+
}
|
|
2682
|
+
if (providerSessionId || options.providerAdapter) {
|
|
2683
|
+
rememberCompanionSession(sessionKey, {
|
|
2684
|
+
providerSessionId:
|
|
2685
|
+
providerSessionId
|
|
2686
|
+
|| rememberedSession?.providerSessionId
|
|
2687
|
+
|| sessionKey,
|
|
2688
|
+
agent: definition.id,
|
|
2689
|
+
updatedAt: Date.now(),
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
const completion =
|
|
2693
|
+
typeof harnessTools.finalizeCompletion === 'function'
|
|
2694
|
+
? harnessTools.finalizeCompletion(rawCompletion)
|
|
2695
|
+
: rawCompletion;
|
|
2494
2696
|
const files = harnessTools.directWorkspace
|
|
2495
2697
|
? []
|
|
2496
2698
|
: collectOutcomeWorkspaceChanges(run.outcome, materialized);
|
|
@@ -2524,7 +2726,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2524
2726
|
outcomeId: run?.outcome?.outcomeId,
|
|
2525
2727
|
model: selectedModelId,
|
|
2526
2728
|
status:
|
|
2527
|
-
normalized.status === '
|
|
2729
|
+
normalized.status === 'completed'
|
|
2528
2730
|
? 'succeeded'
|
|
2529
2731
|
: normalized.status,
|
|
2530
2732
|
durationMs: Date.now() - callStartedAt,
|
|
@@ -2534,6 +2736,22 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2534
2736
|
},
|
|
2535
2737
|
],
|
|
2536
2738
|
});
|
|
2739
|
+
} catch (error) {
|
|
2740
|
+
const failure =
|
|
2741
|
+
error instanceof Error
|
|
2742
|
+
? error
|
|
2743
|
+
: new Error(String(error || 'The Framer outcome failed.'));
|
|
2744
|
+
usageAccumulator.add(failure.companionUsage || {});
|
|
2745
|
+
const framer =
|
|
2746
|
+
typeof harnessTools.summary === 'function'
|
|
2747
|
+
? harnessTools.summary()
|
|
2748
|
+
: undefined;
|
|
2749
|
+
failure.framer = framer;
|
|
2750
|
+
failure.companionUsage = {
|
|
2751
|
+
...usageAccumulator.snapshot(),
|
|
2752
|
+
...(framer ? { framer } : {}),
|
|
2753
|
+
};
|
|
2754
|
+
throw failure;
|
|
2537
2755
|
} finally {
|
|
2538
2756
|
finished = true;
|
|
2539
2757
|
controlMonitorAbort.abort();
|
|
@@ -2563,7 +2781,6 @@ export async function executeRun(
|
|
|
2563
2781
|
inspectAgentAuthentication,
|
|
2564
2782
|
runtimeProfile: suppliedRuntimeProfile,
|
|
2565
2783
|
framerAgentRunCli,
|
|
2566
|
-
openAuthorizationUrl,
|
|
2567
2784
|
allowInsecureHttp = false,
|
|
2568
2785
|
} = {},
|
|
2569
2786
|
) {
|
|
@@ -2685,7 +2902,6 @@ export async function executeRun(
|
|
|
2685
2902
|
apiBaseUrl,
|
|
2686
2903
|
deviceToken,
|
|
2687
2904
|
fetchImpl,
|
|
2688
|
-
openAuthorizationUrl,
|
|
2689
2905
|
allowInsecureHttp,
|
|
2690
2906
|
};
|
|
2691
2907
|
if (outcomeRun) {
|
|
@@ -2699,12 +2915,17 @@ export async function executeRun(
|
|
|
2699
2915
|
return { cancelled: false };
|
|
2700
2916
|
} catch (error) {
|
|
2701
2917
|
if (error?.code === 'RUN_CANCELLED') {
|
|
2702
|
-
|
|
2918
|
+
const message = cancellationMessage(error);
|
|
2919
|
+
trace.info('run_cancelled', {
|
|
2920
|
+
message,
|
|
2921
|
+
cancellationSource: error?.cancellationSource,
|
|
2922
|
+
});
|
|
2703
2923
|
await send('error', {
|
|
2704
2924
|
operationType: 'cancelled',
|
|
2705
2925
|
outcome: 'cancelled',
|
|
2706
|
-
message
|
|
2926
|
+
message,
|
|
2707
2927
|
code: 'RUN_CANCELLED',
|
|
2928
|
+
cancellationSource: error?.cancellationSource || 'unknown',
|
|
2708
2929
|
...(error?.companionUsage || usageAccumulator.snapshot()),
|
|
2709
2930
|
}).catch(() => undefined);
|
|
2710
2931
|
return { cancelled: true };
|
package/src/api.js
CHANGED
|
@@ -200,6 +200,40 @@ export function getFramerProjectAuthorization(apiBaseUrl, {
|
|
|
200
200
|
);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
export function reportFramerProjectAuthorizationVerification(apiBaseUrl, {
|
|
204
|
+
deviceToken,
|
|
205
|
+
runId,
|
|
206
|
+
status,
|
|
207
|
+
framerProjectId,
|
|
208
|
+
framerProjectUrl,
|
|
209
|
+
credentialVersion,
|
|
210
|
+
errorCode,
|
|
211
|
+
fetchImpl,
|
|
212
|
+
signal,
|
|
213
|
+
allowInsecureHttp,
|
|
214
|
+
}) {
|
|
215
|
+
return requestJson(
|
|
216
|
+
apiBaseUrl,
|
|
217
|
+
`/ai/framer/companion/runs/${encodeURIComponent(runId)}/framer-authorization/verification`,
|
|
218
|
+
{
|
|
219
|
+
method: 'POST',
|
|
220
|
+
deviceToken,
|
|
221
|
+
fetchImpl,
|
|
222
|
+
signal,
|
|
223
|
+
allowInsecureHttp,
|
|
224
|
+
body: {
|
|
225
|
+
status,
|
|
226
|
+
framerProjectId,
|
|
227
|
+
...(framerProjectUrl ? { framerProjectUrl } : {}),
|
|
228
|
+
credentialVersion,
|
|
229
|
+
...(errorCode ? { errorCode } : {}),
|
|
230
|
+
platform: process.platform,
|
|
231
|
+
companionVersion: BRIDGE_VERSION,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
203
237
|
export function postRunEvent(apiBaseUrl, {
|
|
204
238
|
deviceToken,
|
|
205
239
|
runId,
|