@gakim-digital/dexter-bridge 0.5.21 → 0.11.1
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 +129 -35
- package/package.json +19 -5
- package/src/agent.js +1575 -334
- package/src/agentOutput.js +209 -0
- package/src/api.js +82 -1
- package/src/cli.js +267 -39
- package/src/config.js +35 -7
- package/src/framerAgentTools.js +2547 -0
- package/src/framerBehaviorContract.js +962 -0
- package/src/harnessMcpServer.js +240 -0
- package/src/harnessTools.js +548 -0
- package/src/logger.js +2 -1
- package/src/nativeSkills.js +295 -0
- package/src/outcomeWorkspace.js +351 -0
- package/src/protocol.js +288 -0
- package/src/providers/acp.js +241 -0
- package/src/providers/codexAppServer.js +1059 -157
- package/src/providers/codexStructuredOutput.js +243 -16
- package/src/providers/directByok.js +197 -0
- package/src/providers/index.js +33 -7
- package/src/providers/openCode.js +607 -0
- package/src/runtimeProfiles.js +284 -0
- package/src/providers/claudeAgentSdk.js +0 -507
package/src/agent.js
CHANGED
|
@@ -2,9 +2,19 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import crossSpawn from 'cross-spawn';
|
|
6
|
-
import {
|
|
7
|
-
|
|
7
|
+
import {
|
|
8
|
+
getFramerProjectAuthorization,
|
|
9
|
+
postRunEvent,
|
|
10
|
+
reportFramerProjectAuthorizationVerification,
|
|
11
|
+
} from './api.js';
|
|
12
|
+
import {
|
|
13
|
+
createCompanionUsageAccumulator,
|
|
14
|
+
createClaudeProgressParser,
|
|
15
|
+
normalizeCompanionTokenUsage,
|
|
16
|
+
parseAgentOutput,
|
|
17
|
+
} from './agentOutput.js';
|
|
8
18
|
import {
|
|
9
19
|
DEFAULT_BRIDGE_AGENT,
|
|
10
20
|
companionModelDefinition,
|
|
@@ -14,30 +24,69 @@ import {
|
|
|
14
24
|
normalizeCompanionModelName,
|
|
15
25
|
} from './config.js';
|
|
16
26
|
import {
|
|
27
|
+
assertOutcomeResponseContract,
|
|
28
|
+
buildOutcomePrompt,
|
|
17
29
|
buildModelTurnDeltaPrompt,
|
|
18
30
|
buildModelTurnFallbackPrompt,
|
|
19
31
|
buildModelTurnPrompt,
|
|
20
32
|
extractJsonObject,
|
|
21
33
|
modelTurnOutputSchema,
|
|
22
34
|
normalizeModelTurnCompletion,
|
|
35
|
+
normalizeOutcomeCompletion,
|
|
36
|
+
normalizeStructuredResponseContract,
|
|
37
|
+
outcomeOutputSchema,
|
|
23
38
|
runSummary,
|
|
39
|
+
STRUCTURED_RESPONSE_CONTRACTS,
|
|
24
40
|
} from './protocol.js';
|
|
41
|
+
import { clip, createRunLogger, errorMeta, logLocationHint, summarizePayload, summarizeToolResult } from './logger.js';
|
|
25
42
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
summarizeToolResult,
|
|
32
|
-
} from './logger.js';
|
|
43
|
+
claudeNativeSkillPrompt,
|
|
44
|
+
materializeNativeSkills,
|
|
45
|
+
nativeSkillLifecycle,
|
|
46
|
+
normalizeNativeSkills,
|
|
47
|
+
} from './nativeSkills.js';
|
|
33
48
|
import { createLocalAgentAdapter } from './providers/index.js';
|
|
49
|
+
import {
|
|
50
|
+
collectOutcomeWorkspaceChanges,
|
|
51
|
+
materializeOutcomeWorkspace,
|
|
52
|
+
removeOutcomeWorkspace,
|
|
53
|
+
} from './outcomeWorkspace.js';
|
|
54
|
+
import {
|
|
55
|
+
codexDynamicToolSpecs,
|
|
56
|
+
createHarnessToolRuntime,
|
|
57
|
+
} from './harnessTools.js';
|
|
58
|
+
import { createFramerAgentToolRuntime } from './framerAgentTools.js';
|
|
59
|
+
|
|
60
|
+
const HARNESS_MCP_SERVER_PATH = fileURLToPath(
|
|
61
|
+
new URL('./harnessMcpServer.js', import.meta.url),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export function harnessMcpProcessEnvironment(
|
|
65
|
+
gateway,
|
|
66
|
+
versions = process.versions,
|
|
67
|
+
allowedToolNames,
|
|
68
|
+
) {
|
|
69
|
+
return {
|
|
70
|
+
INSTAWEB_HARNESS_GATEWAY_URL: gateway.url,
|
|
71
|
+
INSTAWEB_HARNESS_GATEWAY_TOKEN: gateway.token,
|
|
72
|
+
...(Array.isArray(allowedToolNames)
|
|
73
|
+
? {
|
|
74
|
+
INSTAWEB_HARNESS_ALLOWED_TOOLS:
|
|
75
|
+
JSON.stringify(allowedToolNames),
|
|
76
|
+
}
|
|
77
|
+
: {}),
|
|
78
|
+
...(versions?.electron ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
34
81
|
|
|
35
82
|
const companionModelSessions = new Map();
|
|
36
83
|
const sharedProviderAdapters = new Map();
|
|
37
84
|
const MAX_COMPANION_MODEL_SESSIONS = 64;
|
|
38
85
|
|
|
39
86
|
function enabledFlag(value, fallback = false) {
|
|
40
|
-
const raw = String(value ?? '')
|
|
87
|
+
const raw = String(value ?? '')
|
|
88
|
+
.trim()
|
|
89
|
+
.toLowerCase();
|
|
41
90
|
if (!raw) return fallback;
|
|
42
91
|
return !['0', 'false', 'no', 'off', 'disabled'].includes(raw);
|
|
43
92
|
}
|
|
@@ -47,7 +96,9 @@ function deltaModelSessionsEnabled(env = process.env) {
|
|
|
47
96
|
}
|
|
48
97
|
|
|
49
98
|
function companionSessionKey(run) {
|
|
50
|
-
return run?.
|
|
99
|
+
return run?.outcome?.session?.sessionId
|
|
100
|
+
|| run?.outcome?.session?.goalSessionId
|
|
101
|
+
|| run?.modelTurn?.session?.goalSessionId
|
|
51
102
|
|| run?.modelTurn?.session?.sessionId
|
|
52
103
|
|| run?.turnId
|
|
53
104
|
|| run?.runId;
|
|
@@ -77,6 +128,22 @@ function resumeFailure(error) {
|
|
|
77
128
|
return /resume|session|thread|conversation|not found|unknown id|expired/i.test(message);
|
|
78
129
|
}
|
|
79
130
|
|
|
131
|
+
function freshSessionRetryFailure(error, adapter) {
|
|
132
|
+
return adapter?.id === 'codex' && [
|
|
133
|
+
'APP_AGENT_TURN_INACTIVITY_TIMEOUT',
|
|
134
|
+
'CODEX_STRUCTURED_OUTPUT_MALFORMED',
|
|
135
|
+
'CODEX_STRUCTURED_OUTPUT_INVALID',
|
|
136
|
+
'CODEX_TOOL_ARGUMENTS_TRANSPORT_INVALID',
|
|
137
|
+
'CODEX_TOOL_ARGUMENTS_MALFORMED',
|
|
138
|
+
'CODEX_TOOL_ARGUMENTS_NOT_OBJECT',
|
|
139
|
+
].includes(error?.code);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function providerAttemptCount(value) {
|
|
143
|
+
const direct = Number(value?.providerAttemptCount);
|
|
144
|
+
return Number.isFinite(direct) && direct > 0 ? Math.floor(direct) : 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
80
147
|
export const AGENT_DEFINITIONS = {
|
|
81
148
|
'claude-code': {
|
|
82
149
|
id: 'claude-code',
|
|
@@ -110,6 +177,7 @@ function nowIso() {
|
|
|
110
177
|
|
|
111
178
|
const TERMINAL_EVENT_TYPES = new Set(['done', 'error']);
|
|
112
179
|
const TERMINAL_EVENT_MAX_ATTEMPTS = 5;
|
|
180
|
+
const TOOL_EVENT_MAX_ATTEMPTS = 3;
|
|
113
181
|
|
|
114
182
|
function waitForRetry(delayMs) {
|
|
115
183
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
@@ -117,25 +185,30 @@ function waitForRetry(delayMs) {
|
|
|
117
185
|
|
|
118
186
|
function terminalEventRetryDelay(error, attempt) {
|
|
119
187
|
const status = Number(error?.status);
|
|
120
|
-
const retryable =
|
|
121
|
-
!Number.isFinite(status)
|
|
122
|
-
|| status === 408
|
|
123
|
-
|| status === 429
|
|
124
|
-
|| status >= 500;
|
|
188
|
+
const retryable = !Number.isFinite(status) || status === 408 || status === 429 || status >= 500;
|
|
125
189
|
if (!retryable) return null;
|
|
126
190
|
if (Number.isFinite(error?.retryAfterMs) && error.retryAfterMs > 0) {
|
|
127
191
|
return Math.min(65_000, Math.max(250, error.retryAfterMs));
|
|
128
192
|
}
|
|
129
|
-
return Math.min(8_000, 500 *
|
|
193
|
+
return Math.min(8_000, 500 * 2 ** (attempt - 1));
|
|
130
194
|
}
|
|
131
195
|
|
|
132
|
-
function createEventPoster({
|
|
196
|
+
function createEventPoster({
|
|
197
|
+
apiBaseUrl,
|
|
198
|
+
deviceToken,
|
|
199
|
+
run,
|
|
200
|
+
fetchImpl,
|
|
201
|
+
trace,
|
|
202
|
+
allowInsecureHttp = false,
|
|
203
|
+
}) {
|
|
133
204
|
return async function send(type, payload = {}) {
|
|
134
205
|
const eventId = `${type}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
135
206
|
const started = Date.now();
|
|
136
207
|
const maxAttempts = TERMINAL_EVENT_TYPES.has(type)
|
|
137
208
|
? TERMINAL_EVENT_MAX_ATTEMPTS
|
|
138
|
-
:
|
|
209
|
+
: type === 'tool_call'
|
|
210
|
+
? TOOL_EVENT_MAX_ATTEMPTS
|
|
211
|
+
: 1;
|
|
139
212
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
140
213
|
trace?.info('event_post_start', {
|
|
141
214
|
type,
|
|
@@ -148,6 +221,7 @@ function createEventPoster({ apiBaseUrl, deviceToken, run, fetchImpl, trace }) {
|
|
|
148
221
|
deviceToken,
|
|
149
222
|
runId: run.runId,
|
|
150
223
|
fetchImpl,
|
|
224
|
+
allowInsecureHttp,
|
|
151
225
|
event: {
|
|
152
226
|
type,
|
|
153
227
|
eventId,
|
|
@@ -166,10 +240,7 @@ function createEventPoster({ apiBaseUrl, deviceToken, run, fetchImpl, trace }) {
|
|
|
166
240
|
});
|
|
167
241
|
return response;
|
|
168
242
|
} catch (error) {
|
|
169
|
-
const retryDelayMs =
|
|
170
|
-
attempt < maxAttempts
|
|
171
|
-
? terminalEventRetryDelay(error, attempt)
|
|
172
|
-
: null;
|
|
243
|
+
const retryDelayMs = attempt < maxAttempts ? terminalEventRetryDelay(error, attempt) : null;
|
|
173
244
|
trace?.error('event_post_failed', {
|
|
174
245
|
type,
|
|
175
246
|
eventId,
|
|
@@ -193,7 +264,10 @@ function commandFromEnv(name, fallback, env = process.env) {
|
|
|
193
264
|
function parseExtraArgs(value, fallback = '') {
|
|
194
265
|
const source = value === undefined || value === null || value === '' ? fallback : value;
|
|
195
266
|
if (!source) return [];
|
|
196
|
-
return String(source)
|
|
267
|
+
return String(source)
|
|
268
|
+
.split(/\s+/)
|
|
269
|
+
.map((item) => item.trim())
|
|
270
|
+
.filter(Boolean);
|
|
197
271
|
}
|
|
198
272
|
|
|
199
273
|
function argsAlreadySelectModel(args) {
|
|
@@ -205,7 +279,11 @@ function argsIncludeFlag(args, flag) {
|
|
|
205
279
|
}
|
|
206
280
|
|
|
207
281
|
function structuredUsageEnabled(env = process.env) {
|
|
208
|
-
return
|
|
282
|
+
return (
|
|
283
|
+
String(env.DEXTER_BRIDGE_STRUCTURED_USAGE || 'true')
|
|
284
|
+
.trim()
|
|
285
|
+
.toLowerCase() !== 'false'
|
|
286
|
+
);
|
|
209
287
|
}
|
|
210
288
|
|
|
211
289
|
function argsWithRequiredAgentFlags(args, definition) {
|
|
@@ -229,11 +307,38 @@ function claudeModelEngineSystemPrompt(product) {
|
|
|
229
307
|
'Never execute, simulate, or emit native Claude Code tool calls for those tool names.',
|
|
230
308
|
'The only allowed tool is StructuredOutput, supplied by the JSON schema.',
|
|
231
309
|
`Encode requested ${name} actions only inside StructuredOutput.toolCalls.`,
|
|
232
|
-
'
|
|
310
|
+
'Use only the explicitly invoked InstaWebAI native skill package. Do not discover or invoke any other skills.',
|
|
311
|
+
'Do not inspect the filesystem, project, shell, plugins, MCP servers, or browser.',
|
|
233
312
|
`Be concise: reason only as much as needed to choose the next remote ${name} action.`,
|
|
234
313
|
].join(' ');
|
|
235
314
|
}
|
|
236
315
|
|
|
316
|
+
function claudeOutcomeSystemPrompt(product) {
|
|
317
|
+
const name = normalizeBridgeProduct(product).name;
|
|
318
|
+
if (normalizeBridgeProduct(product).id === 'dexter') {
|
|
319
|
+
return [
|
|
320
|
+
`You are an autonomous Framer harness embedded inside ${name}.`,
|
|
321
|
+
'Work only through the approved Framer Agent tools connected to the user-selected project.',
|
|
322
|
+
'Inspect the project, perform the requested work, verify the result, and return the structured outcome.',
|
|
323
|
+
'Do not access local credentials, unrelated files, other projects, account settings, or billing.',
|
|
324
|
+
'Do not publish unless the assignment explicitly authorizes publishing.',
|
|
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.',
|
|
327
|
+
`Return only the structured outcome result requested by ${name}.`,
|
|
328
|
+
].join(' ');
|
|
329
|
+
}
|
|
330
|
+
return [
|
|
331
|
+
`You are a coding harness embedded inside ${name}.`,
|
|
332
|
+
'Work only inside the current isolated workspace and through the approved InstaWebAI harness tools.',
|
|
333
|
+
'Read and edit the files needed to complete the entire assigned outcome.',
|
|
334
|
+
'Run the commands, preview tools, and browser checks needed to build and verify the app without asking for approval.',
|
|
335
|
+
'Call progress_update before the first inspection or edit and again at every meaningful phase change. Keep each update to one or two natural first-person sentences about what you are doing, why it matters, or what comes next.',
|
|
336
|
+
'Do not access parent directories, environment files, credentials, unrelated user files, plugins, or the external network.',
|
|
337
|
+
'Prioritize working behavior and contract correctness before visual polish.',
|
|
338
|
+
`Return only the structured outcome result requested by ${name}.`,
|
|
339
|
+
].join(' ');
|
|
340
|
+
}
|
|
341
|
+
|
|
237
342
|
export function mapClaudeCliModelId(value) {
|
|
238
343
|
const raw = String(value || '').trim();
|
|
239
344
|
return CLAUDE_CLI_MODEL_IDS[raw.toLowerCase()] || raw;
|
|
@@ -242,20 +347,17 @@ export function mapClaudeCliModelId(value) {
|
|
|
242
347
|
function argsWithSelectedModel(args, modelDefinition, definition) {
|
|
243
348
|
if (!modelDefinition?.invocationName || modelDefinition.invocationName === 'dry-run') return args;
|
|
244
349
|
if (argsAlreadySelectModel(args)) return args;
|
|
245
|
-
const invocationName =
|
|
246
|
-
|
|
247
|
-
|
|
350
|
+
const invocationName =
|
|
351
|
+
definition.id === 'claude-code'
|
|
352
|
+
? mapClaudeCliModelId(modelDefinition.invocationName)
|
|
353
|
+
: modelDefinition.invocationName;
|
|
248
354
|
return [...args, '--model', invocationName];
|
|
249
355
|
}
|
|
250
356
|
|
|
251
357
|
function argsWithStructuredOutput(args, definition, env = process.env) {
|
|
252
358
|
if (!structuredUsageEnabled(env)) return args;
|
|
253
359
|
if (definition.id === 'claude-code') {
|
|
254
|
-
const streamed = [
|
|
255
|
-
...argsWithoutFlagValue(args, '--output-format'),
|
|
256
|
-
'--output-format',
|
|
257
|
-
'stream-json',
|
|
258
|
-
];
|
|
360
|
+
const streamed = [...argsWithoutFlagValue(args, '--output-format'), '--output-format', 'stream-json'];
|
|
259
361
|
if (!argsIncludeFlag(streamed, '--include-partial-messages')) {
|
|
260
362
|
streamed.push('--include-partial-messages');
|
|
261
363
|
}
|
|
@@ -268,21 +370,72 @@ function argsWithStructuredOutput(args, definition, env = process.env) {
|
|
|
268
370
|
return args;
|
|
269
371
|
}
|
|
270
372
|
|
|
271
|
-
function argsWithClaudeIsolation(args, definition) {
|
|
373
|
+
function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
272
374
|
if (definition.id !== 'claude-code') return args;
|
|
273
375
|
const isolated = argsWithoutVariadicFlag(args, '--tools');
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
376
|
+
const harnessToolNames = Array.isArray(options.harnessToolNames)
|
|
377
|
+
? options.harnessToolNames
|
|
378
|
+
: [
|
|
379
|
+
'workspace_inspect',
|
|
380
|
+
'workspace_sync',
|
|
381
|
+
'shell_run',
|
|
382
|
+
'preview_control',
|
|
383
|
+
'browser_control',
|
|
384
|
+
'data_inspect',
|
|
385
|
+
'verification_run',
|
|
386
|
+
];
|
|
387
|
+
const harnessTools = harnessToolNames.map(
|
|
388
|
+
(name) => `mcp__instawebai__${name}`,
|
|
389
|
+
);
|
|
390
|
+
const nativeTools = options.framerHarness
|
|
391
|
+
? harnessTools
|
|
392
|
+
: [
|
|
393
|
+
'Read',
|
|
394
|
+
'Write',
|
|
395
|
+
'Edit',
|
|
396
|
+
'Glob',
|
|
397
|
+
'Grep',
|
|
398
|
+
'Bash',
|
|
399
|
+
...(options.nativeSkillsEnabled ? ['Skill'] : []),
|
|
400
|
+
...harnessTools,
|
|
401
|
+
];
|
|
402
|
+
isolated.push(
|
|
403
|
+
'--tools',
|
|
404
|
+
options.harnessMode
|
|
405
|
+
? nativeTools.join(',')
|
|
406
|
+
: options.nativeSkillsEnabled
|
|
407
|
+
? 'Skill'
|
|
408
|
+
: '',
|
|
409
|
+
);
|
|
410
|
+
const withoutDisabledSkills = isolated.filter((arg) => arg !== '--disable-slash-commands');
|
|
411
|
+
if (!options.nativeSkillsEnabled) withoutDisabledSkills.push('--disable-slash-commands');
|
|
412
|
+
const withoutAllowedTools = argsWithoutVariadicFlag(withoutDisabledSkills, '--allowedTools');
|
|
413
|
+
const projectSettingsOnly = argsWithoutFlagValue(withoutAllowedTools, '--setting-sources');
|
|
414
|
+
projectSettingsOnly.push('--setting-sources', 'project');
|
|
415
|
+
const permissionMode = argsWithoutFlagValue(projectSettingsOnly, '--permission-mode');
|
|
416
|
+
permissionMode.push(
|
|
417
|
+
'--permission-mode',
|
|
418
|
+
options.harnessMode && !options.framerHarness ? 'acceptEdits' : 'dontAsk',
|
|
419
|
+
);
|
|
420
|
+
if (options.harnessMode) permissionMode.push('--allowedTools', nativeTools.join(','));
|
|
421
|
+
if (!argsIncludeFlag(permissionMode, '--strict-mcp-config')) permissionMode.push('--strict-mcp-config');
|
|
422
|
+
if (!argsIncludeFlag(permissionMode, '--no-chrome')) permissionMode.push('--no-chrome');
|
|
423
|
+
let configured = permissionMode;
|
|
424
|
+
if (options.harnessMode && options.mcpConfig) {
|
|
425
|
+
configured = argsWithoutFlagValue(configured, '--mcp-config');
|
|
426
|
+
configured.push('--mcp-config', JSON.stringify(options.mcpConfig));
|
|
427
|
+
}
|
|
428
|
+
if (options.harnessMode && !argsIncludeFlag(configured, '--restricted')) {
|
|
429
|
+
configured.push('--restricted');
|
|
430
|
+
}
|
|
431
|
+
return configured;
|
|
279
432
|
}
|
|
280
433
|
|
|
281
434
|
function normalizeClaudeEffort(value, fallback = 'medium') {
|
|
282
|
-
const effort = String(value || '')
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
435
|
+
const effort = String(value || '')
|
|
436
|
+
.trim()
|
|
437
|
+
.toLowerCase();
|
|
438
|
+
return ['low', 'medium', 'high', 'xhigh', 'max'].includes(effort) ? effort : fallback;
|
|
286
439
|
}
|
|
287
440
|
|
|
288
441
|
function claudeEffortForStep(step, env = process.env) {
|
|
@@ -307,7 +460,9 @@ function argsWithClaudeModelEngine(args, definition, options = {}, env = process
|
|
|
307
460
|
return [
|
|
308
461
|
...isolated,
|
|
309
462
|
'--system-prompt',
|
|
310
|
-
|
|
463
|
+
options.harnessMode
|
|
464
|
+
? claudeOutcomeSystemPrompt(options.product)
|
|
465
|
+
: claudeModelEngineSystemPrompt(options.product),
|
|
311
466
|
'--effort',
|
|
312
467
|
claudeEffortForStep(options.step, env),
|
|
313
468
|
];
|
|
@@ -341,33 +496,61 @@ function argsWithoutVariadicFlag(args, flag) {
|
|
|
341
496
|
return filtered;
|
|
342
497
|
}
|
|
343
498
|
|
|
499
|
+
function normalizeClaudeJsonSchema(value, path = '$') {
|
|
500
|
+
if (Array.isArray(value)) {
|
|
501
|
+
return value.map((item, index) => normalizeClaudeJsonSchema(item, `${path}[${index}]`));
|
|
502
|
+
}
|
|
503
|
+
if (!value || typeof value !== 'object') return value;
|
|
504
|
+
|
|
505
|
+
const normalized = Object.fromEntries(
|
|
506
|
+
Object.entries(value).map(([key, nested]) => [
|
|
507
|
+
key,
|
|
508
|
+
normalizeClaudeJsonSchema(nested, `${path}.${key}`),
|
|
509
|
+
]),
|
|
510
|
+
);
|
|
511
|
+
for (const [exclusiveKey, inclusiveKey] of [
|
|
512
|
+
['exclusiveMinimum', 'minimum'],
|
|
513
|
+
['exclusiveMaximum', 'maximum'],
|
|
514
|
+
]) {
|
|
515
|
+
const exclusive = normalized[exclusiveKey];
|
|
516
|
+
if (typeof exclusive !== 'boolean') continue;
|
|
517
|
+
if (!exclusive) {
|
|
518
|
+
delete normalized[exclusiveKey];
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const bound = normalized[inclusiveKey];
|
|
522
|
+
if (typeof bound !== 'number' || !Number.isFinite(bound)) {
|
|
523
|
+
throw new Error(
|
|
524
|
+
`Invalid Claude JSON Schema at ${path}: ${exclusiveKey}: true requires a finite ${inclusiveKey}.`,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
normalized[exclusiveKey] = bound;
|
|
528
|
+
delete normalized[inclusiveKey];
|
|
529
|
+
}
|
|
530
|
+
return normalized;
|
|
531
|
+
}
|
|
532
|
+
|
|
344
533
|
function compactClaudeOutputSchema(outputSchema) {
|
|
345
534
|
const toolCalls = outputSchema?.properties?.toolCalls;
|
|
346
535
|
const itemSchema = toolCalls?.items;
|
|
347
|
-
const variants = Array.isArray(itemSchema?.anyOf)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
)),
|
|
358
|
-
));
|
|
536
|
+
const variants = Array.isArray(itemSchema?.anyOf) ? itemSchema.anyOf : itemSchema ? [itemSchema] : [];
|
|
537
|
+
const toolNames = Array.from(
|
|
538
|
+
new Set(
|
|
539
|
+
variants.flatMap((variant) =>
|
|
540
|
+
Array.isArray(variant?.properties?.name?.enum)
|
|
541
|
+
? variant.properties.name.enum.filter((name) => typeof name === 'string' && name)
|
|
542
|
+
: [],
|
|
543
|
+
),
|
|
544
|
+
),
|
|
545
|
+
);
|
|
359
546
|
return {
|
|
360
547
|
type: 'object',
|
|
361
548
|
properties: {
|
|
362
549
|
text: { type: 'string' },
|
|
363
550
|
toolCalls: {
|
|
364
551
|
type: 'array',
|
|
365
|
-
...(Number.isFinite(Number(toolCalls?.minItems))
|
|
366
|
-
|
|
367
|
-
: {}),
|
|
368
|
-
...(Number.isFinite(Number(toolCalls?.maxItems))
|
|
369
|
-
? { maxItems: Number(toolCalls.maxItems) }
|
|
370
|
-
: {}),
|
|
552
|
+
...(Number.isFinite(Number(toolCalls?.minItems)) ? { minItems: Number(toolCalls.minItems) } : {}),
|
|
553
|
+
...(Number.isFinite(Number(toolCalls?.maxItems)) ? { maxItems: Number(toolCalls.maxItems) } : {}),
|
|
371
554
|
items: {
|
|
372
555
|
type: 'object',
|
|
373
556
|
properties: {
|
|
@@ -392,16 +575,24 @@ function compactClaudeOutputSchema(outputSchema) {
|
|
|
392
575
|
};
|
|
393
576
|
}
|
|
394
577
|
|
|
395
|
-
function argsWithOutputSchema(
|
|
578
|
+
function argsWithOutputSchema(
|
|
579
|
+
args,
|
|
580
|
+
definition,
|
|
581
|
+
outputSchema,
|
|
582
|
+
responseContract = 'model-turn',
|
|
583
|
+
) {
|
|
396
584
|
if (definition.id !== 'claude-code' || !outputSchema) return args;
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
585
|
+
const normalizedResponseContract =
|
|
586
|
+
normalizeStructuredResponseContract(responseContract);
|
|
587
|
+
const normalizedSchema = normalizeClaudeJsonSchema(outputSchema);
|
|
588
|
+
// Model turns use a compact transport envelope because their tool catalog can
|
|
589
|
+
// contain flexible schemas. Harness outcomes already use a small, strict,
|
|
590
|
+
// provider-neutral result schema and must remain in that direct shape.
|
|
591
|
+
const launchSchema =
|
|
592
|
+
normalizedResponseContract === STRUCTURED_RESPONSE_CONTRACTS.OUTCOME
|
|
593
|
+
? assertOutcomeResponseContract(responseContract, normalizedSchema)
|
|
594
|
+
: compactClaudeOutputSchema(normalizedSchema);
|
|
595
|
+
return [...argsWithoutFlagValue(args, '--json-schema'), '--json-schema', JSON.stringify(launchSchema)];
|
|
405
596
|
}
|
|
406
597
|
|
|
407
598
|
function argsWithPromptInput(args, definition) {
|
|
@@ -456,7 +647,12 @@ function argsWithResumedSession(args, definition, sessionId) {
|
|
|
456
647
|
}
|
|
457
648
|
|
|
458
649
|
function agentSessionResumeEnabled(definition, env = process.env) {
|
|
459
|
-
if (
|
|
650
|
+
if (
|
|
651
|
+
String(env.DEXTER_BRIDGE_SESSION_RESUME || 'true')
|
|
652
|
+
.trim()
|
|
653
|
+
.toLowerCase() === 'false'
|
|
654
|
+
)
|
|
655
|
+
return false;
|
|
460
656
|
const baseArgs = parseExtraArgs(env[definition.argsEnv], definition.defaultArgs);
|
|
461
657
|
if (definition.id === 'claude-code' && argsIncludeFlag(baseArgs, '--no-session-persistence')) return false;
|
|
462
658
|
if (definition.id === 'codex' && argsIncludeFlag(baseArgs, '--ephemeral')) return false;
|
|
@@ -468,13 +664,17 @@ export function buildAgentArgs(definition, modelDefinition, env = process.env, o
|
|
|
468
664
|
const requiredArgs = argsWithRequiredAgentFlags(baseArgs, definition);
|
|
469
665
|
const modelArgs = argsWithSelectedModel(requiredArgs, modelDefinition, definition);
|
|
470
666
|
const structuredArgs = argsWithStructuredOutput(modelArgs, definition, env);
|
|
471
|
-
const isolatedArgs = argsWithClaudeIsolation(
|
|
667
|
+
const isolatedArgs = argsWithClaudeIsolation(
|
|
668
|
+
structuredArgs,
|
|
669
|
+
definition,
|
|
670
|
+
options,
|
|
671
|
+
);
|
|
472
672
|
const modelEngineArgs = argsWithClaudeModelEngine(isolatedArgs, definition, options, env);
|
|
473
673
|
const schemaArgs = argsWithOutputSchema(
|
|
474
674
|
modelEngineArgs,
|
|
475
675
|
definition,
|
|
476
676
|
options.outputSchema,
|
|
477
|
-
options.
|
|
677
|
+
options.responseContract,
|
|
478
678
|
);
|
|
479
679
|
const resumedArgs = argsWithResumedSession(schemaArgs, definition, options.resumeSessionId);
|
|
480
680
|
return argsWithPromptInput(resumedArgs, definition);
|
|
@@ -497,7 +697,12 @@ let cachedLoginShellEnvMeta = { attempted: false, loaded: false, keys: 0 };
|
|
|
497
697
|
|
|
498
698
|
function loginShellEnv() {
|
|
499
699
|
if (process.env.DEXTER_BRIDGE_DISABLE_LOGIN_SHELL_ENV === '1' || process.platform === 'win32') {
|
|
500
|
-
cachedLoginShellEnvMeta = {
|
|
700
|
+
cachedLoginShellEnvMeta = {
|
|
701
|
+
attempted: false,
|
|
702
|
+
loaded: false,
|
|
703
|
+
keys: 0,
|
|
704
|
+
disabled: true,
|
|
705
|
+
};
|
|
501
706
|
return {};
|
|
502
707
|
}
|
|
503
708
|
if (cachedLoginShellEnv) return cachedLoginShellEnv;
|
|
@@ -561,20 +766,18 @@ export function mergePathEntries(paths, platform = process.platform) {
|
|
|
561
766
|
|
|
562
767
|
export function processEnvWithCliPath(baseEnv = process.env, platform = process.platform) {
|
|
563
768
|
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
564
|
-
const homeDirectory =
|
|
565
|
-
|
|
566
|
-
baseEnv.USERPROFILE
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
]
|
|
577
|
-
: ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
|
|
769
|
+
const homeDirectory =
|
|
770
|
+
platform === 'win32'
|
|
771
|
+
? String(baseEnv.USERPROFILE || baseEnv.HOME || (process.platform === 'win32' ? os.homedir() : '')).trim()
|
|
772
|
+
: String(baseEnv.HOME || (platform === process.platform ? os.homedir() : '')).trim();
|
|
773
|
+
const fallbackPath =
|
|
774
|
+
platform === 'win32'
|
|
775
|
+
? [
|
|
776
|
+
homeDirectory ? pathApi.join(homeDirectory, '.local', 'bin') : '',
|
|
777
|
+
baseEnv.APPDATA ? pathApi.join(baseEnv.APPDATA, 'npm') : '',
|
|
778
|
+
baseEnv.LOCALAPPDATA ? pathApi.join(baseEnv.LOCALAPPDATA, 'Programs') : '',
|
|
779
|
+
]
|
|
780
|
+
: ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
|
|
578
781
|
const shellEnv = platform === 'win32' ? {} : loginShellEnv();
|
|
579
782
|
const existingPath = String(baseEnv.PATH || '');
|
|
580
783
|
const mergedPath = mergePathEntries([shellEnv.PATH, existingPath, ...fallbackPath], platform);
|
|
@@ -585,7 +788,11 @@ export function processEnvWithCliPath(baseEnv = process.env, platform = process.
|
|
|
585
788
|
};
|
|
586
789
|
}
|
|
587
790
|
|
|
588
|
-
export function resolveAgentCwd(
|
|
791
|
+
export function resolveAgentCwd(
|
|
792
|
+
env = process.env,
|
|
793
|
+
currentWorkingDirectory = process.cwd(),
|
|
794
|
+
platform = process.platform,
|
|
795
|
+
) {
|
|
589
796
|
const explicit = String(env.DEXTER_BRIDGE_AGENT_CWD || env.DEXTER_BRIDGE_WORKDIR || '').trim();
|
|
590
797
|
if (explicit) return explicit;
|
|
591
798
|
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
@@ -596,7 +803,9 @@ export function resolveAgentCwd(env = process.env, currentWorkingDirectory = pro
|
|
|
596
803
|
|
|
597
804
|
function processEnvSummary(env, platform = process.platform) {
|
|
598
805
|
const delimiter = platform === 'win32' ? ';' : ':';
|
|
599
|
-
const pathEntries = String(env.PATH || '')
|
|
806
|
+
const pathEntries = String(env.PATH || '')
|
|
807
|
+
.split(delimiter)
|
|
808
|
+
.filter(Boolean);
|
|
600
809
|
return {
|
|
601
810
|
hasAzureOpenAiApiKey: Boolean(env.AZURE_OPENAI_API_KEY),
|
|
602
811
|
hasOpenAiApiKey: Boolean(env.OPENAI_API_KEY),
|
|
@@ -608,18 +817,28 @@ function processEnvSummary(env, platform = process.platform) {
|
|
|
608
817
|
};
|
|
609
818
|
}
|
|
610
819
|
|
|
611
|
-
export function executableCandidates(
|
|
820
|
+
export function executableCandidates(
|
|
821
|
+
command,
|
|
822
|
+
env = process.env,
|
|
823
|
+
platform = process.platform,
|
|
824
|
+
existsSync = fs.existsSync,
|
|
825
|
+
) {
|
|
612
826
|
const raw = String(command || '').trim();
|
|
613
827
|
if (!raw) return [];
|
|
614
828
|
if (/[\\/]/.test(raw)) return [raw];
|
|
615
829
|
const delimiter = platform === 'win32' ? ';' : ':';
|
|
616
830
|
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
617
|
-
const pathEntries = String(env.PATH || '')
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
831
|
+
const pathEntries = String(env.PATH || '')
|
|
832
|
+
.split(delimiter)
|
|
833
|
+
.filter(Boolean);
|
|
834
|
+
const extensions =
|
|
835
|
+
platform === 'win32'
|
|
836
|
+
? path.win32.extname(raw)
|
|
837
|
+
? ['']
|
|
838
|
+
: String(env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
839
|
+
.split(';')
|
|
840
|
+
.filter(Boolean)
|
|
841
|
+
: [''];
|
|
623
842
|
const candidates = [];
|
|
624
843
|
for (const directory of pathEntries) {
|
|
625
844
|
for (const extension of extensions) {
|
|
@@ -634,7 +853,12 @@ export function executableCandidates(command, env = process.env, platform = proc
|
|
|
634
853
|
return [...new Set(candidates.length ? candidates : [raw])];
|
|
635
854
|
}
|
|
636
855
|
|
|
637
|
-
export function resolveExecutableCommand(
|
|
856
|
+
export function resolveExecutableCommand(
|
|
857
|
+
command,
|
|
858
|
+
env = process.env,
|
|
859
|
+
platform = process.platform,
|
|
860
|
+
existsSync = fs.existsSync,
|
|
861
|
+
) {
|
|
638
862
|
return executableCandidates(command, env, platform, existsSync)[0] || String(command || '').trim();
|
|
639
863
|
}
|
|
640
864
|
|
|
@@ -644,7 +868,9 @@ export function parseAgentVersion(output) {
|
|
|
644
868
|
}
|
|
645
869
|
|
|
646
870
|
function versionParts(value) {
|
|
647
|
-
const match = String(value || '')
|
|
871
|
+
const match = String(value || '')
|
|
872
|
+
.trim()
|
|
873
|
+
.match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
648
874
|
return match ? match.slice(1).map(Number) : null;
|
|
649
875
|
}
|
|
650
876
|
|
|
@@ -673,9 +899,7 @@ export function selectAgentRuntime(inspections, definition, modelDefinition) {
|
|
|
673
899
|
return {
|
|
674
900
|
ok: false,
|
|
675
901
|
command: first?.command || definition.fallbackCommand,
|
|
676
|
-
code: first?.code === 'ENOENT'
|
|
677
|
-
? 'DEXTER_AGENT_NOT_FOUND'
|
|
678
|
-
: first?.code,
|
|
902
|
+
code: first?.code === 'ENOENT' ? 'DEXTER_AGENT_NOT_FOUND' : first?.code,
|
|
679
903
|
error: first?.error || `${definition.label} is not installed or could not be started.`,
|
|
680
904
|
candidates: inspections,
|
|
681
905
|
};
|
|
@@ -683,7 +907,9 @@ export function selectAgentRuntime(inspections, definition, modelDefinition) {
|
|
|
683
907
|
|
|
684
908
|
const ranked = successful
|
|
685
909
|
.map((inspection, index) => ({ ...inspection, discoveryIndex: index }))
|
|
686
|
-
.sort(
|
|
910
|
+
.sort(
|
|
911
|
+
(left, right) => compareAgentVersions(right.version, left.version) || left.discoveryIndex - right.discoveryIndex,
|
|
912
|
+
);
|
|
687
913
|
const compatible = ranked.find((inspection) => modelSupportsAgentVersion(modelDefinition, inspection.version));
|
|
688
914
|
if (compatible) {
|
|
689
915
|
return {
|
|
@@ -708,7 +934,12 @@ export function selectAgentRuntime(inspections, definition, modelDefinition) {
|
|
|
708
934
|
|
|
709
935
|
export function processInvocation(command, args, env = process.env, platform = process.platform) {
|
|
710
936
|
const resolvedCommand = resolveExecutableCommand(command, env, platform);
|
|
711
|
-
return {
|
|
937
|
+
return {
|
|
938
|
+
command: resolvedCommand,
|
|
939
|
+
args,
|
|
940
|
+
resolvedCommand,
|
|
941
|
+
windowsHide: platform === 'win32',
|
|
942
|
+
};
|
|
712
943
|
}
|
|
713
944
|
|
|
714
945
|
function safeCommandArgs(args) {
|
|
@@ -734,9 +965,7 @@ function jsonDiagnosticEvents(value) {
|
|
|
734
965
|
.flatMap((line) => {
|
|
735
966
|
try {
|
|
736
967
|
const parsed = JSON.parse(line);
|
|
737
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
738
|
-
? [parsed]
|
|
739
|
-
: [];
|
|
968
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? [parsed] : [];
|
|
740
969
|
} catch {
|
|
741
970
|
return [];
|
|
742
971
|
}
|
|
@@ -745,15 +974,24 @@ function jsonDiagnosticEvents(value) {
|
|
|
745
974
|
|
|
746
975
|
function claudeFailureDetails(stdout) {
|
|
747
976
|
const events = jsonDiagnosticEvents(stdout);
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
977
|
+
const init = [...events].reverse().find((event) => event.type === 'system' && event.subtype === 'init');
|
|
978
|
+
const model = typeof init?.model === 'string' ? init.model.trim().toLowerCase() : '';
|
|
979
|
+
const rateLimit = [...events]
|
|
980
|
+
.reverse()
|
|
981
|
+
.find(
|
|
982
|
+
(event) =>
|
|
983
|
+
event.type === 'rate_limit_event' && event.rate_limit_info && typeof event.rate_limit_info === 'object',
|
|
984
|
+
);
|
|
752
985
|
const rateLimitInfo = rateLimit?.rate_limit_info;
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
986
|
+
if (rateLimitInfo?.errorCode === 'credits_required' || rateLimitInfo?.overageDisabledReason === 'out_of_credits') {
|
|
987
|
+
if (model.includes('fable')) {
|
|
988
|
+
return {
|
|
989
|
+
code: 'CLAUDE_CREDITS_REQUIRED',
|
|
990
|
+
message:
|
|
991
|
+
'Claude Fable could not run because this Claude account has no extra-usage credits available. '
|
|
992
|
+
+ 'Add Claude usage credits or switch this run to Opus or Sonnet.',
|
|
993
|
+
};
|
|
994
|
+
}
|
|
757
995
|
return {
|
|
758
996
|
code: 'CLAUDE_CREDITS_REQUIRED',
|
|
759
997
|
message:
|
|
@@ -772,15 +1010,14 @@ function claudeFailureDetails(stdout) {
|
|
|
772
1010
|
};
|
|
773
1011
|
}
|
|
774
1012
|
|
|
775
|
-
const resultError = [...events]
|
|
776
|
-
|
|
777
|
-
&& (event.is_error === true || event.subtype === 'error'));
|
|
1013
|
+
const resultError = [...events]
|
|
1014
|
+
.reverse()
|
|
1015
|
+
.find((event) => event.type === 'result' && (event.is_error === true || event.subtype === 'error'));
|
|
778
1016
|
const resultMessages = Array.isArray(resultError?.errors)
|
|
779
1017
|
? resultError.errors.filter((message) => typeof message === 'string' && message.trim())
|
|
780
1018
|
: [];
|
|
781
1019
|
const resultMessage =
|
|
782
|
-
resultMessages.join(' ')
|
|
783
|
-
|| (typeof resultError?.result === 'string' ? resultError.result.trim() : '');
|
|
1020
|
+
resultMessages.join(' ') || (typeof resultError?.result === 'string' ? resultError.result.trim() : '');
|
|
784
1021
|
if (resultMessage) {
|
|
785
1022
|
return {
|
|
786
1023
|
code: 'CLAUDE_RUN_FAILED',
|
|
@@ -798,10 +1035,7 @@ export function agentFailureDetails(command, code, stdout = '', stderr = '') {
|
|
|
798
1035
|
}
|
|
799
1036
|
|
|
800
1037
|
const combined = [stderr, stdout].filter(Boolean).join('\n').trim();
|
|
801
|
-
const lines = combined
|
|
802
|
-
.split(/\r?\n/)
|
|
803
|
-
.map(normalizeDiagnosticLine)
|
|
804
|
-
.filter(Boolean);
|
|
1038
|
+
const lines = combined.split(/\r?\n/).map(normalizeDiagnosticLine).filter(Boolean);
|
|
805
1039
|
const diagnosticPatterns = [
|
|
806
1040
|
/missing environment variable/i,
|
|
807
1041
|
/not inside a trusted directory/i,
|
|
@@ -816,9 +1050,7 @@ export function agentFailureDetails(command, code, stdout = '', stderr = '') {
|
|
|
816
1050
|
/^failed\b/i,
|
|
817
1051
|
];
|
|
818
1052
|
const diagnostics = lines.filter((line) => diagnosticPatterns.some((pattern) => pattern.test(line)));
|
|
819
|
-
const detail = diagnostics.length
|
|
820
|
-
? diagnostics.slice(-3).join(' ')
|
|
821
|
-
: clip(combined, 1000);
|
|
1053
|
+
const detail = diagnostics.length ? diagnostics.slice(-3).join(' ') : clip(combined, 1000);
|
|
822
1054
|
return {
|
|
823
1055
|
code: 'AGENT_PROCESS_FAILED',
|
|
824
1056
|
message: `${command} exited with code ${code}.${detail ? ` ${clip(detail, 1000)}` : ''}`.trim(),
|
|
@@ -829,20 +1061,26 @@ export function agentFailureMessage(command, code, stdout = '', stderr = '') {
|
|
|
829
1061
|
return agentFailureDetails(command, code, stdout, stderr).message;
|
|
830
1062
|
}
|
|
831
1063
|
|
|
832
|
-
function runProcess(
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1064
|
+
function runProcess(
|
|
1065
|
+
command,
|
|
1066
|
+
args,
|
|
1067
|
+
stdin,
|
|
1068
|
+
{
|
|
1069
|
+
timeoutMs = 120000,
|
|
1070
|
+
maxDurationMs,
|
|
1071
|
+
trace,
|
|
1072
|
+
childEnv: providedChildEnv,
|
|
1073
|
+
cwd: providedCwd,
|
|
1074
|
+
platform = process.platform,
|
|
1075
|
+
signal,
|
|
1076
|
+
killGraceMs = 250,
|
|
1077
|
+
onProgress,
|
|
1078
|
+
stdoutProgress,
|
|
1079
|
+
} = {},
|
|
1080
|
+
) {
|
|
841
1081
|
return new Promise((resolve, reject) => {
|
|
842
1082
|
if (signal?.aborted) {
|
|
843
|
-
const error = signal.reason instanceof Error
|
|
844
|
-
? signal.reason
|
|
845
|
-
: new Error('The model process was cancelled.');
|
|
1083
|
+
const error = signal.reason instanceof Error ? signal.reason : new Error('The model process was cancelled.');
|
|
846
1084
|
error.code = error.code || 'RUN_CANCELLED';
|
|
847
1085
|
reject(error);
|
|
848
1086
|
return;
|
|
@@ -853,7 +1091,10 @@ function runProcess(command, args, stdin, {
|
|
|
853
1091
|
// hardDeadlineMs bounds total wall-clock time regardless of activity.
|
|
854
1092
|
const hardDeadlineMs = Math.max(maxDurationMs || timeoutMs * 5, 10);
|
|
855
1093
|
const childEnv = processEnvWithCliPath(providedChildEnv || process.env, platform);
|
|
856
|
-
const cwd =
|
|
1094
|
+
const cwd =
|
|
1095
|
+
typeof providedCwd === 'string' && providedCwd.trim()
|
|
1096
|
+
? providedCwd
|
|
1097
|
+
: resolveAgentCwd(childEnv, process.cwd(), platform);
|
|
857
1098
|
const invocation = processInvocation(command, args, childEnv, platform);
|
|
858
1099
|
trace?.info('agent_process_spawn', {
|
|
859
1100
|
command,
|
|
@@ -872,11 +1113,18 @@ function runProcess(command, args, stdin, {
|
|
|
872
1113
|
windowsHide: invocation.windowsHide,
|
|
873
1114
|
detached: platform !== 'win32',
|
|
874
1115
|
});
|
|
1116
|
+
onProgress?.({
|
|
1117
|
+
kind: 'turn_started',
|
|
1118
|
+
provider: 'cli',
|
|
1119
|
+
occurredAt: new Date().toISOString(),
|
|
1120
|
+
});
|
|
875
1121
|
let stdout = '';
|
|
876
1122
|
let stderr = '';
|
|
877
1123
|
let settled = false;
|
|
1124
|
+
let terminationError = null;
|
|
878
1125
|
let inactivityTimer = null;
|
|
879
1126
|
let forceKillTimer = null;
|
|
1127
|
+
let terminationFallbackTimer = null;
|
|
880
1128
|
const killChildTree = (killSignal) => {
|
|
881
1129
|
if (platform !== 'win32' && child.pid) {
|
|
882
1130
|
try {
|
|
@@ -896,38 +1144,50 @@ function runProcess(command, args, stdin, {
|
|
|
896
1144
|
clearTimeout(inactivityTimer);
|
|
897
1145
|
clearTimeout(deadlineTimer);
|
|
898
1146
|
};
|
|
899
|
-
const
|
|
1147
|
+
const settleWithError = (error) => {
|
|
1148
|
+
if (settled) return;
|
|
1149
|
+
settled = true;
|
|
1150
|
+
clearTimers();
|
|
1151
|
+
clearTimeout(forceKillTimer);
|
|
1152
|
+
clearTimeout(terminationFallbackTimer);
|
|
1153
|
+
signal?.removeEventListener('abort', abort);
|
|
1154
|
+
error.stdout = stdout;
|
|
1155
|
+
error.stderr = stderr;
|
|
1156
|
+
reject(error);
|
|
1157
|
+
};
|
|
1158
|
+
const terminateChildTree = (error) => {
|
|
900
1159
|
killChildTree('SIGTERM');
|
|
901
|
-
forceKillTimer = setTimeout(() =>
|
|
1160
|
+
forceKillTimer = setTimeout(() => {
|
|
1161
|
+
killChildTree('SIGKILL');
|
|
1162
|
+
terminationFallbackTimer = setTimeout(
|
|
1163
|
+
() => settleWithError(error),
|
|
1164
|
+
Math.max(killGraceMs, 250),
|
|
1165
|
+
);
|
|
1166
|
+
terminationFallbackTimer.unref?.();
|
|
1167
|
+
}, killGraceMs);
|
|
902
1168
|
forceKillTimer.unref?.();
|
|
903
1169
|
};
|
|
904
1170
|
const abort = () => {
|
|
905
|
-
if (settled) return;
|
|
906
|
-
settled = true;
|
|
1171
|
+
if (settled || terminationError) return;
|
|
907
1172
|
clearTimers();
|
|
908
|
-
|
|
909
|
-
const error = signal?.reason instanceof Error
|
|
910
|
-
? signal.reason
|
|
911
|
-
: new Error('The model process was cancelled.');
|
|
1173
|
+
const error = signal?.reason instanceof Error ? signal.reason : new Error('The model process was cancelled.');
|
|
912
1174
|
error.code = error.code || 'RUN_CANCELLED';
|
|
913
|
-
|
|
914
|
-
error
|
|
1175
|
+
terminationError = error;
|
|
1176
|
+
terminateChildTree(error);
|
|
915
1177
|
trace?.info('agent_process_cancelled', {
|
|
916
1178
|
command,
|
|
917
1179
|
durationMs: Date.now() - started,
|
|
918
1180
|
});
|
|
919
|
-
reject(error);
|
|
920
1181
|
};
|
|
921
|
-
const timeOut = (message) => {
|
|
922
|
-
if (settled) return;
|
|
923
|
-
settled = true;
|
|
1182
|
+
const timeOut = (code, message) => {
|
|
1183
|
+
if (settled || terminationError) return;
|
|
924
1184
|
clearTimers();
|
|
925
|
-
terminateChildTree();
|
|
926
1185
|
const error = new Error(message);
|
|
927
|
-
error.code =
|
|
928
|
-
error.
|
|
929
|
-
error.stderr = stderr;
|
|
1186
|
+
error.code = code;
|
|
1187
|
+
error.retryable = true;
|
|
930
1188
|
error.timedOut = true;
|
|
1189
|
+
terminationError = error;
|
|
1190
|
+
terminateChildTree(error);
|
|
931
1191
|
trace?.error('agent_process_timeout', {
|
|
932
1192
|
command,
|
|
933
1193
|
durationMs: Date.now() - started,
|
|
@@ -938,47 +1198,74 @@ function runProcess(command, args, stdin, {
|
|
|
938
1198
|
stdoutExcerpt: clip(stdout, 1000),
|
|
939
1199
|
stderrExcerpt: clip(stderr, 1000),
|
|
940
1200
|
});
|
|
941
|
-
reject(error);
|
|
942
1201
|
};
|
|
943
1202
|
const armInactivityTimer = () => {
|
|
944
1203
|
clearTimeout(inactivityTimer);
|
|
945
1204
|
inactivityTimer = setTimeout(() => {
|
|
946
|
-
timeOut(
|
|
1205
|
+
timeOut(
|
|
1206
|
+
'APP_AGENT_TURN_INACTIVITY_TIMEOUT',
|
|
1207
|
+
`${command} timed out after ${timeoutMs}ms without meaningful output.`,
|
|
1208
|
+
);
|
|
947
1209
|
}, timeoutMs);
|
|
948
1210
|
};
|
|
949
1211
|
const deadlineTimer = setTimeout(() => {
|
|
950
|
-
timeOut(
|
|
1212
|
+
timeOut(
|
|
1213
|
+
'APP_AGENT_TURN_HARD_TIMEOUT',
|
|
1214
|
+
`${command} timed out after ${hardDeadlineMs}ms at the safety limit.`,
|
|
1215
|
+
);
|
|
951
1216
|
}, hardDeadlineMs);
|
|
952
1217
|
signal?.addEventListener('abort', abort, { once: true });
|
|
953
1218
|
armInactivityTimer();
|
|
954
1219
|
|
|
955
1220
|
child.stdout.on('data', (chunk) => {
|
|
956
|
-
|
|
957
|
-
|
|
1221
|
+
const text = chunk.toString('utf8');
|
|
1222
|
+
stdout += text;
|
|
1223
|
+
if (!terminationError) {
|
|
1224
|
+
armInactivityTimer();
|
|
1225
|
+
stdoutProgress?.push(text);
|
|
1226
|
+
onProgress?.({
|
|
1227
|
+
kind: 'output_delta',
|
|
1228
|
+
provider: 'cli',
|
|
1229
|
+
occurredAt: new Date().toISOString(),
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
958
1232
|
});
|
|
959
1233
|
child.stderr.on('data', (chunk) => {
|
|
960
1234
|
stderr += chunk.toString('utf8');
|
|
961
|
-
|
|
1235
|
+
if (!terminationError) {
|
|
1236
|
+
armInactivityTimer();
|
|
1237
|
+
onProgress?.({
|
|
1238
|
+
kind: 'output_delta',
|
|
1239
|
+
provider: 'cli',
|
|
1240
|
+
occurredAt: new Date().toISOString(),
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
962
1243
|
});
|
|
963
1244
|
child.on('error', (error) => {
|
|
964
1245
|
if (settled) return;
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1246
|
+
if (terminationError) {
|
|
1247
|
+
settleWithError(terminationError);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
969
1250
|
trace?.error('agent_process_error', {
|
|
970
1251
|
command,
|
|
971
1252
|
durationMs: Date.now() - started,
|
|
972
1253
|
error: errorMeta(error),
|
|
973
1254
|
});
|
|
974
|
-
|
|
1255
|
+
settleWithError(error);
|
|
975
1256
|
});
|
|
976
1257
|
child.on('close', (code) => {
|
|
977
1258
|
if (settled) return;
|
|
1259
|
+
if (terminationError) {
|
|
1260
|
+
settleWithError(terminationError);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
978
1263
|
settled = true;
|
|
979
1264
|
clearTimers();
|
|
980
1265
|
clearTimeout(forceKillTimer);
|
|
1266
|
+
clearTimeout(terminationFallbackTimer);
|
|
981
1267
|
signal?.removeEventListener('abort', abort);
|
|
1268
|
+
stdoutProgress?.flush();
|
|
982
1269
|
const meta = {
|
|
983
1270
|
command,
|
|
984
1271
|
code,
|
|
@@ -1015,11 +1302,33 @@ function definitionForAgent(agent) {
|
|
|
1015
1302
|
return AGENT_DEFINITIONS[normalizeAgentName(agent)] || AGENT_DEFINITIONS[DEFAULT_BRIDGE_AGENT];
|
|
1016
1303
|
}
|
|
1017
1304
|
|
|
1305
|
+
function definitionForRuntimeProfile(profile, fallbackAgent) {
|
|
1306
|
+
if (!profile) return definitionForAgent(fallbackAgent);
|
|
1307
|
+
if (profile.driver === 'claude-code-cli') return AGENT_DEFINITIONS['claude-code'];
|
|
1308
|
+
if (profile.driver === 'codex-app-server') return AGENT_DEFINITIONS.codex;
|
|
1309
|
+
return {
|
|
1310
|
+
id: profile.id || profile.driver,
|
|
1311
|
+
label: profile.label || profile.driver || 'Local runtime',
|
|
1312
|
+
model: profile.models?.[0]?.id || 'local-runtime',
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1018
1316
|
function adapterSessionId(run) {
|
|
1019
1317
|
return run?.turnId || run?.runId;
|
|
1020
1318
|
}
|
|
1021
1319
|
|
|
1022
1320
|
function adapterInvocationModel(run, selectedModel, agent) {
|
|
1321
|
+
const runtimeInvocation = run?.runtimeProfile?.model?.invocationName;
|
|
1322
|
+
if (typeof runtimeInvocation === 'string' && runtimeInvocation.trim()) {
|
|
1323
|
+
return runtimeInvocation.trim();
|
|
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
|
+
}
|
|
1023
1332
|
const explicit = run?.companion?.model?.invocationName;
|
|
1024
1333
|
if (typeof explicit === 'string' && explicit.trim()) return explicit.trim();
|
|
1025
1334
|
if (selectedModel?.invocationName) return selectedModel.invocationName;
|
|
@@ -1029,14 +1338,63 @@ function adapterInvocationModel(run, selectedModel, agent) {
|
|
|
1029
1338
|
return raw.startsWith(prefix) ? raw.slice(prefix.length) : raw || undefined;
|
|
1030
1339
|
}
|
|
1031
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
|
+
|
|
1032
1385
|
function controlRequestsCancellation(response) {
|
|
1033
1386
|
return Boolean(
|
|
1034
|
-
response?.control?.cancelRequested
|
|
1035
|
-
|| response?.status === 'cancelled'
|
|
1036
|
-
|| response?.status === 'expired',
|
|
1387
|
+
response?.control?.cancelRequested || response?.status === 'cancelled' || response?.status === 'expired',
|
|
1037
1388
|
);
|
|
1038
1389
|
}
|
|
1039
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
|
+
|
|
1040
1398
|
function boundedDurationMs(value, fallback, minimum = 10, maximum = 60 * 60 * 1000) {
|
|
1041
1399
|
const parsed = Number(value);
|
|
1042
1400
|
if (!Number.isFinite(parsed)) return fallback;
|
|
@@ -1050,27 +1408,57 @@ function waitForControl(delayMs, signal) {
|
|
|
1050
1408
|
return;
|
|
1051
1409
|
}
|
|
1052
1410
|
const timer = setTimeout(resolve, delayMs);
|
|
1053
|
-
signal?.addEventListener(
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1411
|
+
signal?.addEventListener(
|
|
1412
|
+
'abort',
|
|
1413
|
+
() => {
|
|
1414
|
+
clearTimeout(timer);
|
|
1415
|
+
reject(signal.reason || new Error('Control monitor stopped.'));
|
|
1416
|
+
},
|
|
1417
|
+
{ once: true },
|
|
1418
|
+
);
|
|
1057
1419
|
});
|
|
1058
1420
|
}
|
|
1059
1421
|
|
|
1060
1422
|
class CompanionRunCancelledError extends Error {
|
|
1061
|
-
constructor(
|
|
1423
|
+
constructor(
|
|
1424
|
+
message = 'The local bridge model turn was cancelled.',
|
|
1425
|
+
cancellationSource = 'unknown',
|
|
1426
|
+
) {
|
|
1062
1427
|
super(message);
|
|
1063
1428
|
this.name = 'CompanionRunCancelledError';
|
|
1064
1429
|
this.code = 'RUN_CANCELLED';
|
|
1430
|
+
this.cancellationSource = cancellationSource;
|
|
1065
1431
|
}
|
|
1066
1432
|
}
|
|
1067
1433
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
}
|
|
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.';
|
|
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(' ');
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
async function callProviderAdapter(
|
|
1451
|
+
adapter,
|
|
1452
|
+
input,
|
|
1453
|
+
{
|
|
1454
|
+
send,
|
|
1455
|
+
trace,
|
|
1456
|
+
controlPollMs = 5_000,
|
|
1457
|
+
providerProgressIntervalMs = 2_000,
|
|
1458
|
+
cancellationGraceMs = 15_000,
|
|
1459
|
+
product,
|
|
1460
|
+
} = {},
|
|
1461
|
+
) {
|
|
1074
1462
|
const productName = normalizeBridgeProduct(product).name;
|
|
1075
1463
|
if (!adapter || typeof adapter.runModelTurn !== 'function') {
|
|
1076
1464
|
throw new Error('The selected provider adapter cannot execute model turns.');
|
|
@@ -1079,12 +1467,81 @@ async function callProviderAdapter(adapter, input, {
|
|
|
1079
1467
|
let finished = false;
|
|
1080
1468
|
let cancelRequested = false;
|
|
1081
1469
|
let cancelSent = false;
|
|
1470
|
+
let cancelTimer = null;
|
|
1471
|
+
let cancelActionPromise = null;
|
|
1472
|
+
let rejectCancellation;
|
|
1473
|
+
let lastProviderProgressSentAt = 0;
|
|
1474
|
+
let activityChain = Promise.resolve();
|
|
1475
|
+
const cancellation = new Promise((_, reject) => {
|
|
1476
|
+
rejectCancellation = reject;
|
|
1477
|
+
});
|
|
1082
1478
|
|
|
1083
|
-
|
|
1479
|
+
function requestCancel() {
|
|
1084
1480
|
cancelRequested = true;
|
|
1085
|
-
if (cancelSent) return;
|
|
1481
|
+
if (cancelSent) return cancelActionPromise;
|
|
1086
1482
|
cancelSent = true;
|
|
1087
|
-
|
|
1483
|
+
cancelTimer = setTimeout(() => {
|
|
1484
|
+
void adapter.resetSession?.(input.sessionId);
|
|
1485
|
+
rejectCancellation(new CompanionRunCancelledError(
|
|
1486
|
+
'The provider did not finish cancelling before the cleanup deadline.',
|
|
1487
|
+
));
|
|
1488
|
+
}, cancellationGraceMs);
|
|
1489
|
+
cancelTimer.unref?.();
|
|
1490
|
+
cancelActionPromise = Promise.resolve()
|
|
1491
|
+
.then(() => adapter.cancel?.(input.sessionId))
|
|
1492
|
+
.catch((error) => {
|
|
1493
|
+
trace?.warn('adapter_cancel_failed', {
|
|
1494
|
+
error: errorMeta(error),
|
|
1495
|
+
});
|
|
1496
|
+
});
|
|
1497
|
+
return cancelActionPromise;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function waitForCancellationAcknowledgement(providerTurn) {
|
|
1501
|
+
return Promise.race([
|
|
1502
|
+
Promise.allSettled([
|
|
1503
|
+
providerTurn,
|
|
1504
|
+
cancelActionPromise || Promise.resolve(),
|
|
1505
|
+
]),
|
|
1506
|
+
cancellation,
|
|
1507
|
+
]);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function finishCancellation() {
|
|
1511
|
+
return new CompanionRunCancelledError();
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function postActivity(payload) {
|
|
1515
|
+
activityChain = activityChain
|
|
1516
|
+
.then(async () => {
|
|
1517
|
+
if (finished) return;
|
|
1518
|
+
const response = await send('activity', payload);
|
|
1519
|
+
if (controlRequestsCancellation(response)) await requestCancel();
|
|
1520
|
+
})
|
|
1521
|
+
.catch((error) => {
|
|
1522
|
+
if (!finished) {
|
|
1523
|
+
trace?.warn('adapter_activity_post_failed', {
|
|
1524
|
+
error: errorMeta(error),
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
return activityChain;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function reportProviderProgress(progress = {}) {
|
|
1532
|
+
if (finished || cancelRequested) return;
|
|
1533
|
+
const now = Date.now();
|
|
1534
|
+
if (now - lastProviderProgressSentAt < providerProgressIntervalMs) return;
|
|
1535
|
+
lastProviderProgressSentAt = now;
|
|
1536
|
+
void postActivity({
|
|
1537
|
+
stage: 'model_turn',
|
|
1538
|
+
source: 'provider',
|
|
1539
|
+
meaningful: true,
|
|
1540
|
+
kind: progress.kind || 'output_delta',
|
|
1541
|
+
provider: progress.provider || adapter.id || null,
|
|
1542
|
+
occurredAt: progress.occurredAt || new Date(now).toISOString(),
|
|
1543
|
+
message: `${adapter.label || adapter.id || 'Provider'} is actively generating the next ${productName} action.`,
|
|
1544
|
+
});
|
|
1088
1545
|
}
|
|
1089
1546
|
|
|
1090
1547
|
async function monitorControl() {
|
|
@@ -1092,11 +1549,12 @@ async function callProviderAdapter(adapter, input, {
|
|
|
1092
1549
|
try {
|
|
1093
1550
|
await waitForControl(controlPollMs, monitorAbort.signal);
|
|
1094
1551
|
if (finished || monitorAbort.signal.aborted) return;
|
|
1095
|
-
|
|
1552
|
+
await postActivity({
|
|
1096
1553
|
stage: 'model_turn',
|
|
1554
|
+
source: 'bridge',
|
|
1555
|
+
meaningful: false,
|
|
1097
1556
|
message: `${adapter.label || adapter.id || 'Provider'} is still generating the next ${productName} action.`,
|
|
1098
1557
|
});
|
|
1099
|
-
if (controlRequestsCancellation(response)) await requestCancel();
|
|
1100
1558
|
} catch (error) {
|
|
1101
1559
|
if (monitorAbort.signal.aborted) return;
|
|
1102
1560
|
trace?.warn('adapter_control_poll_failed', { error: errorMeta(error) });
|
|
@@ -1105,15 +1563,28 @@ async function callProviderAdapter(adapter, input, {
|
|
|
1105
1563
|
}
|
|
1106
1564
|
|
|
1107
1565
|
const controlMonitor = monitorControl();
|
|
1566
|
+
const providerTurn = Promise.resolve().then(() =>
|
|
1567
|
+
adapter.runModelTurn({
|
|
1568
|
+
...input,
|
|
1569
|
+
onProgress: reportProviderProgress,
|
|
1570
|
+
}));
|
|
1108
1571
|
try {
|
|
1109
|
-
const result = await
|
|
1110
|
-
|
|
1572
|
+
const result = await Promise.race([providerTurn, cancellation]);
|
|
1573
|
+
await Promise.race([activityChain, cancellation]);
|
|
1574
|
+
if (cancelRequested) {
|
|
1575
|
+
await waitForCancellationAcknowledgement(providerTurn);
|
|
1576
|
+
throw finishCancellation();
|
|
1577
|
+
}
|
|
1111
1578
|
return result;
|
|
1112
1579
|
} catch (error) {
|
|
1113
|
-
if (cancelRequested)
|
|
1580
|
+
if (cancelRequested) {
|
|
1581
|
+
await waitForCancellationAcknowledgement(providerTurn).catch(() => undefined);
|
|
1582
|
+
throw finishCancellation();
|
|
1583
|
+
}
|
|
1114
1584
|
throw error;
|
|
1115
1585
|
} finally {
|
|
1116
1586
|
finished = true;
|
|
1587
|
+
clearTimeout(cancelTimer);
|
|
1117
1588
|
monitorAbort.abort();
|
|
1118
1589
|
await controlMonitor.catch(() => undefined);
|
|
1119
1590
|
}
|
|
@@ -1123,35 +1594,32 @@ export async function resolveAgentRuntime(definition, modelDefinition, options =
|
|
|
1123
1594
|
const platform = options.platform || process.platform;
|
|
1124
1595
|
const childEnv = processEnvWithCliPath(options.env || process.env, platform);
|
|
1125
1596
|
const configuredCommand = commandFromEnv(definition.commandEnv, definition.fallbackCommand, childEnv);
|
|
1126
|
-
const candidates = executableCandidates(
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
});
|
|
1597
|
+
const candidates = executableCandidates(configuredCommand, childEnv, platform, options.existsSync || fs.existsSync);
|
|
1598
|
+
const inspect =
|
|
1599
|
+
options.inspect
|
|
1600
|
+
|| (async (command) => {
|
|
1601
|
+
try {
|
|
1602
|
+
const result = await runProcess(command, ['--version'], '', {
|
|
1603
|
+
timeoutMs: 10000,
|
|
1604
|
+
childEnv,
|
|
1605
|
+
platform,
|
|
1606
|
+
});
|
|
1607
|
+
const output = (result.stdout || result.stderr || '').trim();
|
|
1608
|
+
return {
|
|
1609
|
+
ok: true,
|
|
1610
|
+
command,
|
|
1611
|
+
output,
|
|
1612
|
+
version: parseAgentVersion(output),
|
|
1613
|
+
};
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
return {
|
|
1616
|
+
ok: false,
|
|
1617
|
+
command,
|
|
1618
|
+
code: error?.code,
|
|
1619
|
+
error: error?.message || String(error || ''),
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
});
|
|
1155
1623
|
const inspections = await Promise.all(candidates.map((candidate) => inspect(candidate)));
|
|
1156
1624
|
return selectAgentRuntime(inspections, definition, modelDefinition);
|
|
1157
1625
|
}
|
|
@@ -1210,7 +1678,10 @@ export async function checkAgentAuthentication(agent, runtime, options = {}) {
|
|
|
1210
1678
|
const childEnv = processEnvWithCliPath(options.env || process.env, platform);
|
|
1211
1679
|
const inspect = options.inspect || inspectClaudeAuthentication;
|
|
1212
1680
|
try {
|
|
1213
|
-
const authentication = await inspect(runtime.command, {
|
|
1681
|
+
const authentication = await inspect(runtime.command, {
|
|
1682
|
+
childEnv,
|
|
1683
|
+
platform,
|
|
1684
|
+
});
|
|
1214
1685
|
if (authentication?.loggedIn === true) {
|
|
1215
1686
|
return {
|
|
1216
1687
|
ok: true,
|
|
@@ -1240,8 +1711,13 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
1240
1711
|
const args = buildAgentArgs(definition, modelDefinition, process.env, {
|
|
1241
1712
|
resumeSessionId: options.resumeSessionId,
|
|
1242
1713
|
outputSchema: options.outputSchema,
|
|
1714
|
+
responseContract: options.responseContract,
|
|
1243
1715
|
step: options.step,
|
|
1244
1716
|
product: options.product,
|
|
1717
|
+
nativeSkillsEnabled: options.nativeSkillsEnabled,
|
|
1718
|
+
harnessMode: options.harnessMode,
|
|
1719
|
+
mcpConfig: options.mcpConfig,
|
|
1720
|
+
harnessToolNames: options.harnessToolNames,
|
|
1245
1721
|
});
|
|
1246
1722
|
const timeoutMs = options.timeoutMs || Number(process.env.DEXTER_BRIDGE_AGENT_TIMEOUT_MS || 120000);
|
|
1247
1723
|
const maxDurationMs = boundedDurationMs(
|
|
@@ -1267,13 +1743,17 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
1267
1743
|
maxDurationMs,
|
|
1268
1744
|
trace: options.trace,
|
|
1269
1745
|
signal: options.signal,
|
|
1746
|
+
cwd: options.cwd,
|
|
1747
|
+
onProgress: options.onProgress,
|
|
1748
|
+
stdoutProgress:
|
|
1749
|
+
definition.id === 'claude-code'
|
|
1750
|
+
? createClaudeProgressParser(options.onProgress, { cwd: options.cwd })
|
|
1751
|
+
: undefined,
|
|
1270
1752
|
});
|
|
1271
1753
|
}
|
|
1272
1754
|
|
|
1273
1755
|
function agentErrorWithOutputUsage(agent, error) {
|
|
1274
|
-
const failure = error instanceof Error
|
|
1275
|
-
? error
|
|
1276
|
-
: new Error(String(error || 'The companion model call failed.'));
|
|
1756
|
+
const failure = error instanceof Error ? error : new Error(String(error || 'The companion model call failed.'));
|
|
1277
1757
|
const parsed = parseAgentOutput(agent, failure.stdout || '');
|
|
1278
1758
|
if (!parsed.usageAvailable) return failure;
|
|
1279
1759
|
const accumulator = createCompanionUsageAccumulator(agent);
|
|
@@ -1286,7 +1766,7 @@ function agentErrorWithOutputUsage(agent, error) {
|
|
|
1286
1766
|
async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
1287
1767
|
const product = normalizeBridgeProduct(run?.product || options.product);
|
|
1288
1768
|
const productName = product.name;
|
|
1289
|
-
if (normalizeAgentName(agent) === 'dry-run') {
|
|
1769
|
+
if (normalizeAgentName(agent) === 'dry-run' && !options.providerAdapter) {
|
|
1290
1770
|
await send('done', {
|
|
1291
1771
|
operationType: 'chat',
|
|
1292
1772
|
outcome: 'answer',
|
|
@@ -1301,27 +1781,47 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1301
1781
|
});
|
|
1302
1782
|
return;
|
|
1303
1783
|
}
|
|
1304
|
-
const definition =
|
|
1305
|
-
const requestedModel =
|
|
1306
|
-
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1784
|
+
const definition = definitionForRuntimeProfile(run?.runtimeProfile, agent);
|
|
1785
|
+
const requestedModel =
|
|
1786
|
+
run?.runtimeProfile?.model?.id || run?.companion?.model?.id || run?.model || options.selectedModel;
|
|
1787
|
+
const selectedModel = run?.runtimeProfile?.model || companionModelDefinition(requestedModel, definition.id);
|
|
1788
|
+
const selectedModelId =
|
|
1789
|
+
typeof requestedModel === 'string' && requestedModel.trim()
|
|
1790
|
+
? requestedModel.trim()
|
|
1791
|
+
: selectedModel?.id || definition.model;
|
|
1310
1792
|
const adapter = options.providerAdapter || null;
|
|
1311
1793
|
const sessionKey = companionSessionKey(run);
|
|
1312
1794
|
const rememberedSession = rememberedCompanionSession(sessionKey);
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1795
|
+
const nativeSkills = normalizeNativeSkills(run?.modelTurn?.nativeSkills);
|
|
1796
|
+
const nativeProvider =
|
|
1797
|
+
definition.id === 'claude-code'
|
|
1798
|
+
? 'claude-code'
|
|
1799
|
+
: adapter?.id === 'codex' || definition.id === 'codex'
|
|
1800
|
+
? 'codex'
|
|
1801
|
+
: null;
|
|
1802
|
+
const materializedNativeSkills =
|
|
1803
|
+
nativeProvider === 'claude-code'
|
|
1804
|
+
? materializeNativeSkills(nativeSkills, {
|
|
1805
|
+
provider: nativeProvider,
|
|
1806
|
+
sessionId: sessionKey,
|
|
1807
|
+
})
|
|
1808
|
+
: null;
|
|
1809
|
+
const deltaRequested = deltaModelSessionsEnabled(options.env) && run?.modelTurn?.session?.contextMode === 'delta';
|
|
1316
1810
|
let callContextMode = deltaRequested && rememberedSession ? 'delta' : 'full';
|
|
1317
|
-
let prompt =
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1811
|
+
let prompt =
|
|
1812
|
+
callContextMode === 'delta'
|
|
1813
|
+
? buildModelTurnDeltaPrompt(run.modelTurn, product)
|
|
1814
|
+
: deltaRequested
|
|
1815
|
+
? buildModelTurnFallbackPrompt(run.modelTurn, product)
|
|
1816
|
+
: buildModelTurnPrompt(run.modelTurn, product);
|
|
1817
|
+
if (materializedNativeSkills) {
|
|
1818
|
+
prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
|
|
1819
|
+
}
|
|
1322
1820
|
const callStartedAt = Date.now();
|
|
1323
1821
|
let providerSessionId;
|
|
1324
1822
|
let fallbackAfterResumeFailure = false;
|
|
1823
|
+
let providerAttempts = 0;
|
|
1824
|
+
let retryReason = null;
|
|
1325
1825
|
const statusResponse = await send('status', {
|
|
1326
1826
|
stage: 'model_turn',
|
|
1327
1827
|
message: `${definition.label} is generating the next ${productName} action.`,
|
|
@@ -1332,32 +1832,35 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1332
1832
|
}
|
|
1333
1833
|
const usageAccumulator = createCompanionUsageAccumulator(definition.id);
|
|
1334
1834
|
const failedModelCall = (error) => {
|
|
1335
|
-
const failure = error instanceof Error
|
|
1336
|
-
? error
|
|
1337
|
-
: new Error(String(error || 'The companion model call failed.'));
|
|
1835
|
+
const failure = error instanceof Error ? error : new Error(String(error || 'The companion model call failed.'));
|
|
1338
1836
|
usageAccumulator.add(failure.companionUsage || {});
|
|
1339
1837
|
const snapshot = usageAccumulator.snapshot();
|
|
1340
1838
|
failure.companionUsage = {
|
|
1341
1839
|
...snapshot,
|
|
1342
|
-
modelCalls: [
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1840
|
+
modelCalls: [
|
|
1841
|
+
{
|
|
1842
|
+
callId: run.runId,
|
|
1843
|
+
step: Number(run?.modelTurn?.step ?? 0),
|
|
1844
|
+
model: selectedModelId,
|
|
1845
|
+
status: 'failed',
|
|
1846
|
+
durationMs: Date.now() - callStartedAt,
|
|
1847
|
+
promptChars: prompt.length,
|
|
1848
|
+
requestedContextMode: deltaRequested ? 'delta' : 'full',
|
|
1849
|
+
contextMode: callContextMode,
|
|
1850
|
+
resumed: callContextMode === 'delta',
|
|
1851
|
+
resumeSessionAvailable: Boolean(rememberedSession),
|
|
1852
|
+
fallbackAfterResumeFailure,
|
|
1853
|
+
providerAttemptCount: Math.max(1, providerAttempts),
|
|
1854
|
+
retryReason,
|
|
1855
|
+
...snapshot.tokenUsage,
|
|
1856
|
+
estimatedCostUSD: snapshot.estimatedCostUSD,
|
|
1857
|
+
},
|
|
1858
|
+
],
|
|
1357
1859
|
};
|
|
1358
1860
|
return failure;
|
|
1359
1861
|
};
|
|
1360
1862
|
let resultText;
|
|
1863
|
+
let nativeSkillStatus = null;
|
|
1361
1864
|
if (adapter) {
|
|
1362
1865
|
const sessionId = sessionKey || adapterSessionId(run);
|
|
1363
1866
|
const invocationModel = adapterInvocationModel(run, selectedModel, definition.id);
|
|
@@ -1378,18 +1881,14 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1378
1881
|
prompt,
|
|
1379
1882
|
model: invocationModel,
|
|
1380
1883
|
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
120000,
|
|
1384
|
-
1000,
|
|
1385
|
-
),
|
|
1884
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
1885
|
+
timeoutMs: boundedDurationMs(options.timeoutMs ?? options.env?.DEXTER_BRIDGE_AGENT_TIMEOUT_MS, 120000, 1000),
|
|
1386
1886
|
maxDurationMs: boundedDurationMs(
|
|
1387
|
-
run?.modelTurn?.maxDurationMs
|
|
1388
|
-
?? options.maxDurationMs
|
|
1389
|
-
?? options.env?.DEXTER_BRIDGE_AGENT_MAX_DURATION_MS,
|
|
1887
|
+
run?.modelTurn?.maxDurationMs ?? options.maxDurationMs ?? options.env?.DEXTER_BRIDGE_AGENT_MAX_DURATION_MS,
|
|
1390
1888
|
600000,
|
|
1391
1889
|
1000,
|
|
1392
1890
|
),
|
|
1891
|
+
nativeSkills,
|
|
1393
1892
|
};
|
|
1394
1893
|
const adapterOptions = {
|
|
1395
1894
|
send,
|
|
@@ -1405,36 +1904,72 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1405
1904
|
let result;
|
|
1406
1905
|
try {
|
|
1407
1906
|
result = await callProviderAdapter(adapter, adapterInput, adapterOptions);
|
|
1907
|
+
providerAttempts += providerAttemptCount(result);
|
|
1908
|
+
retryReason = result?.retryReason || retryReason;
|
|
1408
1909
|
} catch (error) {
|
|
1409
|
-
|
|
1910
|
+
const retryResumeFailure = callContextMode === 'delta' && resumeFailure(error);
|
|
1911
|
+
const retryFreshSession = freshSessionRetryFailure(error, adapter);
|
|
1912
|
+
providerAttempts += providerAttemptCount(error);
|
|
1913
|
+
if (!retryResumeFailure && !retryFreshSession) {
|
|
1914
|
+
throw failedModelCall(error);
|
|
1915
|
+
}
|
|
1410
1916
|
usageAccumulator.add(error?.companionUsage || {});
|
|
1411
1917
|
fallbackAfterResumeFailure = true;
|
|
1918
|
+
retryReason =
|
|
1919
|
+
error?.code === 'APP_AGENT_TURN_INACTIVITY_TIMEOUT'
|
|
1920
|
+
? 'timeout'
|
|
1921
|
+
: retryFreshSession
|
|
1922
|
+
? 'transport_error'
|
|
1923
|
+
: 'resume_failure';
|
|
1412
1924
|
callContextMode = 'full';
|
|
1413
1925
|
prompt = buildModelTurnFallbackPrompt(run.modelTurn, product);
|
|
1926
|
+
if (materializedNativeSkills) {
|
|
1927
|
+
prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
|
|
1928
|
+
}
|
|
1414
1929
|
await adapter.resetSession?.(sessionId);
|
|
1415
1930
|
options.trace?.warn('agent_adapter_resume_fallback', {
|
|
1416
1931
|
sessionId,
|
|
1417
1932
|
error: errorMeta(error),
|
|
1418
1933
|
fallbackPromptChars: prompt.length,
|
|
1419
1934
|
});
|
|
1420
|
-
|
|
1935
|
+
const retryTimeoutMs = retryFreshSession
|
|
1936
|
+
? boundedDurationMs(options.env?.DEXTER_BRIDGE_CODEX_RETRY_TIMEOUT_MS, 180000, 1000)
|
|
1937
|
+
: adapterInput.timeoutMs;
|
|
1938
|
+
result = await callProviderAdapter(
|
|
1939
|
+
adapter,
|
|
1940
|
+
{
|
|
1941
|
+
...adapterInput,
|
|
1942
|
+
prompt,
|
|
1943
|
+
timeoutMs: retryTimeoutMs,
|
|
1944
|
+
maxTransportCorrectionAttempts: 1,
|
|
1945
|
+
},
|
|
1946
|
+
adapterOptions,
|
|
1947
|
+
)
|
|
1948
|
+
.then((result) => {
|
|
1949
|
+
providerAttempts += providerAttemptCount(result);
|
|
1950
|
+
retryReason = retryReason || result?.retryReason || null;
|
|
1951
|
+
return result;
|
|
1952
|
+
})
|
|
1421
1953
|
.catch((error) => {
|
|
1954
|
+
providerAttempts += providerAttemptCount(error);
|
|
1422
1955
|
throw failedModelCall(error);
|
|
1423
1956
|
});
|
|
1424
1957
|
}
|
|
1425
1958
|
usageAccumulator.add(result);
|
|
1426
1959
|
resultText = result?.text;
|
|
1427
1960
|
providerSessionId = result?.threadId || result?.sessionId;
|
|
1961
|
+
nativeSkillStatus = result?.nativeSkillLifecycle || null;
|
|
1428
1962
|
} else {
|
|
1429
|
-
const runtime =
|
|
1430
|
-
|
|
1431
|
-
|
|
1963
|
+
const runtime =
|
|
1964
|
+
options.runtime
|
|
1965
|
+
|| (await resolveAgentRuntime(definition, selectedModel, {
|
|
1966
|
+
env: options.env,
|
|
1967
|
+
}));
|
|
1432
1968
|
if (!runtime.ok) {
|
|
1433
1969
|
throw new Error(runtime.error || `${definition.label} is not available.`);
|
|
1434
1970
|
}
|
|
1435
1971
|
const resumeSessionId =
|
|
1436
|
-
callContextMode === 'delta'
|
|
1437
|
-
&& agentSessionResumeEnabled(definition, options.env)
|
|
1972
|
+
callContextMode === 'delta' && agentSessionResumeEnabled(definition, options.env)
|
|
1438
1973
|
? rememberedSession?.providerSessionId
|
|
1439
1974
|
: undefined;
|
|
1440
1975
|
const cliAbort = new AbortController();
|
|
@@ -1447,23 +1982,60 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1447
1982
|
);
|
|
1448
1983
|
let cliFinished = false;
|
|
1449
1984
|
let cliCancelled = false;
|
|
1985
|
+
let lastProviderProgressSentAt = 0;
|
|
1986
|
+
let activityChain = Promise.resolve();
|
|
1987
|
+
const requestCliCancel = () => {
|
|
1988
|
+
if (cliCancelled) return;
|
|
1989
|
+
cliCancelled = true;
|
|
1990
|
+
cliAbort.abort(
|
|
1991
|
+
Object.assign(
|
|
1992
|
+
new Error(`The ${productName} bridge model turn was cancelled.`),
|
|
1993
|
+
{ code: 'RUN_CANCELLED' },
|
|
1994
|
+
),
|
|
1995
|
+
);
|
|
1996
|
+
};
|
|
1997
|
+
const postCliActivity = (payload) => {
|
|
1998
|
+
activityChain = activityChain
|
|
1999
|
+
.then(async () => {
|
|
2000
|
+
const response = await send('activity', payload);
|
|
2001
|
+
if (controlRequestsCancellation(response)) requestCliCancel();
|
|
2002
|
+
})
|
|
2003
|
+
.catch((error) => {
|
|
2004
|
+
if (!cliFinished) {
|
|
2005
|
+
options.trace?.warn('cli_activity_post_failed', {
|
|
2006
|
+
error: errorMeta(error),
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
});
|
|
2010
|
+
return activityChain;
|
|
2011
|
+
};
|
|
2012
|
+
const reportCliProgress = (progress = {}) => {
|
|
2013
|
+
if (cliFinished || cliCancelled) return;
|
|
2014
|
+
const now = Date.now();
|
|
2015
|
+
if (now - lastProviderProgressSentAt < 2_000) return;
|
|
2016
|
+
lastProviderProgressSentAt = now;
|
|
2017
|
+
void postCliActivity({
|
|
2018
|
+
stage: 'model_turn',
|
|
2019
|
+
source: 'provider',
|
|
2020
|
+
meaningful: true,
|
|
2021
|
+
kind: progress.kind || 'output_delta',
|
|
2022
|
+
provider: definition.id,
|
|
2023
|
+
occurredAt: progress.occurredAt || new Date(now).toISOString(),
|
|
2024
|
+
message: `${definition.label} is actively generating the next ${productName} action.`,
|
|
2025
|
+
});
|
|
2026
|
+
};
|
|
1450
2027
|
const controlMonitor = (async () => {
|
|
1451
2028
|
while (!cliFinished && !monitorAbort.signal.aborted) {
|
|
1452
2029
|
try {
|
|
1453
2030
|
await waitForControl(controlPollMs, monitorAbort.signal);
|
|
1454
2031
|
if (cliFinished || monitorAbort.signal.aborted) return;
|
|
1455
|
-
|
|
2032
|
+
await postCliActivity({
|
|
1456
2033
|
stage: 'model_turn',
|
|
2034
|
+
source: 'bridge',
|
|
2035
|
+
meaningful: false,
|
|
1457
2036
|
message: `${definition.label} is still generating the next ${productName} action.`,
|
|
1458
2037
|
});
|
|
1459
|
-
if (
|
|
1460
|
-
cliCancelled = true;
|
|
1461
|
-
cliAbort.abort(Object.assign(
|
|
1462
|
-
new Error(`The ${productName} bridge model turn was cancelled.`),
|
|
1463
|
-
{ code: 'RUN_CANCELLED' },
|
|
1464
|
-
));
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
2038
|
+
if (cliCancelled) return;
|
|
1467
2039
|
} catch (error) {
|
|
1468
2040
|
if (monitorAbort.signal.aborted) return;
|
|
1469
2041
|
options.trace?.warn('cli_control_poll_failed', {
|
|
@@ -1480,10 +2052,14 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1480
2052
|
runtime,
|
|
1481
2053
|
resumeSessionId,
|
|
1482
2054
|
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
2055
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
1483
2056
|
step: run?.modelTurn?.step,
|
|
1484
2057
|
product,
|
|
1485
2058
|
maxDurationMs: run?.modelTurn?.maxDurationMs ?? options.maxDurationMs,
|
|
1486
2059
|
signal: cliAbort.signal,
|
|
2060
|
+
cwd: materializedNativeSkills?.workspace,
|
|
2061
|
+
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2062
|
+
onProgress: reportCliProgress,
|
|
1487
2063
|
});
|
|
1488
2064
|
} catch (error) {
|
|
1489
2065
|
if (cliCancelled || error?.code === 'RUN_CANCELLED') {
|
|
@@ -1495,6 +2071,9 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1495
2071
|
fallbackAfterResumeFailure = true;
|
|
1496
2072
|
callContextMode = 'full';
|
|
1497
2073
|
prompt = buildModelTurnFallbackPrompt(run.modelTurn, product);
|
|
2074
|
+
if (materializedNativeSkills) {
|
|
2075
|
+
prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
|
|
2076
|
+
}
|
|
1498
2077
|
options.trace?.warn('agent_cli_resume_fallback', {
|
|
1499
2078
|
resumeSessionId,
|
|
1500
2079
|
error: errorMeta(failure),
|
|
@@ -1506,10 +2085,14 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1506
2085
|
runtime,
|
|
1507
2086
|
resumeSessionId: undefined,
|
|
1508
2087
|
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
2088
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
1509
2089
|
step: run?.modelTurn?.step,
|
|
1510
2090
|
product,
|
|
1511
2091
|
maxDurationMs: run?.modelTurn?.maxDurationMs ?? options.maxDurationMs,
|
|
1512
2092
|
signal: cliAbort.signal,
|
|
2093
|
+
cwd: materializedNativeSkills?.workspace,
|
|
2094
|
+
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2095
|
+
onProgress: reportCliProgress,
|
|
1513
2096
|
}).catch((error) => {
|
|
1514
2097
|
if (cliCancelled || error?.code === 'RUN_CANCELLED') {
|
|
1515
2098
|
throw new CompanionRunCancelledError();
|
|
@@ -1521,10 +2104,19 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1521
2104
|
monitorAbort.abort();
|
|
1522
2105
|
await controlMonitor.catch(() => undefined);
|
|
1523
2106
|
}
|
|
2107
|
+
await activityChain;
|
|
2108
|
+
if (cliCancelled) throw new CompanionRunCancelledError();
|
|
1524
2109
|
const parsed = parseAgentOutput(definition.id, result.stdout);
|
|
1525
2110
|
usageAccumulator.add(parsed);
|
|
1526
2111
|
resultText = parsed.resultText;
|
|
1527
2112
|
providerSessionId = parsed.sessionId;
|
|
2113
|
+
nativeSkillStatus = materializedNativeSkills
|
|
2114
|
+
? nativeSkillLifecycle(
|
|
2115
|
+
materializedNativeSkills,
|
|
2116
|
+
'slash-command',
|
|
2117
|
+
callContextMode === 'delta',
|
|
2118
|
+
)
|
|
2119
|
+
: null;
|
|
1528
2120
|
}
|
|
1529
2121
|
if (providerSessionId || adapter) {
|
|
1530
2122
|
rememberCompanionSession(sessionKey, {
|
|
@@ -1545,6 +2137,8 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1545
2137
|
resumed: callContextMode === 'delta',
|
|
1546
2138
|
resumeSessionAvailable: Boolean(rememberedSession),
|
|
1547
2139
|
fallbackAfterResumeFailure,
|
|
2140
|
+
providerAttemptCount: Math.max(1, providerAttempts),
|
|
2141
|
+
retryReason,
|
|
1548
2142
|
...usageSnapshot.tokenUsage,
|
|
1549
2143
|
estimatedCostUSD: usageSnapshot.estimatedCostUSD,
|
|
1550
2144
|
};
|
|
@@ -1554,10 +2148,7 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1554
2148
|
};
|
|
1555
2149
|
let completion;
|
|
1556
2150
|
try {
|
|
1557
|
-
completion = normalizeModelTurnCompletion(
|
|
1558
|
-
extractJsonObject(resultText),
|
|
1559
|
-
selectedModelId,
|
|
1560
|
-
);
|
|
2151
|
+
completion = normalizeModelTurnCompletion(extractJsonObject(resultText), selectedModelId);
|
|
1561
2152
|
} catch (error) {
|
|
1562
2153
|
error.companionUsage = {
|
|
1563
2154
|
...usage,
|
|
@@ -1570,33 +2161,641 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1570
2161
|
outcome: 'answer',
|
|
1571
2162
|
completion,
|
|
1572
2163
|
model: selectedModelId,
|
|
2164
|
+
...(nativeSkillStatus
|
|
2165
|
+
? { nativeSkillLifecycle: nativeSkillStatus }
|
|
2166
|
+
: {}),
|
|
1573
2167
|
...usage,
|
|
1574
2168
|
});
|
|
1575
2169
|
}
|
|
1576
2170
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
selectedModel,
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
2171
|
+
async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
2172
|
+
const product = normalizeBridgeProduct(run?.product || options.product);
|
|
2173
|
+
const definition = definitionForRuntimeProfile(run?.runtimeProfile, agent);
|
|
2174
|
+
const requestedModel =
|
|
2175
|
+
run?.runtimeProfile?.model?.id
|
|
2176
|
+
|| run?.companion?.model?.id
|
|
2177
|
+
|| run?.model
|
|
2178
|
+
|| options.selectedModel;
|
|
2179
|
+
const selectedModel = harnessOutcomeModel(run, definition);
|
|
2180
|
+
const selectedModelId =
|
|
2181
|
+
selectedModel?.id
|
|
2182
|
+
|| (
|
|
2183
|
+
typeof requestedModel === 'string' && requestedModel.trim()
|
|
2184
|
+
? requestedModel.trim()
|
|
2185
|
+
: definition.model
|
|
2186
|
+
);
|
|
2187
|
+
const outcomeMaxDurationMs = boundedDurationMs(
|
|
2188
|
+
run?.outcome?.maxDurationMs
|
|
2189
|
+
?? options.maxDurationMs
|
|
2190
|
+
?? options.env?.DEXTER_BRIDGE_AGENT_MAX_DURATION_MS,
|
|
2191
|
+
20 * 60_000,
|
|
2192
|
+
1_000,
|
|
2193
|
+
);
|
|
2194
|
+
const outcomeInactivityTimeoutMs = Math.min(
|
|
2195
|
+
outcomeMaxDurationMs,
|
|
2196
|
+
boundedDurationMs(
|
|
2197
|
+
options.timeoutMs ?? options.env?.DEXTER_BRIDGE_AGENT_TIMEOUT_MS,
|
|
2198
|
+
10 * 60_000,
|
|
2199
|
+
1_000,
|
|
2200
|
+
),
|
|
2201
|
+
);
|
|
2202
|
+
const sessionKey = companionSessionKey(run);
|
|
2203
|
+
const infrastructureAbortController = new AbortController();
|
|
2204
|
+
const controlMonitorAbort = new AbortController();
|
|
2205
|
+
let finished = false;
|
|
2206
|
+
let cancelRequested = false;
|
|
2207
|
+
let cancellationSource = 'unknown';
|
|
2208
|
+
let cancelAction = null;
|
|
2209
|
+
let rejectCancellation;
|
|
2210
|
+
let latestTokenUsage = normalizeCompanionTokenUsage();
|
|
2211
|
+
let activityChain = Promise.resolve();
|
|
2212
|
+
const cancellation = new Promise((_, reject) => {
|
|
2213
|
+
rejectCancellation = reject;
|
|
2214
|
+
});
|
|
2215
|
+
const cancellationError = () => {
|
|
2216
|
+
const error = new CompanionRunCancelledError(
|
|
2217
|
+
'The local bridge model turn was cancelled.',
|
|
2218
|
+
cancellationSource,
|
|
2219
|
+
);
|
|
2220
|
+
error.companionUsage = {
|
|
2221
|
+
tokenUsage: latestTokenUsage,
|
|
2222
|
+
usageAvailable: latestTokenUsage.totalTokens > 0,
|
|
2223
|
+
usageSource: definition.id,
|
|
2224
|
+
usageAccuracy: latestTokenUsage.totalTokens > 0 ? 'reported' : 'unavailable',
|
|
2225
|
+
};
|
|
2226
|
+
return error;
|
|
2227
|
+
};
|
|
2228
|
+
const requestCancel = (source = 'unknown') => {
|
|
2229
|
+
if (cancellationSource === 'unknown' && source) {
|
|
2230
|
+
cancellationSource = source;
|
|
2231
|
+
}
|
|
2232
|
+
if (cancelRequested) return cancelAction;
|
|
2233
|
+
cancelRequested = true;
|
|
2234
|
+
infrastructureAbortController.abort();
|
|
2235
|
+
cancelAction = Promise.resolve(options.providerAdapter?.cancel?.(sessionKey))
|
|
2236
|
+
.catch((error) => {
|
|
2237
|
+
options.trace?.warn?.('outcome_cancel_failed', { error: errorMeta(error) });
|
|
2238
|
+
});
|
|
2239
|
+
rejectCancellation(cancellationError());
|
|
2240
|
+
return cancelAction;
|
|
2241
|
+
};
|
|
2242
|
+
const postActivity = (payload) => {
|
|
2243
|
+
activityChain = activityChain
|
|
2244
|
+
.then(async () => {
|
|
2245
|
+
if (finished) return;
|
|
2246
|
+
const response = await send('activity', payload);
|
|
2247
|
+
if (controlRequestsCancellation(response)) {
|
|
2248
|
+
await requestCancel(controlCancellationSource(response));
|
|
2249
|
+
}
|
|
2250
|
+
})
|
|
2251
|
+
.catch((error) => {
|
|
2252
|
+
if (!finished) {
|
|
2253
|
+
options.trace?.warn?.('outcome_activity_post_failed', {
|
|
2254
|
+
error: errorMeta(error),
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
});
|
|
2258
|
+
return activityChain;
|
|
2259
|
+
};
|
|
2260
|
+
const progress = (event = {}) => {
|
|
2261
|
+
if (event.tokenUsage) {
|
|
2262
|
+
latestTokenUsage = normalizeCompanionTokenUsage(event.tokenUsage);
|
|
2263
|
+
}
|
|
2264
|
+
if (event.kind === 'token_usage') {
|
|
2265
|
+
void postActivity({
|
|
2266
|
+
stage: 'outcome',
|
|
2267
|
+
outcomeId: run?.outcome?.outcomeId,
|
|
2268
|
+
source: event.source || 'provider',
|
|
2269
|
+
meaningful: false,
|
|
2270
|
+
kind: 'token_usage',
|
|
2271
|
+
provider: definition.id,
|
|
2272
|
+
tokenUsage: event.tokenUsage || undefined,
|
|
2273
|
+
occurredAt: event.occurredAt || new Date().toISOString(),
|
|
2274
|
+
});
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
const meaningful =
|
|
2278
|
+
Boolean(event.message || event.name) ||
|
|
2279
|
+
event.kind === 'turn_started';
|
|
2280
|
+
if (!meaningful) return;
|
|
2281
|
+
const userFacingUpdate =
|
|
2282
|
+
event.kind === 'status_update'
|
|
2283
|
+
&& event.source === 'harness';
|
|
2284
|
+
void postActivity({
|
|
2285
|
+
stage: 'outcome',
|
|
2286
|
+
outcomeId: run?.outcome?.outcomeId,
|
|
2287
|
+
source: event.source || 'provider',
|
|
2288
|
+
meaningful: true,
|
|
2289
|
+
kind: userFacingUpdate ? 'output_delta' : event.kind || 'output_delta',
|
|
2290
|
+
provider: definition.id,
|
|
2291
|
+
tool: event.name || undefined,
|
|
2292
|
+
toolPhase: event.phase || undefined,
|
|
2293
|
+
activity:
|
|
2294
|
+
event.activity ||
|
|
2295
|
+
(userFacingUpdate ? `assistant-progress:${Date.now()}` : undefined),
|
|
2296
|
+
paths: event.paths || undefined,
|
|
2297
|
+
tokenUsage: event.tokenUsage || undefined,
|
|
2298
|
+
occurredAt: event.occurredAt || new Date().toISOString(),
|
|
2299
|
+
message:
|
|
2300
|
+
event.message ||
|
|
2301
|
+
(event.name
|
|
2302
|
+
? `${definition.label} is using ${event.name}.`
|
|
2303
|
+
: `${definition.label} started implementing your request.`),
|
|
2304
|
+
});
|
|
2305
|
+
};
|
|
2306
|
+
const monitorControl = async () => {
|
|
2307
|
+
while (!finished && !controlMonitorAbort.signal.aborted) {
|
|
2308
|
+
try {
|
|
2309
|
+
await waitForControl(
|
|
2310
|
+
Math.max(500, Number(options.controlPollMs) || 5_000),
|
|
2311
|
+
controlMonitorAbort.signal,
|
|
2312
|
+
);
|
|
2313
|
+
if (finished || controlMonitorAbort.signal.aborted) return;
|
|
2314
|
+
const response = await send('activity', {
|
|
2315
|
+
stage: 'outcome',
|
|
2316
|
+
outcomeId: run?.outcome?.outcomeId,
|
|
2317
|
+
source: 'bridge',
|
|
2318
|
+
meaningful: false,
|
|
2319
|
+
kind: 'heartbeat',
|
|
2320
|
+
provider: definition.id,
|
|
2321
|
+
occurredAt: new Date().toISOString(),
|
|
2322
|
+
message: `${definition.label} is still working.`,
|
|
2323
|
+
});
|
|
2324
|
+
if (controlRequestsCancellation(response)) {
|
|
2325
|
+
await requestCancel(controlCancellationSource(response));
|
|
2326
|
+
}
|
|
2327
|
+
} catch (error) {
|
|
2328
|
+
if (!controlMonitorAbort.signal.aborted && !cancelRequested) {
|
|
2329
|
+
options.trace?.warn?.('outcome_control_poll_failed', {
|
|
2330
|
+
error: errorMeta(error),
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
const controlMonitor = monitorControl();
|
|
2337
|
+
const rememberedSession =
|
|
2338
|
+
rememberedCompanionSession(sessionKey) ||
|
|
2339
|
+
(run?.outcome?.session?.providerSessionId
|
|
2340
|
+
? {
|
|
2341
|
+
providerSessionId: run.outcome.session.providerSessionId,
|
|
2342
|
+
agent: definition.id,
|
|
2343
|
+
updatedAt: Date.now(),
|
|
2344
|
+
}
|
|
2345
|
+
: null);
|
|
2346
|
+
const framerAgentOutcome =
|
|
2347
|
+
run?.outcome?.kind === 'framer-project'
|
|
2348
|
+
|| run?.outcome?.context?.executionMode === 'framer-agent';
|
|
2349
|
+
const materialized = materializeOutcomeWorkspace(run?.outcome, {
|
|
2350
|
+
sessionId: run?.outcome?.context?.harnessSessionId || sessionKey,
|
|
2351
|
+
});
|
|
2352
|
+
const nativeSkills = normalizeNativeSkills(run?.outcome?.nativeSkills);
|
|
2353
|
+
const nativeProvider =
|
|
2354
|
+
definition.id === 'claude-code'
|
|
2355
|
+
? 'claude-code'
|
|
2356
|
+
: definition.id === 'opencode' ||
|
|
2357
|
+
options.providerAdapter?.driverKind === 'opencode'
|
|
2358
|
+
? 'opencode'
|
|
2359
|
+
: options.providerAdapter?.id === 'codex' || definition.id === 'codex'
|
|
2360
|
+
? 'codex'
|
|
2361
|
+
: null;
|
|
2362
|
+
const materializedNativeSkills =
|
|
2363
|
+
nativeProvider === 'claude-code' || nativeProvider === 'opencode'
|
|
2364
|
+
? materializeNativeSkills(nativeSkills, {
|
|
2365
|
+
provider: nativeProvider,
|
|
2366
|
+
sessionId: run?.outcome?.context?.harnessSessionId || sessionKey,
|
|
2367
|
+
projectDirectory: materialized.directory,
|
|
2368
|
+
})
|
|
2369
|
+
: null;
|
|
2370
|
+
const harnessTools = framerAgentOutcome
|
|
2371
|
+
? createFramerAgentToolRuntime({
|
|
2372
|
+
assignment: run?.outcome,
|
|
2373
|
+
cwd: materialized.directory,
|
|
2374
|
+
env: options.env,
|
|
2375
|
+
trace: options.trace,
|
|
2376
|
+
onActivity: progress,
|
|
2377
|
+
runCli: options.framerAgentRunCli,
|
|
2378
|
+
authorizeProject:
|
|
2379
|
+
options.apiBaseUrl && options.deviceToken
|
|
2380
|
+
? ({ initiate, forceRefresh, signal }) =>
|
|
2381
|
+
getFramerProjectAuthorization(options.apiBaseUrl, {
|
|
2382
|
+
deviceToken: options.deviceToken,
|
|
2383
|
+
runId: run.runId,
|
|
2384
|
+
initiate,
|
|
2385
|
+
forceRefresh,
|
|
2386
|
+
fetchImpl: options.fetchImpl,
|
|
2387
|
+
signal,
|
|
2388
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
2389
|
+
})
|
|
2390
|
+
: undefined,
|
|
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,
|
|
2414
|
+
})
|
|
2415
|
+
: createHarnessToolRuntime({
|
|
2416
|
+
assignment: run?.outcome,
|
|
2417
|
+
materialized,
|
|
2418
|
+
send,
|
|
2419
|
+
trace: options.trace,
|
|
2420
|
+
onActivity: progress,
|
|
2421
|
+
});
|
|
2422
|
+
let prompt = '';
|
|
2423
|
+
const schema = outcomeOutputSchema();
|
|
2424
|
+
assertOutcomeResponseContract(
|
|
2425
|
+
STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
2426
|
+
schema,
|
|
2427
|
+
);
|
|
2428
|
+
const callStartedAt = Date.now();
|
|
2429
|
+
const usageAccumulator = createCompanionUsageAccumulator(definition.id);
|
|
2430
|
+
let providerAttempts = 0;
|
|
2431
|
+
let result;
|
|
2432
|
+
try {
|
|
2433
|
+
if (framerAgentOutcome) {
|
|
2434
|
+
await Promise.race([harnessTools.preflight(), cancellation]);
|
|
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
|
+
}
|
|
2454
|
+
const gateway =
|
|
2455
|
+
definition.id === 'codex' ? null : await harnessTools.startGateway();
|
|
2456
|
+
const mcpServer = gateway
|
|
2457
|
+
? {
|
|
2458
|
+
command: process.execPath,
|
|
2459
|
+
args: [HARNESS_MCP_SERVER_PATH],
|
|
2460
|
+
cwd: materialized.directory,
|
|
2461
|
+
environment: harnessMcpProcessEnvironment(
|
|
2462
|
+
gateway,
|
|
2463
|
+
process.versions,
|
|
2464
|
+
harnessTools.toolNames,
|
|
2465
|
+
),
|
|
2466
|
+
}
|
|
2467
|
+
: null;
|
|
2468
|
+
const mcpConfig = mcpServer
|
|
2469
|
+
? {
|
|
2470
|
+
mcpServers: {
|
|
2471
|
+
instawebai: {
|
|
2472
|
+
command: mcpServer.command,
|
|
2473
|
+
args: mcpServer.args,
|
|
2474
|
+
env: mcpServer.environment,
|
|
2475
|
+
},
|
|
2476
|
+
},
|
|
2477
|
+
}
|
|
2478
|
+
: null;
|
|
2479
|
+
const statusResponse = await send('status', {
|
|
2480
|
+
stage: 'outcome',
|
|
2481
|
+
outcomeId: run?.outcome?.outcomeId,
|
|
2482
|
+
message: `${definition.label} is implementing ${run?.outcome?.title || 'the requested outcome'}.`,
|
|
2483
|
+
});
|
|
2484
|
+
if (controlRequestsCancellation(statusResponse)) {
|
|
2485
|
+
await options.providerAdapter?.cancel?.(sessionKey);
|
|
2486
|
+
throw new CompanionRunCancelledError(
|
|
2487
|
+
undefined,
|
|
2488
|
+
controlCancellationSource(statusResponse),
|
|
2489
|
+
);
|
|
2490
|
+
}
|
|
2491
|
+
const executeProviderOutcome = async (
|
|
2492
|
+
turnPrompt,
|
|
2493
|
+
resumeSessionIdOverride,
|
|
2494
|
+
step = 0,
|
|
2495
|
+
) => {
|
|
2496
|
+
if (options.providerAdapter?.runOutcome) {
|
|
2497
|
+
const providerResult = await options.providerAdapter.runOutcome({
|
|
2498
|
+
runId: run.runId,
|
|
2499
|
+
sessionId: sessionKey,
|
|
2500
|
+
resumeSessionId: resumeSessionIdOverride,
|
|
2501
|
+
prompt: turnPrompt,
|
|
2502
|
+
model: adapterInvocationModel(run, selectedModel, definition.id),
|
|
2503
|
+
outputSchema: schema,
|
|
2504
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
2505
|
+
timeoutMs: outcomeInactivityTimeoutMs,
|
|
2506
|
+
maxDurationMs: outcomeMaxDurationMs,
|
|
2507
|
+
cwd: materialized.directory,
|
|
2508
|
+
nativeSkills,
|
|
2509
|
+
framerHarness: framerAgentOutcome,
|
|
2510
|
+
harnessTools: {
|
|
2511
|
+
definitions: codexDynamicToolSpecs(harnessTools.definitions),
|
|
2512
|
+
invoke: harnessTools.invoke,
|
|
2513
|
+
...(mcpConfig ? { mcpConfig } : {}),
|
|
2514
|
+
...(mcpServer ? { mcpServer } : {}),
|
|
2515
|
+
},
|
|
2516
|
+
onProgress: progress,
|
|
2517
|
+
});
|
|
2518
|
+
providerAttempts += providerAttemptCount(providerResult);
|
|
2519
|
+
return providerResult;
|
|
2520
|
+
}
|
|
2521
|
+
const runtime =
|
|
2522
|
+
options.runtime
|
|
2523
|
+
|| (await resolveAgentRuntime(definition, selectedModel, {
|
|
2524
|
+
env: options.env,
|
|
2525
|
+
}));
|
|
2526
|
+
if (!runtime.ok) {
|
|
2527
|
+
throw new Error(runtime.error || `${definition.label} is not available.`);
|
|
2528
|
+
}
|
|
2529
|
+
const resumeSessionId =
|
|
2530
|
+
agentSessionResumeEnabled(definition, options.env)
|
|
2531
|
+
? resumeSessionIdOverride
|
|
2532
|
+
: undefined;
|
|
2533
|
+
let processResult;
|
|
2534
|
+
try {
|
|
2535
|
+
providerAttempts += 1;
|
|
2536
|
+
processResult = await callLocalJsonAgent(definition.id, turnPrompt, {
|
|
2537
|
+
...options,
|
|
2538
|
+
model: selectedModelId,
|
|
2539
|
+
timeoutMs: outcomeInactivityTimeoutMs,
|
|
2540
|
+
runtime,
|
|
2541
|
+
resumeSessionId,
|
|
2542
|
+
outputSchema: schema,
|
|
2543
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
2544
|
+
product,
|
|
2545
|
+
step,
|
|
2546
|
+
maxDurationMs: outcomeMaxDurationMs,
|
|
2547
|
+
cwd: materialized.directory,
|
|
2548
|
+
harnessMode: true,
|
|
2549
|
+
framerHarness: framerAgentOutcome,
|
|
2550
|
+
harnessToolNames: harnessTools.toolNames,
|
|
2551
|
+
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2552
|
+
signal: infrastructureAbortController.signal,
|
|
2553
|
+
...(mcpConfig ? { mcpConfig } : {}),
|
|
2554
|
+
onProgress: progress,
|
|
2555
|
+
});
|
|
2556
|
+
} catch (error) {
|
|
2557
|
+
if (!resumeSessionId || !resumeFailure(error)) throw error;
|
|
2558
|
+
providerAttempts += 1;
|
|
2559
|
+
processResult = await callLocalJsonAgent(definition.id, turnPrompt, {
|
|
2560
|
+
...options,
|
|
2561
|
+
model: selectedModelId,
|
|
2562
|
+
timeoutMs: outcomeInactivityTimeoutMs,
|
|
2563
|
+
runtime,
|
|
2564
|
+
outputSchema: schema,
|
|
2565
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
2566
|
+
product,
|
|
2567
|
+
step,
|
|
2568
|
+
maxDurationMs: outcomeMaxDurationMs,
|
|
2569
|
+
cwd: materialized.directory,
|
|
2570
|
+
harnessMode: true,
|
|
2571
|
+
framerHarness: framerAgentOutcome,
|
|
2572
|
+
harnessToolNames: harnessTools.toolNames,
|
|
2573
|
+
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2574
|
+
signal: infrastructureAbortController.signal,
|
|
2575
|
+
...(mcpConfig ? { mcpConfig } : {}),
|
|
2576
|
+
onProgress: progress,
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
const parsedOutput = parseAgentOutput(definition.id, processResult.stdout);
|
|
2580
|
+
return {
|
|
2581
|
+
...parsedOutput,
|
|
2582
|
+
text: parsedOutput.resultText,
|
|
2583
|
+
sessionId: parsedOutput.sessionId,
|
|
2584
|
+
};
|
|
2585
|
+
};
|
|
2586
|
+
const providerExecution = executeProviderOutcome(
|
|
2587
|
+
prompt,
|
|
2588
|
+
rememberedSession?.providerSessionId,
|
|
2589
|
+
);
|
|
2590
|
+
const infrastructureFailure = harnessTools
|
|
2591
|
+
.waitForFatalInfrastructure()
|
|
2592
|
+
.then(async (error) => {
|
|
2593
|
+
options.trace?.error?.('harness_infrastructure_failed', {
|
|
2594
|
+
code: error?.code,
|
|
2595
|
+
message: error?.message,
|
|
2596
|
+
});
|
|
2597
|
+
infrastructureAbortController.abort(error);
|
|
2598
|
+
await Promise.resolve(
|
|
2599
|
+
options.providerAdapter?.cancel?.(sessionKey),
|
|
2600
|
+
).catch(() => undefined);
|
|
2601
|
+
throw error;
|
|
2602
|
+
});
|
|
2603
|
+
try {
|
|
2604
|
+
result = await Promise.race([
|
|
2605
|
+
providerExecution,
|
|
2606
|
+
infrastructureFailure,
|
|
2607
|
+
cancellation,
|
|
2608
|
+
]);
|
|
2609
|
+
await Promise.race([activityChain, cancellation]);
|
|
2610
|
+
} catch (error) {
|
|
2611
|
+
if (cancelRequested) throw cancellationError();
|
|
2612
|
+
throw error;
|
|
2613
|
+
}
|
|
2614
|
+
let providerSessionId = result?.threadId || result?.sessionId;
|
|
2615
|
+
if (providerSessionId || options.providerAdapter) {
|
|
2616
|
+
rememberCompanionSession(sessionKey, {
|
|
2617
|
+
providerSessionId:
|
|
2618
|
+
providerSessionId ||
|
|
2619
|
+
rememberedSession?.providerSessionId ||
|
|
2620
|
+
sessionKey,
|
|
2621
|
+
agent: definition.id,
|
|
2622
|
+
updatedAt: Date.now(),
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
usageAccumulator.add(result);
|
|
2626
|
+
let rawCompletion = normalizeOutcomeCompletion(
|
|
2627
|
+
extractJsonObject(result?.text || result?.resultText || ''),
|
|
2628
|
+
run?.outcome?.outcomeId,
|
|
2629
|
+
);
|
|
2630
|
+
let synchronization = null;
|
|
2631
|
+
if (harnessTools.directWorkspace) {
|
|
2632
|
+
synchronization = await harnessTools.inspect();
|
|
2633
|
+
} else if (
|
|
2634
|
+
rawCompletion.status === 'completed'
|
|
2635
|
+
|| harnessTools.hasPendingChanges()
|
|
2636
|
+
) {
|
|
2637
|
+
synchronization = await harnessTools.synchronize();
|
|
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;
|
|
2696
|
+
const files = harnessTools.directWorkspace
|
|
2697
|
+
? []
|
|
2698
|
+
: collectOutcomeWorkspaceChanges(run.outcome, materialized);
|
|
2699
|
+
const normalized = synchronization
|
|
2700
|
+
? {
|
|
2701
|
+
...completion,
|
|
2702
|
+
synchronized: synchronization.synchronized !== false,
|
|
2703
|
+
sourceHash:
|
|
2704
|
+
synchronization?.sourceHash || harnessTools.sourceHash() || null,
|
|
2705
|
+
...(typeof harnessTools.summary === 'function'
|
|
2706
|
+
? { framer: harnessTools.summary() }
|
|
2707
|
+
: {}),
|
|
2708
|
+
}
|
|
2709
|
+
: completion;
|
|
2710
|
+
const usageSnapshot = usageAccumulator.snapshot();
|
|
2711
|
+
await send('done', {
|
|
2712
|
+
operationType: 'outcome',
|
|
2713
|
+
outcome: {
|
|
2714
|
+
...normalized,
|
|
2715
|
+
files,
|
|
2716
|
+
},
|
|
2717
|
+
model: selectedModelId,
|
|
2718
|
+
providerSessionId:
|
|
2719
|
+
providerSessionId ||
|
|
2720
|
+
rememberedSession?.providerSessionId ||
|
|
2721
|
+
sessionKey,
|
|
2722
|
+
...usageSnapshot,
|
|
2723
|
+
modelCalls: [
|
|
2724
|
+
{
|
|
2725
|
+
callId: run.runId,
|
|
2726
|
+
outcomeId: run?.outcome?.outcomeId,
|
|
2727
|
+
model: selectedModelId,
|
|
2728
|
+
status:
|
|
2729
|
+
normalized.status === 'completed'
|
|
2730
|
+
? 'succeeded'
|
|
2731
|
+
: normalized.status,
|
|
2732
|
+
durationMs: Date.now() - callStartedAt,
|
|
2733
|
+
providerAttemptCount: Math.max(1, providerAttempts),
|
|
2734
|
+
...usageSnapshot.tokenUsage,
|
|
2735
|
+
estimatedCostUSD: usageSnapshot.estimatedCostUSD,
|
|
2736
|
+
},
|
|
2737
|
+
],
|
|
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;
|
|
2755
|
+
} finally {
|
|
2756
|
+
finished = true;
|
|
2757
|
+
controlMonitorAbort.abort();
|
|
2758
|
+
await controlMonitor.catch(() => undefined);
|
|
2759
|
+
await harnessTools
|
|
2760
|
+
.close({ waitForPending: !cancelRequested })
|
|
2761
|
+
.catch(() => undefined);
|
|
2762
|
+
removeOutcomeWorkspace(materialized);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
export async function executeRun(
|
|
2767
|
+
run,
|
|
2768
|
+
{
|
|
2769
|
+
apiBaseUrl,
|
|
2770
|
+
deviceToken,
|
|
2771
|
+
agent = process.env.DEXTER_BRIDGE_AGENT || DEFAULT_BRIDGE_AGENT,
|
|
2772
|
+
product: configuredProduct,
|
|
2773
|
+
fetchImpl,
|
|
2774
|
+
log = console.log,
|
|
2775
|
+
logDir,
|
|
2776
|
+
selectedModel,
|
|
2777
|
+
providerAdapter,
|
|
2778
|
+
adapterOptions,
|
|
2779
|
+
env = process.env,
|
|
2780
|
+
controlPollMs,
|
|
2781
|
+
inspectAgentAuthentication,
|
|
2782
|
+
runtimeProfile: suppliedRuntimeProfile,
|
|
2783
|
+
framerAgentRunCli,
|
|
2784
|
+
allowInsecureHttp = false,
|
|
2785
|
+
} = {},
|
|
2786
|
+
) {
|
|
1592
2787
|
const product = normalizeBridgeProduct(run?.product || configuredProduct);
|
|
1593
2788
|
const productName = product.name;
|
|
1594
2789
|
if (!run?.runId) throw new Error('Companion run payload is missing runId.');
|
|
1595
|
-
if (
|
|
2790
|
+
if (!['dexter-companion-v4', 'dexter-companion-v5', 'dexter-companion-v6', 'dexter-companion-v7', 'dexter-companion-v8'].includes(run?.protocol?.version)) {
|
|
1596
2791
|
throw new Error(`Unsupported bridge protocol ${run.protocol.version}. Update the local bridge and reconnect.`);
|
|
1597
2792
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
2793
|
+
const modelTurnRun =
|
|
2794
|
+
run?.kind === 'model_turn' && run?.protocol?.mode === 'model_turn';
|
|
2795
|
+
const outcomeRun =
|
|
2796
|
+
run?.kind === 'outcome' && run?.protocol?.mode === 'outcome';
|
|
2797
|
+
if (!modelTurnRun && !outcomeRun) {
|
|
2798
|
+
throw new Error('The local bridge only accepts server-owned model_turn or outcome runs.');
|
|
1600
2799
|
}
|
|
1601
2800
|
const writeLine = (message) => {
|
|
1602
2801
|
if (typeof log !== 'function') return;
|
|
@@ -1604,18 +2803,33 @@ export async function executeRun(run, {
|
|
|
1604
2803
|
else log(message);
|
|
1605
2804
|
};
|
|
1606
2805
|
const trace = createRunLogger({ runId: run.runId, logDir, mirror: log });
|
|
1607
|
-
const send = createEventPoster({
|
|
1608
|
-
|
|
1609
|
-
|
|
2806
|
+
const send = createEventPoster({
|
|
2807
|
+
apiBaseUrl,
|
|
2808
|
+
deviceToken,
|
|
2809
|
+
run,
|
|
2810
|
+
fetchImpl,
|
|
2811
|
+
trace,
|
|
2812
|
+
allowInsecureHttp,
|
|
2813
|
+
});
|
|
2814
|
+
const runtimeProfile = run?.runtimeProfile || suppliedRuntimeProfile || null;
|
|
2815
|
+
const normalizedAgent = normalizeAgentName(
|
|
2816
|
+
run?.companion?.agent || (runtimeProfile?.driver === 'claude-code-cli' ? 'claude-code' : agent),
|
|
2817
|
+
);
|
|
2818
|
+
const runModel = runtimeProfile?.model?.id || run?.companion?.model?.id || run?.model || selectedModel;
|
|
1610
2819
|
const usageAccumulator = createCompanionUsageAccumulator(normalizedAgent);
|
|
1611
2820
|
const adapterProvided = providerAdapter !== undefined;
|
|
1612
|
-
const shareAdapter =
|
|
1613
|
-
|
|
2821
|
+
const shareAdapter =
|
|
2822
|
+
!adapterProvided &&
|
|
2823
|
+
(
|
|
2824
|
+
(modelTurnRun && deltaModelSessionsEnabled(env)) ||
|
|
2825
|
+
(outcomeRun && runtimeProfile?.capabilities?.sessionResume === true)
|
|
2826
|
+
);
|
|
2827
|
+
const sharedAdapterKey = `${product.id}:${runtimeProfile?.id || normalizedAgent}`;
|
|
1614
2828
|
let activeAdapter = adapterProvided ? providerAdapter : null;
|
|
1615
2829
|
if (!adapterProvided && shareAdapter) {
|
|
1616
2830
|
activeAdapter = sharedProviderAdapters.get(sharedAdapterKey) || null;
|
|
1617
2831
|
if (!activeAdapter) {
|
|
1618
|
-
activeAdapter = createLocalAgentAdapter(normalizedAgent, {
|
|
2832
|
+
activeAdapter = createLocalAgentAdapter(runtimeProfile || normalizedAgent, {
|
|
1619
2833
|
...adapterOptions,
|
|
1620
2834
|
env: adapterOptions?.env || env,
|
|
1621
2835
|
product,
|
|
@@ -1624,7 +2838,7 @@ export async function executeRun(run, {
|
|
|
1624
2838
|
if (activeAdapter) sharedProviderAdapters.set(sharedAdapterKey, activeAdapter);
|
|
1625
2839
|
}
|
|
1626
2840
|
} else if (!adapterProvided) {
|
|
1627
|
-
activeAdapter = createLocalAgentAdapter(normalizedAgent, {
|
|
2841
|
+
activeAdapter = createLocalAgentAdapter(runtimeProfile || normalizedAgent, {
|
|
1628
2842
|
...adapterOptions,
|
|
1629
2843
|
env: adapterOptions?.env || env,
|
|
1630
2844
|
product,
|
|
@@ -1632,10 +2846,11 @@ export async function executeRun(run, {
|
|
|
1632
2846
|
});
|
|
1633
2847
|
}
|
|
1634
2848
|
const ownsAdapter = !adapterProvided && !shareAdapter && Boolean(activeAdapter);
|
|
1635
|
-
if (
|
|
1636
|
-
activeAdapter?.
|
|
1637
|
-
|
|
1638
|
-
|
|
2849
|
+
if (activeAdapter?.profileId && runtimeProfile?.id && activeAdapter.profileId !== runtimeProfile.id) {
|
|
2850
|
+
if (ownsAdapter) activeAdapter.close?.();
|
|
2851
|
+
throw new Error(`Provider adapter ${activeAdapter.profileId} cannot execute runtime profile ${runtimeProfile.id}.`);
|
|
2852
|
+
}
|
|
2853
|
+
if (!runtimeProfile && activeAdapter?.id && activeAdapter.id !== normalizedAgent) {
|
|
1639
2854
|
if (ownsAdapter) activeAdapter.close?.();
|
|
1640
2855
|
throw new Error(`Provider adapter ${activeAdapter.id} cannot execute ${normalizedAgent} runs.`);
|
|
1641
2856
|
}
|
|
@@ -1654,12 +2869,13 @@ export async function executeRun(run, {
|
|
|
1654
2869
|
if (!activeAdapter && normalizedAgent !== 'dry-run') {
|
|
1655
2870
|
const definition = definitionForAgent(normalizedAgent);
|
|
1656
2871
|
const selectedModelDefinition = companionModelDefinition(runModel, normalizedAgent);
|
|
1657
|
-
runtime = await resolveAgentRuntime(definition, selectedModelDefinition, {
|
|
2872
|
+
runtime = await resolveAgentRuntime(definition, selectedModelDefinition, {
|
|
2873
|
+
env,
|
|
2874
|
+
});
|
|
1658
2875
|
if (!runtime.ok) {
|
|
1659
|
-
throw Object.assign(
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
);
|
|
2876
|
+
throw Object.assign(new Error(runtime.error || `${definition.label} is not available.`), {
|
|
2877
|
+
code: runtime.code,
|
|
2878
|
+
});
|
|
1663
2879
|
}
|
|
1664
2880
|
const authentication = await checkAgentAuthentication(normalizedAgent, runtime, {
|
|
1665
2881
|
env,
|
|
@@ -1672,7 +2888,7 @@ export async function executeRun(run, {
|
|
|
1672
2888
|
});
|
|
1673
2889
|
}
|
|
1674
2890
|
}
|
|
1675
|
-
|
|
2891
|
+
const executionOptions = {
|
|
1676
2892
|
model: runModel || selectedModel,
|
|
1677
2893
|
selectedModel: runModel || selectedModel,
|
|
1678
2894
|
providerAdapter: activeAdapter,
|
|
@@ -1681,17 +2897,36 @@ export async function executeRun(run, {
|
|
|
1681
2897
|
env,
|
|
1682
2898
|
controlPollMs,
|
|
1683
2899
|
product,
|
|
2900
|
+
runtimeProfile,
|
|
2901
|
+
framerAgentRunCli,
|
|
2902
|
+
apiBaseUrl,
|
|
2903
|
+
deviceToken,
|
|
2904
|
+
fetchImpl,
|
|
2905
|
+
allowInsecureHttp,
|
|
2906
|
+
};
|
|
2907
|
+
if (outcomeRun) {
|
|
2908
|
+
await executeOutcomeRun(run, send, normalizedAgent, executionOptions);
|
|
2909
|
+
} else {
|
|
2910
|
+
await executeModelTurnRun(run, send, normalizedAgent, executionOptions);
|
|
2911
|
+
}
|
|
2912
|
+
trace.info('run_done', {
|
|
2913
|
+
status: outcomeRun ? 'outcome_sent' : 'model_turn_sent',
|
|
1684
2914
|
});
|
|
1685
|
-
trace.info('run_done', { status: 'model_turn_sent' });
|
|
1686
2915
|
return { cancelled: false };
|
|
1687
2916
|
} catch (error) {
|
|
1688
2917
|
if (error?.code === 'RUN_CANCELLED') {
|
|
1689
|
-
|
|
2918
|
+
const message = cancellationMessage(error);
|
|
2919
|
+
trace.info('run_cancelled', {
|
|
2920
|
+
message,
|
|
2921
|
+
cancellationSource: error?.cancellationSource,
|
|
2922
|
+
});
|
|
1690
2923
|
await send('error', {
|
|
1691
2924
|
operationType: 'cancelled',
|
|
1692
2925
|
outcome: 'cancelled',
|
|
1693
|
-
message
|
|
2926
|
+
message,
|
|
1694
2927
|
code: 'RUN_CANCELLED',
|
|
2928
|
+
cancellationSource: error?.cancellationSource || 'unknown',
|
|
2929
|
+
...(error?.companionUsage || usageAccumulator.snapshot()),
|
|
1695
2930
|
}).catch(() => undefined);
|
|
1696
2931
|
return { cancelled: true };
|
|
1697
2932
|
}
|
|
@@ -1700,6 +2935,7 @@ export async function executeRun(run, {
|
|
|
1700
2935
|
stage: 'agent',
|
|
1701
2936
|
message: error?.message || 'Local bridge failed.',
|
|
1702
2937
|
code: error?.code,
|
|
2938
|
+
retryable: error?.retryable === true,
|
|
1703
2939
|
...(error?.companionUsage || usageAccumulator.snapshot()),
|
|
1704
2940
|
}).catch(() => undefined);
|
|
1705
2941
|
throw error;
|
|
@@ -1716,7 +2952,11 @@ export function __resetAgentSessionsForTests() {
|
|
|
1716
2952
|
|
|
1717
2953
|
export function checkCommand(command, args = ['--version'], timeoutMs = 10000) {
|
|
1718
2954
|
return runProcess(command, args, '', { timeoutMs })
|
|
1719
|
-
.then((result) => ({
|
|
2955
|
+
.then((result) => ({
|
|
2956
|
+
ok: true,
|
|
2957
|
+
command,
|
|
2958
|
+
output: (result.stdout || result.stderr || '').trim(),
|
|
2959
|
+
}))
|
|
1720
2960
|
.catch((error) => ({ ok: false, command, error: error.message }));
|
|
1721
2961
|
}
|
|
1722
2962
|
|
|
@@ -1743,8 +2983,9 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
1743
2983
|
existsSync: options.existsSync,
|
|
1744
2984
|
inspect: options.inspectRuntime,
|
|
1745
2985
|
});
|
|
1746
|
-
const supportedModels = companionModelsForAgent(normalizedAgent)
|
|
1747
|
-
|
|
2986
|
+
const supportedModels = companionModelsForAgent(normalizedAgent).filter((candidate) =>
|
|
2987
|
+
modelSupportsAgentVersion(candidate, runtime.version),
|
|
2988
|
+
);
|
|
1748
2989
|
if (!runtime.ok) {
|
|
1749
2990
|
return {
|
|
1750
2991
|
...runtime,
|
|
@@ -1759,8 +3000,8 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
1759
3000
|
}
|
|
1760
3001
|
if (normalizedAgent === 'codex') {
|
|
1761
3002
|
const adapter =
|
|
1762
|
-
options.providerAdapter
|
|
1763
|
-
createLocalAgentAdapter('codex', {
|
|
3003
|
+
options.providerAdapter
|
|
3004
|
+
|| createLocalAgentAdapter('codex', {
|
|
1764
3005
|
env: options.env,
|
|
1765
3006
|
});
|
|
1766
3007
|
try {
|
|
@@ -1772,15 +3013,15 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
1772
3013
|
ok: false,
|
|
1773
3014
|
agent: definition.id,
|
|
1774
3015
|
label: definition.label,
|
|
1775
|
-
models:
|
|
1776
|
-
modelDetails:
|
|
3016
|
+
models: supportedModels.map((candidate) => candidate.id),
|
|
3017
|
+
modelDetails: supportedModels,
|
|
1777
3018
|
installed: detection.installed !== false,
|
|
1778
3019
|
signedIn: false,
|
|
1779
3020
|
status: detection.installed === false ? 'unavailable' : 'authentication_required',
|
|
1780
3021
|
code: detection.installed === false ? 'DEXTER_AGENT_NOT_FOUND' : AGENT_AUTHENTICATION_REQUIRED_CODE,
|
|
1781
3022
|
error:
|
|
1782
|
-
detection.error
|
|
1783
|
-
'Codex is not signed in. Run `codex login` on this computer, then run the bridge command again.',
|
|
3023
|
+
detection.error
|
|
3024
|
+
|| 'Codex is not signed in. Run `codex login` on this computer, then run the bridge command again.',
|
|
1784
3025
|
};
|
|
1785
3026
|
}
|
|
1786
3027
|
const reportedModels = await adapter.models();
|