@gakim-digital/dexter-bridge 0.5.20 → 0.11.0
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 +128 -18
- package/package.json +19 -5
- package/src/agent.js +1500 -335
- package/src/agentOutput.js +209 -0
- package/src/api.js +49 -2
- package/src/cli.js +310 -54
- package/src/config.js +82 -15
- package/src/framerAgentTools.js +1108 -0
- package/src/harnessMcpServer.js +240 -0
- package/src/harnessTools.js +548 -0
- package/src/logger.js +1 -1
- package/src/nativeSkills.js +295 -0
- package/src/outcomeWorkspace.js +351 -0
- package/src/protocol.js +257 -10
- package/src/providers/acp.js +241 -0
- package/src/providers/codexAppServer.js +1074 -170
- 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,41 +2,87 @@ 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 { postRunEvent } from './api.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getFramerProjectAuthorization, postRunEvent } from './api.js';
|
|
8
|
+
import {
|
|
9
|
+
createCompanionUsageAccumulator,
|
|
10
|
+
createClaudeProgressParser,
|
|
11
|
+
normalizeCompanionTokenUsage,
|
|
12
|
+
parseAgentOutput,
|
|
13
|
+
} from './agentOutput.js';
|
|
8
14
|
import {
|
|
9
15
|
DEFAULT_BRIDGE_AGENT,
|
|
10
16
|
companionModelDefinition,
|
|
11
17
|
companionModelsForAgent,
|
|
18
|
+
normalizeBridgeProduct,
|
|
12
19
|
normalizeAgentName,
|
|
13
20
|
normalizeCompanionModelName,
|
|
14
21
|
} from './config.js';
|
|
15
22
|
import {
|
|
23
|
+
assertOutcomeResponseContract,
|
|
24
|
+
buildOutcomePrompt,
|
|
16
25
|
buildModelTurnDeltaPrompt,
|
|
17
26
|
buildModelTurnFallbackPrompt,
|
|
18
27
|
buildModelTurnPrompt,
|
|
19
28
|
extractJsonObject,
|
|
20
29
|
modelTurnOutputSchema,
|
|
21
30
|
normalizeModelTurnCompletion,
|
|
31
|
+
normalizeOutcomeCompletion,
|
|
32
|
+
normalizeStructuredResponseContract,
|
|
33
|
+
outcomeOutputSchema,
|
|
22
34
|
runSummary,
|
|
35
|
+
STRUCTURED_RESPONSE_CONTRACTS,
|
|
23
36
|
} from './protocol.js';
|
|
37
|
+
import { clip, createRunLogger, errorMeta, logLocationHint, summarizePayload, summarizeToolResult } from './logger.js';
|
|
24
38
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
summarizeToolResult,
|
|
31
|
-
} from './logger.js';
|
|
39
|
+
claudeNativeSkillPrompt,
|
|
40
|
+
materializeNativeSkills,
|
|
41
|
+
nativeSkillLifecycle,
|
|
42
|
+
normalizeNativeSkills,
|
|
43
|
+
} from './nativeSkills.js';
|
|
32
44
|
import { createLocalAgentAdapter } from './providers/index.js';
|
|
45
|
+
import {
|
|
46
|
+
collectOutcomeWorkspaceChanges,
|
|
47
|
+
materializeOutcomeWorkspace,
|
|
48
|
+
removeOutcomeWorkspace,
|
|
49
|
+
} from './outcomeWorkspace.js';
|
|
50
|
+
import {
|
|
51
|
+
codexDynamicToolSpecs,
|
|
52
|
+
createHarnessToolRuntime,
|
|
53
|
+
} from './harnessTools.js';
|
|
54
|
+
import { createFramerAgentToolRuntime } from './framerAgentTools.js';
|
|
55
|
+
|
|
56
|
+
const HARNESS_MCP_SERVER_PATH = fileURLToPath(
|
|
57
|
+
new URL('./harnessMcpServer.js', import.meta.url),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export function harnessMcpProcessEnvironment(
|
|
61
|
+
gateway,
|
|
62
|
+
versions = process.versions,
|
|
63
|
+
allowedToolNames,
|
|
64
|
+
) {
|
|
65
|
+
return {
|
|
66
|
+
INSTAWEB_HARNESS_GATEWAY_URL: gateway.url,
|
|
67
|
+
INSTAWEB_HARNESS_GATEWAY_TOKEN: gateway.token,
|
|
68
|
+
...(Array.isArray(allowedToolNames)
|
|
69
|
+
? {
|
|
70
|
+
INSTAWEB_HARNESS_ALLOWED_TOOLS:
|
|
71
|
+
JSON.stringify(allowedToolNames),
|
|
72
|
+
}
|
|
73
|
+
: {}),
|
|
74
|
+
...(versions?.electron ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
33
77
|
|
|
34
78
|
const companionModelSessions = new Map();
|
|
35
79
|
const sharedProviderAdapters = new Map();
|
|
36
80
|
const MAX_COMPANION_MODEL_SESSIONS = 64;
|
|
37
81
|
|
|
38
82
|
function enabledFlag(value, fallback = false) {
|
|
39
|
-
const raw = String(value ?? '')
|
|
83
|
+
const raw = String(value ?? '')
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase();
|
|
40
86
|
if (!raw) return fallback;
|
|
41
87
|
return !['0', 'false', 'no', 'off', 'disabled'].includes(raw);
|
|
42
88
|
}
|
|
@@ -46,7 +92,9 @@ function deltaModelSessionsEnabled(env = process.env) {
|
|
|
46
92
|
}
|
|
47
93
|
|
|
48
94
|
function companionSessionKey(run) {
|
|
49
|
-
return run?.
|
|
95
|
+
return run?.outcome?.session?.goalSessionId
|
|
96
|
+
|| run?.outcome?.session?.sessionId
|
|
97
|
+
|| run?.modelTurn?.session?.goalSessionId
|
|
50
98
|
|| run?.modelTurn?.session?.sessionId
|
|
51
99
|
|| run?.turnId
|
|
52
100
|
|| run?.runId;
|
|
@@ -76,6 +124,22 @@ function resumeFailure(error) {
|
|
|
76
124
|
return /resume|session|thread|conversation|not found|unknown id|expired/i.test(message);
|
|
77
125
|
}
|
|
78
126
|
|
|
127
|
+
function freshSessionRetryFailure(error, adapter) {
|
|
128
|
+
return adapter?.id === 'codex' && [
|
|
129
|
+
'APP_AGENT_TURN_INACTIVITY_TIMEOUT',
|
|
130
|
+
'CODEX_STRUCTURED_OUTPUT_MALFORMED',
|
|
131
|
+
'CODEX_STRUCTURED_OUTPUT_INVALID',
|
|
132
|
+
'CODEX_TOOL_ARGUMENTS_TRANSPORT_INVALID',
|
|
133
|
+
'CODEX_TOOL_ARGUMENTS_MALFORMED',
|
|
134
|
+
'CODEX_TOOL_ARGUMENTS_NOT_OBJECT',
|
|
135
|
+
].includes(error?.code);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function providerAttemptCount(value) {
|
|
139
|
+
const direct = Number(value?.providerAttemptCount);
|
|
140
|
+
return Number.isFinite(direct) && direct > 0 ? Math.floor(direct) : 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
79
143
|
export const AGENT_DEFINITIONS = {
|
|
80
144
|
'claude-code': {
|
|
81
145
|
id: 'claude-code',
|
|
@@ -109,6 +173,7 @@ function nowIso() {
|
|
|
109
173
|
|
|
110
174
|
const TERMINAL_EVENT_TYPES = new Set(['done', 'error']);
|
|
111
175
|
const TERMINAL_EVENT_MAX_ATTEMPTS = 5;
|
|
176
|
+
const TOOL_EVENT_MAX_ATTEMPTS = 3;
|
|
112
177
|
|
|
113
178
|
function waitForRetry(delayMs) {
|
|
114
179
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
@@ -116,25 +181,30 @@ function waitForRetry(delayMs) {
|
|
|
116
181
|
|
|
117
182
|
function terminalEventRetryDelay(error, attempt) {
|
|
118
183
|
const status = Number(error?.status);
|
|
119
|
-
const retryable =
|
|
120
|
-
!Number.isFinite(status)
|
|
121
|
-
|| status === 408
|
|
122
|
-
|| status === 429
|
|
123
|
-
|| status >= 500;
|
|
184
|
+
const retryable = !Number.isFinite(status) || status === 408 || status === 429 || status >= 500;
|
|
124
185
|
if (!retryable) return null;
|
|
125
186
|
if (Number.isFinite(error?.retryAfterMs) && error.retryAfterMs > 0) {
|
|
126
187
|
return Math.min(65_000, Math.max(250, error.retryAfterMs));
|
|
127
188
|
}
|
|
128
|
-
return Math.min(8_000, 500 *
|
|
189
|
+
return Math.min(8_000, 500 * 2 ** (attempt - 1));
|
|
129
190
|
}
|
|
130
191
|
|
|
131
|
-
function createEventPoster({
|
|
192
|
+
function createEventPoster({
|
|
193
|
+
apiBaseUrl,
|
|
194
|
+
deviceToken,
|
|
195
|
+
run,
|
|
196
|
+
fetchImpl,
|
|
197
|
+
trace,
|
|
198
|
+
allowInsecureHttp = false,
|
|
199
|
+
}) {
|
|
132
200
|
return async function send(type, payload = {}) {
|
|
133
201
|
const eventId = `${type}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
134
202
|
const started = Date.now();
|
|
135
203
|
const maxAttempts = TERMINAL_EVENT_TYPES.has(type)
|
|
136
204
|
? TERMINAL_EVENT_MAX_ATTEMPTS
|
|
137
|
-
:
|
|
205
|
+
: type === 'tool_call'
|
|
206
|
+
? TOOL_EVENT_MAX_ATTEMPTS
|
|
207
|
+
: 1;
|
|
138
208
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
139
209
|
trace?.info('event_post_start', {
|
|
140
210
|
type,
|
|
@@ -147,6 +217,7 @@ function createEventPoster({ apiBaseUrl, deviceToken, run, fetchImpl, trace }) {
|
|
|
147
217
|
deviceToken,
|
|
148
218
|
runId: run.runId,
|
|
149
219
|
fetchImpl,
|
|
220
|
+
allowInsecureHttp,
|
|
150
221
|
event: {
|
|
151
222
|
type,
|
|
152
223
|
eventId,
|
|
@@ -165,10 +236,7 @@ function createEventPoster({ apiBaseUrl, deviceToken, run, fetchImpl, trace }) {
|
|
|
165
236
|
});
|
|
166
237
|
return response;
|
|
167
238
|
} catch (error) {
|
|
168
|
-
const retryDelayMs =
|
|
169
|
-
attempt < maxAttempts
|
|
170
|
-
? terminalEventRetryDelay(error, attempt)
|
|
171
|
-
: null;
|
|
239
|
+
const retryDelayMs = attempt < maxAttempts ? terminalEventRetryDelay(error, attempt) : null;
|
|
172
240
|
trace?.error('event_post_failed', {
|
|
173
241
|
type,
|
|
174
242
|
eventId,
|
|
@@ -192,7 +260,10 @@ function commandFromEnv(name, fallback, env = process.env) {
|
|
|
192
260
|
function parseExtraArgs(value, fallback = '') {
|
|
193
261
|
const source = value === undefined || value === null || value === '' ? fallback : value;
|
|
194
262
|
if (!source) return [];
|
|
195
|
-
return String(source)
|
|
263
|
+
return String(source)
|
|
264
|
+
.split(/\s+/)
|
|
265
|
+
.map((item) => item.trim())
|
|
266
|
+
.filter(Boolean);
|
|
196
267
|
}
|
|
197
268
|
|
|
198
269
|
function argsAlreadySelectModel(args) {
|
|
@@ -204,7 +275,11 @@ function argsIncludeFlag(args, flag) {
|
|
|
204
275
|
}
|
|
205
276
|
|
|
206
277
|
function structuredUsageEnabled(env = process.env) {
|
|
207
|
-
return
|
|
278
|
+
return (
|
|
279
|
+
String(env.DEXTER_BRIDGE_STRUCTURED_USAGE || 'true')
|
|
280
|
+
.trim()
|
|
281
|
+
.toLowerCase() !== 'false'
|
|
282
|
+
);
|
|
208
283
|
}
|
|
209
284
|
|
|
210
285
|
function argsWithRequiredAgentFlags(args, definition) {
|
|
@@ -220,15 +295,44 @@ const CLAUDE_CLI_MODEL_IDS = {
|
|
|
220
295
|
haiku: 'claude-haiku-4-5-20251001',
|
|
221
296
|
};
|
|
222
297
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
298
|
+
function claudeModelEngineSystemPrompt(product) {
|
|
299
|
+
const name = normalizeBridgeProduct(product).name;
|
|
300
|
+
return [
|
|
301
|
+
`You are a model-only completion engine embedded inside ${name}.`,
|
|
302
|
+
`The user prompt contains ${name} messages and a catalog of remote tools as data.`,
|
|
303
|
+
'Never execute, simulate, or emit native Claude Code tool calls for those tool names.',
|
|
304
|
+
'The only allowed tool is StructuredOutput, supplied by the JSON schema.',
|
|
305
|
+
`Encode requested ${name} actions only inside StructuredOutput.toolCalls.`,
|
|
306
|
+
'Use only the explicitly invoked InstaWebAI native skill package. Do not discover or invoke any other skills.',
|
|
307
|
+
'Do not inspect the filesystem, project, shell, plugins, MCP servers, or browser.',
|
|
308
|
+
`Be concise: reason only as much as needed to choose the next remote ${name} action.`,
|
|
309
|
+
].join(' ');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function claudeOutcomeSystemPrompt(product) {
|
|
313
|
+
const name = normalizeBridgeProduct(product).name;
|
|
314
|
+
if (normalizeBridgeProduct(product).id === 'dexter') {
|
|
315
|
+
return [
|
|
316
|
+
`You are an autonomous Framer harness embedded inside ${name}.`,
|
|
317
|
+
'Work only through the approved Framer Agent tools connected to the user-selected project.',
|
|
318
|
+
'Inspect the project, perform the requested work, verify the result, and return the structured outcome.',
|
|
319
|
+
'Do not access local credentials, unrelated files, other projects, account settings, or billing.',
|
|
320
|
+
'Do not publish unless the assignment explicitly authorizes publishing.',
|
|
321
|
+
'Call progress_update before the first inspection or edit and at meaningful phase changes.',
|
|
322
|
+
`Return only the structured outcome result requested by ${name}.`,
|
|
323
|
+
].join(' ');
|
|
324
|
+
}
|
|
325
|
+
return [
|
|
326
|
+
`You are a coding harness embedded inside ${name}.`,
|
|
327
|
+
'Work only inside the current isolated workspace and through the approved InstaWebAI harness tools.',
|
|
328
|
+
'Read and edit the files needed to complete the entire assigned outcome.',
|
|
329
|
+
'Run the commands, preview tools, and browser checks needed to build and verify the app without asking for approval.',
|
|
330
|
+
'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.',
|
|
331
|
+
'Do not access parent directories, environment files, credentials, unrelated user files, plugins, or the external network.',
|
|
332
|
+
'Prioritize working behavior and contract correctness before visual polish.',
|
|
333
|
+
`Return only the structured outcome result requested by ${name}.`,
|
|
334
|
+
].join(' ');
|
|
335
|
+
}
|
|
232
336
|
|
|
233
337
|
export function mapClaudeCliModelId(value) {
|
|
234
338
|
const raw = String(value || '').trim();
|
|
@@ -238,20 +342,17 @@ export function mapClaudeCliModelId(value) {
|
|
|
238
342
|
function argsWithSelectedModel(args, modelDefinition, definition) {
|
|
239
343
|
if (!modelDefinition?.invocationName || modelDefinition.invocationName === 'dry-run') return args;
|
|
240
344
|
if (argsAlreadySelectModel(args)) return args;
|
|
241
|
-
const invocationName =
|
|
242
|
-
|
|
243
|
-
|
|
345
|
+
const invocationName =
|
|
346
|
+
definition.id === 'claude-code'
|
|
347
|
+
? mapClaudeCliModelId(modelDefinition.invocationName)
|
|
348
|
+
: modelDefinition.invocationName;
|
|
244
349
|
return [...args, '--model', invocationName];
|
|
245
350
|
}
|
|
246
351
|
|
|
247
352
|
function argsWithStructuredOutput(args, definition, env = process.env) {
|
|
248
353
|
if (!structuredUsageEnabled(env)) return args;
|
|
249
354
|
if (definition.id === 'claude-code') {
|
|
250
|
-
const streamed = [
|
|
251
|
-
...argsWithoutFlagValue(args, '--output-format'),
|
|
252
|
-
'--output-format',
|
|
253
|
-
'stream-json',
|
|
254
|
-
];
|
|
355
|
+
const streamed = [...argsWithoutFlagValue(args, '--output-format'), '--output-format', 'stream-json'];
|
|
255
356
|
if (!argsIncludeFlag(streamed, '--include-partial-messages')) {
|
|
256
357
|
streamed.push('--include-partial-messages');
|
|
257
358
|
}
|
|
@@ -264,21 +365,72 @@ function argsWithStructuredOutput(args, definition, env = process.env) {
|
|
|
264
365
|
return args;
|
|
265
366
|
}
|
|
266
367
|
|
|
267
|
-
function argsWithClaudeIsolation(args, definition) {
|
|
368
|
+
function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
268
369
|
if (definition.id !== 'claude-code') return args;
|
|
269
370
|
const isolated = argsWithoutVariadicFlag(args, '--tools');
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
371
|
+
const harnessToolNames = Array.isArray(options.harnessToolNames)
|
|
372
|
+
? options.harnessToolNames
|
|
373
|
+
: [
|
|
374
|
+
'workspace_inspect',
|
|
375
|
+
'workspace_sync',
|
|
376
|
+
'shell_run',
|
|
377
|
+
'preview_control',
|
|
378
|
+
'browser_control',
|
|
379
|
+
'data_inspect',
|
|
380
|
+
'verification_run',
|
|
381
|
+
];
|
|
382
|
+
const harnessTools = harnessToolNames.map(
|
|
383
|
+
(name) => `mcp__instawebai__${name}`,
|
|
384
|
+
);
|
|
385
|
+
const nativeTools = options.framerHarness
|
|
386
|
+
? harnessTools
|
|
387
|
+
: [
|
|
388
|
+
'Read',
|
|
389
|
+
'Write',
|
|
390
|
+
'Edit',
|
|
391
|
+
'Glob',
|
|
392
|
+
'Grep',
|
|
393
|
+
'Bash',
|
|
394
|
+
...(options.nativeSkillsEnabled ? ['Skill'] : []),
|
|
395
|
+
...harnessTools,
|
|
396
|
+
];
|
|
397
|
+
isolated.push(
|
|
398
|
+
'--tools',
|
|
399
|
+
options.harnessMode
|
|
400
|
+
? nativeTools.join(',')
|
|
401
|
+
: options.nativeSkillsEnabled
|
|
402
|
+
? 'Skill'
|
|
403
|
+
: '',
|
|
404
|
+
);
|
|
405
|
+
const withoutDisabledSkills = isolated.filter((arg) => arg !== '--disable-slash-commands');
|
|
406
|
+
if (!options.nativeSkillsEnabled) withoutDisabledSkills.push('--disable-slash-commands');
|
|
407
|
+
const withoutAllowedTools = argsWithoutVariadicFlag(withoutDisabledSkills, '--allowedTools');
|
|
408
|
+
const projectSettingsOnly = argsWithoutFlagValue(withoutAllowedTools, '--setting-sources');
|
|
409
|
+
projectSettingsOnly.push('--setting-sources', 'project');
|
|
410
|
+
const permissionMode = argsWithoutFlagValue(projectSettingsOnly, '--permission-mode');
|
|
411
|
+
permissionMode.push(
|
|
412
|
+
'--permission-mode',
|
|
413
|
+
options.harnessMode && !options.framerHarness ? 'acceptEdits' : 'dontAsk',
|
|
414
|
+
);
|
|
415
|
+
if (options.harnessMode) permissionMode.push('--allowedTools', nativeTools.join(','));
|
|
416
|
+
if (!argsIncludeFlag(permissionMode, '--strict-mcp-config')) permissionMode.push('--strict-mcp-config');
|
|
417
|
+
if (!argsIncludeFlag(permissionMode, '--no-chrome')) permissionMode.push('--no-chrome');
|
|
418
|
+
let configured = permissionMode;
|
|
419
|
+
if (options.harnessMode && options.mcpConfig) {
|
|
420
|
+
configured = argsWithoutFlagValue(configured, '--mcp-config');
|
|
421
|
+
configured.push('--mcp-config', JSON.stringify(options.mcpConfig));
|
|
422
|
+
}
|
|
423
|
+
if (options.harnessMode && !argsIncludeFlag(configured, '--restricted')) {
|
|
424
|
+
configured.push('--restricted');
|
|
425
|
+
}
|
|
426
|
+
return configured;
|
|
275
427
|
}
|
|
276
428
|
|
|
277
429
|
function normalizeClaudeEffort(value, fallback = 'medium') {
|
|
278
|
-
const effort = String(value || '')
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
430
|
+
const effort = String(value || '')
|
|
431
|
+
.trim()
|
|
432
|
+
.toLowerCase();
|
|
433
|
+
return ['low', 'medium', 'high', 'xhigh', 'max'].includes(effort) ? effort : fallback;
|
|
282
434
|
}
|
|
283
435
|
|
|
284
436
|
function claudeEffortForStep(step, env = process.env) {
|
|
@@ -303,7 +455,9 @@ function argsWithClaudeModelEngine(args, definition, options = {}, env = process
|
|
|
303
455
|
return [
|
|
304
456
|
...isolated,
|
|
305
457
|
'--system-prompt',
|
|
306
|
-
|
|
458
|
+
options.harnessMode
|
|
459
|
+
? claudeOutcomeSystemPrompt(options.product)
|
|
460
|
+
: claudeModelEngineSystemPrompt(options.product),
|
|
307
461
|
'--effort',
|
|
308
462
|
claudeEffortForStep(options.step, env),
|
|
309
463
|
];
|
|
@@ -337,33 +491,61 @@ function argsWithoutVariadicFlag(args, flag) {
|
|
|
337
491
|
return filtered;
|
|
338
492
|
}
|
|
339
493
|
|
|
494
|
+
function normalizeClaudeJsonSchema(value, path = '$') {
|
|
495
|
+
if (Array.isArray(value)) {
|
|
496
|
+
return value.map((item, index) => normalizeClaudeJsonSchema(item, `${path}[${index}]`));
|
|
497
|
+
}
|
|
498
|
+
if (!value || typeof value !== 'object') return value;
|
|
499
|
+
|
|
500
|
+
const normalized = Object.fromEntries(
|
|
501
|
+
Object.entries(value).map(([key, nested]) => [
|
|
502
|
+
key,
|
|
503
|
+
normalizeClaudeJsonSchema(nested, `${path}.${key}`),
|
|
504
|
+
]),
|
|
505
|
+
);
|
|
506
|
+
for (const [exclusiveKey, inclusiveKey] of [
|
|
507
|
+
['exclusiveMinimum', 'minimum'],
|
|
508
|
+
['exclusiveMaximum', 'maximum'],
|
|
509
|
+
]) {
|
|
510
|
+
const exclusive = normalized[exclusiveKey];
|
|
511
|
+
if (typeof exclusive !== 'boolean') continue;
|
|
512
|
+
if (!exclusive) {
|
|
513
|
+
delete normalized[exclusiveKey];
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const bound = normalized[inclusiveKey];
|
|
517
|
+
if (typeof bound !== 'number' || !Number.isFinite(bound)) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`Invalid Claude JSON Schema at ${path}: ${exclusiveKey}: true requires a finite ${inclusiveKey}.`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
normalized[exclusiveKey] = bound;
|
|
523
|
+
delete normalized[inclusiveKey];
|
|
524
|
+
}
|
|
525
|
+
return normalized;
|
|
526
|
+
}
|
|
527
|
+
|
|
340
528
|
function compactClaudeOutputSchema(outputSchema) {
|
|
341
529
|
const toolCalls = outputSchema?.properties?.toolCalls;
|
|
342
530
|
const itemSchema = toolCalls?.items;
|
|
343
|
-
const variants = Array.isArray(itemSchema?.anyOf)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
)),
|
|
354
|
-
));
|
|
531
|
+
const variants = Array.isArray(itemSchema?.anyOf) ? itemSchema.anyOf : itemSchema ? [itemSchema] : [];
|
|
532
|
+
const toolNames = Array.from(
|
|
533
|
+
new Set(
|
|
534
|
+
variants.flatMap((variant) =>
|
|
535
|
+
Array.isArray(variant?.properties?.name?.enum)
|
|
536
|
+
? variant.properties.name.enum.filter((name) => typeof name === 'string' && name)
|
|
537
|
+
: [],
|
|
538
|
+
),
|
|
539
|
+
),
|
|
540
|
+
);
|
|
355
541
|
return {
|
|
356
542
|
type: 'object',
|
|
357
543
|
properties: {
|
|
358
544
|
text: { type: 'string' },
|
|
359
545
|
toolCalls: {
|
|
360
546
|
type: 'array',
|
|
361
|
-
...(Number.isFinite(Number(toolCalls?.minItems))
|
|
362
|
-
|
|
363
|
-
: {}),
|
|
364
|
-
...(Number.isFinite(Number(toolCalls?.maxItems))
|
|
365
|
-
? { maxItems: Number(toolCalls.maxItems) }
|
|
366
|
-
: {}),
|
|
547
|
+
...(Number.isFinite(Number(toolCalls?.minItems)) ? { minItems: Number(toolCalls.minItems) } : {}),
|
|
548
|
+
...(Number.isFinite(Number(toolCalls?.maxItems)) ? { maxItems: Number(toolCalls.maxItems) } : {}),
|
|
367
549
|
items: {
|
|
368
550
|
type: 'object',
|
|
369
551
|
properties: {
|
|
@@ -388,16 +570,24 @@ function compactClaudeOutputSchema(outputSchema) {
|
|
|
388
570
|
};
|
|
389
571
|
}
|
|
390
572
|
|
|
391
|
-
function argsWithOutputSchema(
|
|
573
|
+
function argsWithOutputSchema(
|
|
574
|
+
args,
|
|
575
|
+
definition,
|
|
576
|
+
outputSchema,
|
|
577
|
+
responseContract = 'model-turn',
|
|
578
|
+
) {
|
|
392
579
|
if (definition.id !== 'claude-code' || !outputSchema) return args;
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
580
|
+
const normalizedResponseContract =
|
|
581
|
+
normalizeStructuredResponseContract(responseContract);
|
|
582
|
+
const normalizedSchema = normalizeClaudeJsonSchema(outputSchema);
|
|
583
|
+
// Model turns use a compact transport envelope because their tool catalog can
|
|
584
|
+
// contain flexible schemas. Harness outcomes already use a small, strict,
|
|
585
|
+
// provider-neutral result schema and must remain in that direct shape.
|
|
586
|
+
const launchSchema =
|
|
587
|
+
normalizedResponseContract === STRUCTURED_RESPONSE_CONTRACTS.OUTCOME
|
|
588
|
+
? assertOutcomeResponseContract(responseContract, normalizedSchema)
|
|
589
|
+
: compactClaudeOutputSchema(normalizedSchema);
|
|
590
|
+
return [...argsWithoutFlagValue(args, '--json-schema'), '--json-schema', JSON.stringify(launchSchema)];
|
|
401
591
|
}
|
|
402
592
|
|
|
403
593
|
function argsWithPromptInput(args, definition) {
|
|
@@ -452,7 +642,12 @@ function argsWithResumedSession(args, definition, sessionId) {
|
|
|
452
642
|
}
|
|
453
643
|
|
|
454
644
|
function agentSessionResumeEnabled(definition, env = process.env) {
|
|
455
|
-
if (
|
|
645
|
+
if (
|
|
646
|
+
String(env.DEXTER_BRIDGE_SESSION_RESUME || 'true')
|
|
647
|
+
.trim()
|
|
648
|
+
.toLowerCase() === 'false'
|
|
649
|
+
)
|
|
650
|
+
return false;
|
|
456
651
|
const baseArgs = parseExtraArgs(env[definition.argsEnv], definition.defaultArgs);
|
|
457
652
|
if (definition.id === 'claude-code' && argsIncludeFlag(baseArgs, '--no-session-persistence')) return false;
|
|
458
653
|
if (definition.id === 'codex' && argsIncludeFlag(baseArgs, '--ephemeral')) return false;
|
|
@@ -464,13 +659,17 @@ export function buildAgentArgs(definition, modelDefinition, env = process.env, o
|
|
|
464
659
|
const requiredArgs = argsWithRequiredAgentFlags(baseArgs, definition);
|
|
465
660
|
const modelArgs = argsWithSelectedModel(requiredArgs, modelDefinition, definition);
|
|
466
661
|
const structuredArgs = argsWithStructuredOutput(modelArgs, definition, env);
|
|
467
|
-
const isolatedArgs = argsWithClaudeIsolation(
|
|
662
|
+
const isolatedArgs = argsWithClaudeIsolation(
|
|
663
|
+
structuredArgs,
|
|
664
|
+
definition,
|
|
665
|
+
options,
|
|
666
|
+
);
|
|
468
667
|
const modelEngineArgs = argsWithClaudeModelEngine(isolatedArgs, definition, options, env);
|
|
469
668
|
const schemaArgs = argsWithOutputSchema(
|
|
470
669
|
modelEngineArgs,
|
|
471
670
|
definition,
|
|
472
671
|
options.outputSchema,
|
|
473
|
-
options.
|
|
672
|
+
options.responseContract,
|
|
474
673
|
);
|
|
475
674
|
const resumedArgs = argsWithResumedSession(schemaArgs, definition, options.resumeSessionId);
|
|
476
675
|
return argsWithPromptInput(resumedArgs, definition);
|
|
@@ -493,7 +692,12 @@ let cachedLoginShellEnvMeta = { attempted: false, loaded: false, keys: 0 };
|
|
|
493
692
|
|
|
494
693
|
function loginShellEnv() {
|
|
495
694
|
if (process.env.DEXTER_BRIDGE_DISABLE_LOGIN_SHELL_ENV === '1' || process.platform === 'win32') {
|
|
496
|
-
cachedLoginShellEnvMeta = {
|
|
695
|
+
cachedLoginShellEnvMeta = {
|
|
696
|
+
attempted: false,
|
|
697
|
+
loaded: false,
|
|
698
|
+
keys: 0,
|
|
699
|
+
disabled: true,
|
|
700
|
+
};
|
|
497
701
|
return {};
|
|
498
702
|
}
|
|
499
703
|
if (cachedLoginShellEnv) return cachedLoginShellEnv;
|
|
@@ -557,20 +761,18 @@ export function mergePathEntries(paths, platform = process.platform) {
|
|
|
557
761
|
|
|
558
762
|
export function processEnvWithCliPath(baseEnv = process.env, platform = process.platform) {
|
|
559
763
|
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
560
|
-
const homeDirectory =
|
|
561
|
-
|
|
562
|
-
baseEnv.USERPROFILE
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
]
|
|
573
|
-
: ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
|
|
764
|
+
const homeDirectory =
|
|
765
|
+
platform === 'win32'
|
|
766
|
+
? String(baseEnv.USERPROFILE || baseEnv.HOME || (process.platform === 'win32' ? os.homedir() : '')).trim()
|
|
767
|
+
: String(baseEnv.HOME || (platform === process.platform ? os.homedir() : '')).trim();
|
|
768
|
+
const fallbackPath =
|
|
769
|
+
platform === 'win32'
|
|
770
|
+
? [
|
|
771
|
+
homeDirectory ? pathApi.join(homeDirectory, '.local', 'bin') : '',
|
|
772
|
+
baseEnv.APPDATA ? pathApi.join(baseEnv.APPDATA, 'npm') : '',
|
|
773
|
+
baseEnv.LOCALAPPDATA ? pathApi.join(baseEnv.LOCALAPPDATA, 'Programs') : '',
|
|
774
|
+
]
|
|
775
|
+
: ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
|
|
574
776
|
const shellEnv = platform === 'win32' ? {} : loginShellEnv();
|
|
575
777
|
const existingPath = String(baseEnv.PATH || '');
|
|
576
778
|
const mergedPath = mergePathEntries([shellEnv.PATH, existingPath, ...fallbackPath], platform);
|
|
@@ -581,7 +783,11 @@ export function processEnvWithCliPath(baseEnv = process.env, platform = process.
|
|
|
581
783
|
};
|
|
582
784
|
}
|
|
583
785
|
|
|
584
|
-
export function resolveAgentCwd(
|
|
786
|
+
export function resolveAgentCwd(
|
|
787
|
+
env = process.env,
|
|
788
|
+
currentWorkingDirectory = process.cwd(),
|
|
789
|
+
platform = process.platform,
|
|
790
|
+
) {
|
|
585
791
|
const explicit = String(env.DEXTER_BRIDGE_AGENT_CWD || env.DEXTER_BRIDGE_WORKDIR || '').trim();
|
|
586
792
|
if (explicit) return explicit;
|
|
587
793
|
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
@@ -592,7 +798,9 @@ export function resolveAgentCwd(env = process.env, currentWorkingDirectory = pro
|
|
|
592
798
|
|
|
593
799
|
function processEnvSummary(env, platform = process.platform) {
|
|
594
800
|
const delimiter = platform === 'win32' ? ';' : ':';
|
|
595
|
-
const pathEntries = String(env.PATH || '')
|
|
801
|
+
const pathEntries = String(env.PATH || '')
|
|
802
|
+
.split(delimiter)
|
|
803
|
+
.filter(Boolean);
|
|
596
804
|
return {
|
|
597
805
|
hasAzureOpenAiApiKey: Boolean(env.AZURE_OPENAI_API_KEY),
|
|
598
806
|
hasOpenAiApiKey: Boolean(env.OPENAI_API_KEY),
|
|
@@ -604,18 +812,28 @@ function processEnvSummary(env, platform = process.platform) {
|
|
|
604
812
|
};
|
|
605
813
|
}
|
|
606
814
|
|
|
607
|
-
export function executableCandidates(
|
|
815
|
+
export function executableCandidates(
|
|
816
|
+
command,
|
|
817
|
+
env = process.env,
|
|
818
|
+
platform = process.platform,
|
|
819
|
+
existsSync = fs.existsSync,
|
|
820
|
+
) {
|
|
608
821
|
const raw = String(command || '').trim();
|
|
609
822
|
if (!raw) return [];
|
|
610
823
|
if (/[\\/]/.test(raw)) return [raw];
|
|
611
824
|
const delimiter = platform === 'win32' ? ';' : ':';
|
|
612
825
|
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
613
|
-
const pathEntries = String(env.PATH || '')
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
826
|
+
const pathEntries = String(env.PATH || '')
|
|
827
|
+
.split(delimiter)
|
|
828
|
+
.filter(Boolean);
|
|
829
|
+
const extensions =
|
|
830
|
+
platform === 'win32'
|
|
831
|
+
? path.win32.extname(raw)
|
|
832
|
+
? ['']
|
|
833
|
+
: String(env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
834
|
+
.split(';')
|
|
835
|
+
.filter(Boolean)
|
|
836
|
+
: [''];
|
|
619
837
|
const candidates = [];
|
|
620
838
|
for (const directory of pathEntries) {
|
|
621
839
|
for (const extension of extensions) {
|
|
@@ -630,7 +848,12 @@ export function executableCandidates(command, env = process.env, platform = proc
|
|
|
630
848
|
return [...new Set(candidates.length ? candidates : [raw])];
|
|
631
849
|
}
|
|
632
850
|
|
|
633
|
-
export function resolveExecutableCommand(
|
|
851
|
+
export function resolveExecutableCommand(
|
|
852
|
+
command,
|
|
853
|
+
env = process.env,
|
|
854
|
+
platform = process.platform,
|
|
855
|
+
existsSync = fs.existsSync,
|
|
856
|
+
) {
|
|
634
857
|
return executableCandidates(command, env, platform, existsSync)[0] || String(command || '').trim();
|
|
635
858
|
}
|
|
636
859
|
|
|
@@ -640,7 +863,9 @@ export function parseAgentVersion(output) {
|
|
|
640
863
|
}
|
|
641
864
|
|
|
642
865
|
function versionParts(value) {
|
|
643
|
-
const match = String(value || '')
|
|
866
|
+
const match = String(value || '')
|
|
867
|
+
.trim()
|
|
868
|
+
.match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
644
869
|
return match ? match.slice(1).map(Number) : null;
|
|
645
870
|
}
|
|
646
871
|
|
|
@@ -669,9 +894,7 @@ export function selectAgentRuntime(inspections, definition, modelDefinition) {
|
|
|
669
894
|
return {
|
|
670
895
|
ok: false,
|
|
671
896
|
command: first?.command || definition.fallbackCommand,
|
|
672
|
-
code: first?.code === 'ENOENT'
|
|
673
|
-
? 'DEXTER_AGENT_NOT_FOUND'
|
|
674
|
-
: first?.code,
|
|
897
|
+
code: first?.code === 'ENOENT' ? 'DEXTER_AGENT_NOT_FOUND' : first?.code,
|
|
675
898
|
error: first?.error || `${definition.label} is not installed or could not be started.`,
|
|
676
899
|
candidates: inspections,
|
|
677
900
|
};
|
|
@@ -679,7 +902,9 @@ export function selectAgentRuntime(inspections, definition, modelDefinition) {
|
|
|
679
902
|
|
|
680
903
|
const ranked = successful
|
|
681
904
|
.map((inspection, index) => ({ ...inspection, discoveryIndex: index }))
|
|
682
|
-
.sort(
|
|
905
|
+
.sort(
|
|
906
|
+
(left, right) => compareAgentVersions(right.version, left.version) || left.discoveryIndex - right.discoveryIndex,
|
|
907
|
+
);
|
|
683
908
|
const compatible = ranked.find((inspection) => modelSupportsAgentVersion(modelDefinition, inspection.version));
|
|
684
909
|
if (compatible) {
|
|
685
910
|
return {
|
|
@@ -697,14 +922,19 @@ export function selectAgentRuntime(inspections, definition, modelDefinition) {
|
|
|
697
922
|
...found,
|
|
698
923
|
ok: false,
|
|
699
924
|
code: 'DEXTER_AGENT_MODEL_VERSION_UNSUPPORTED',
|
|
700
|
-
error: `${modelLabel} requires ${definition.label} ${requiredVersion} or newer.
|
|
925
|
+
error: `${modelLabel} requires ${definition.label} ${requiredVersion} or newer. The bridge found ${foundVersion} at ${found.command}. Update ${definition.label} or set ${definition.commandEnv} to the full path of a compatible installation.`,
|
|
701
926
|
candidates: inspections,
|
|
702
927
|
};
|
|
703
928
|
}
|
|
704
929
|
|
|
705
930
|
export function processInvocation(command, args, env = process.env, platform = process.platform) {
|
|
706
931
|
const resolvedCommand = resolveExecutableCommand(command, env, platform);
|
|
707
|
-
return {
|
|
932
|
+
return {
|
|
933
|
+
command: resolvedCommand,
|
|
934
|
+
args,
|
|
935
|
+
resolvedCommand,
|
|
936
|
+
windowsHide: platform === 'win32',
|
|
937
|
+
};
|
|
708
938
|
}
|
|
709
939
|
|
|
710
940
|
function safeCommandArgs(args) {
|
|
@@ -722,12 +952,85 @@ function normalizeDiagnosticLine(line) {
|
|
|
722
952
|
.replace(/\s+/g, ' ');
|
|
723
953
|
}
|
|
724
954
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const lines = combined
|
|
955
|
+
function jsonDiagnosticEvents(value) {
|
|
956
|
+
return String(value || '')
|
|
728
957
|
.split(/\r?\n/)
|
|
729
|
-
.map(
|
|
730
|
-
.filter(Boolean)
|
|
958
|
+
.map((line) => line.trim())
|
|
959
|
+
.filter(Boolean)
|
|
960
|
+
.flatMap((line) => {
|
|
961
|
+
try {
|
|
962
|
+
const parsed = JSON.parse(line);
|
|
963
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? [parsed] : [];
|
|
964
|
+
} catch {
|
|
965
|
+
return [];
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function claudeFailureDetails(stdout) {
|
|
971
|
+
const events = jsonDiagnosticEvents(stdout);
|
|
972
|
+
const init = [...events].reverse().find((event) => event.type === 'system' && event.subtype === 'init');
|
|
973
|
+
const model = typeof init?.model === 'string' ? init.model.trim().toLowerCase() : '';
|
|
974
|
+
const rateLimit = [...events]
|
|
975
|
+
.reverse()
|
|
976
|
+
.find(
|
|
977
|
+
(event) =>
|
|
978
|
+
event.type === 'rate_limit_event' && event.rate_limit_info && typeof event.rate_limit_info === 'object',
|
|
979
|
+
);
|
|
980
|
+
const rateLimitInfo = rateLimit?.rate_limit_info;
|
|
981
|
+
if (rateLimitInfo?.errorCode === 'credits_required' || rateLimitInfo?.overageDisabledReason === 'out_of_credits') {
|
|
982
|
+
if (model.includes('fable')) {
|
|
983
|
+
return {
|
|
984
|
+
code: 'CLAUDE_CREDITS_REQUIRED',
|
|
985
|
+
message:
|
|
986
|
+
'Claude Fable could not run because this Claude account has no extra-usage credits available. '
|
|
987
|
+
+ 'Add Claude usage credits or switch this run to Opus or Sonnet.',
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
code: 'CLAUDE_CREDITS_REQUIRED',
|
|
992
|
+
message:
|
|
993
|
+
'Claude Code could not run because this Claude account is out of credits. '
|
|
994
|
+
+ 'Add credits or enable extra usage in Claude, then retry. You can also switch to Codex.',
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
if (rateLimitInfo?.status === 'rejected') {
|
|
998
|
+
const resetAt = Number(rateLimitInfo.resetsAt);
|
|
999
|
+
const resetMessage = Number.isFinite(resetAt)
|
|
1000
|
+
? ` Try again after ${new Date(resetAt * 1000).toISOString()}.`
|
|
1001
|
+
: ' Try again later.';
|
|
1002
|
+
return {
|
|
1003
|
+
code: 'CLAUDE_RATE_LIMITED',
|
|
1004
|
+
message: `Claude Code is currently rate limited.${resetMessage}`,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const resultError = [...events]
|
|
1009
|
+
.reverse()
|
|
1010
|
+
.find((event) => event.type === 'result' && (event.is_error === true || event.subtype === 'error'));
|
|
1011
|
+
const resultMessages = Array.isArray(resultError?.errors)
|
|
1012
|
+
? resultError.errors.filter((message) => typeof message === 'string' && message.trim())
|
|
1013
|
+
: [];
|
|
1014
|
+
const resultMessage =
|
|
1015
|
+
resultMessages.join(' ') || (typeof resultError?.result === 'string' ? resultError.result.trim() : '');
|
|
1016
|
+
if (resultMessage) {
|
|
1017
|
+
return {
|
|
1018
|
+
code: 'CLAUDE_RUN_FAILED',
|
|
1019
|
+
message: `Claude Code could not complete this model turn: ${clip(resultMessage, 700)}`,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
export function agentFailureDetails(command, code, stdout = '', stderr = '') {
|
|
1027
|
+
if (/(^|[/\\])claude(?:\.exe)?$/i.test(String(command || ''))) {
|
|
1028
|
+
const claudeFailure = claudeFailureDetails(stdout);
|
|
1029
|
+
if (claudeFailure) return claudeFailure;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const combined = [stderr, stdout].filter(Boolean).join('\n').trim();
|
|
1033
|
+
const lines = combined.split(/\r?\n/).map(normalizeDiagnosticLine).filter(Boolean);
|
|
731
1034
|
const diagnosticPatterns = [
|
|
732
1035
|
/missing environment variable/i,
|
|
733
1036
|
/not inside a trusted directory/i,
|
|
@@ -742,26 +1045,37 @@ export function agentFailureMessage(command, code, stdout = '', stderr = '') {
|
|
|
742
1045
|
/^failed\b/i,
|
|
743
1046
|
];
|
|
744
1047
|
const diagnostics = lines.filter((line) => diagnosticPatterns.some((pattern) => pattern.test(line)));
|
|
745
|
-
const detail = diagnostics.length
|
|
746
|
-
|
|
747
|
-
:
|
|
748
|
-
|
|
1048
|
+
const detail = diagnostics.length ? diagnostics.slice(-3).join(' ') : clip(combined, 1000);
|
|
1049
|
+
return {
|
|
1050
|
+
code: 'AGENT_PROCESS_FAILED',
|
|
1051
|
+
message: `${command} exited with code ${code}.${detail ? ` ${clip(detail, 1000)}` : ''}`.trim(),
|
|
1052
|
+
};
|
|
749
1053
|
}
|
|
750
1054
|
|
|
751
|
-
function
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1055
|
+
export function agentFailureMessage(command, code, stdout = '', stderr = '') {
|
|
1056
|
+
return agentFailureDetails(command, code, stdout, stderr).message;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function runProcess(
|
|
1060
|
+
command,
|
|
1061
|
+
args,
|
|
1062
|
+
stdin,
|
|
1063
|
+
{
|
|
1064
|
+
timeoutMs = 120000,
|
|
1065
|
+
maxDurationMs,
|
|
1066
|
+
trace,
|
|
1067
|
+
childEnv: providedChildEnv,
|
|
1068
|
+
cwd: providedCwd,
|
|
1069
|
+
platform = process.platform,
|
|
1070
|
+
signal,
|
|
1071
|
+
killGraceMs = 250,
|
|
1072
|
+
onProgress,
|
|
1073
|
+
stdoutProgress,
|
|
1074
|
+
} = {},
|
|
1075
|
+
) {
|
|
760
1076
|
return new Promise((resolve, reject) => {
|
|
761
1077
|
if (signal?.aborted) {
|
|
762
|
-
const error = signal.reason instanceof Error
|
|
763
|
-
? signal.reason
|
|
764
|
-
: new Error('The model process was cancelled.');
|
|
1078
|
+
const error = signal.reason instanceof Error ? signal.reason : new Error('The model process was cancelled.');
|
|
765
1079
|
error.code = error.code || 'RUN_CANCELLED';
|
|
766
1080
|
reject(error);
|
|
767
1081
|
return;
|
|
@@ -772,7 +1086,10 @@ function runProcess(command, args, stdin, {
|
|
|
772
1086
|
// hardDeadlineMs bounds total wall-clock time regardless of activity.
|
|
773
1087
|
const hardDeadlineMs = Math.max(maxDurationMs || timeoutMs * 5, 10);
|
|
774
1088
|
const childEnv = processEnvWithCliPath(providedChildEnv || process.env, platform);
|
|
775
|
-
const cwd =
|
|
1089
|
+
const cwd =
|
|
1090
|
+
typeof providedCwd === 'string' && providedCwd.trim()
|
|
1091
|
+
? providedCwd
|
|
1092
|
+
: resolveAgentCwd(childEnv, process.cwd(), platform);
|
|
776
1093
|
const invocation = processInvocation(command, args, childEnv, platform);
|
|
777
1094
|
trace?.info('agent_process_spawn', {
|
|
778
1095
|
command,
|
|
@@ -791,11 +1108,18 @@ function runProcess(command, args, stdin, {
|
|
|
791
1108
|
windowsHide: invocation.windowsHide,
|
|
792
1109
|
detached: platform !== 'win32',
|
|
793
1110
|
});
|
|
1111
|
+
onProgress?.({
|
|
1112
|
+
kind: 'turn_started',
|
|
1113
|
+
provider: 'cli',
|
|
1114
|
+
occurredAt: new Date().toISOString(),
|
|
1115
|
+
});
|
|
794
1116
|
let stdout = '';
|
|
795
1117
|
let stderr = '';
|
|
796
1118
|
let settled = false;
|
|
1119
|
+
let terminationError = null;
|
|
797
1120
|
let inactivityTimer = null;
|
|
798
1121
|
let forceKillTimer = null;
|
|
1122
|
+
let terminationFallbackTimer = null;
|
|
799
1123
|
const killChildTree = (killSignal) => {
|
|
800
1124
|
if (platform !== 'win32' && child.pid) {
|
|
801
1125
|
try {
|
|
@@ -815,38 +1139,50 @@ function runProcess(command, args, stdin, {
|
|
|
815
1139
|
clearTimeout(inactivityTimer);
|
|
816
1140
|
clearTimeout(deadlineTimer);
|
|
817
1141
|
};
|
|
818
|
-
const
|
|
1142
|
+
const settleWithError = (error) => {
|
|
1143
|
+
if (settled) return;
|
|
1144
|
+
settled = true;
|
|
1145
|
+
clearTimers();
|
|
1146
|
+
clearTimeout(forceKillTimer);
|
|
1147
|
+
clearTimeout(terminationFallbackTimer);
|
|
1148
|
+
signal?.removeEventListener('abort', abort);
|
|
1149
|
+
error.stdout = stdout;
|
|
1150
|
+
error.stderr = stderr;
|
|
1151
|
+
reject(error);
|
|
1152
|
+
};
|
|
1153
|
+
const terminateChildTree = (error) => {
|
|
819
1154
|
killChildTree('SIGTERM');
|
|
820
|
-
forceKillTimer = setTimeout(() =>
|
|
1155
|
+
forceKillTimer = setTimeout(() => {
|
|
1156
|
+
killChildTree('SIGKILL');
|
|
1157
|
+
terminationFallbackTimer = setTimeout(
|
|
1158
|
+
() => settleWithError(error),
|
|
1159
|
+
Math.max(killGraceMs, 250),
|
|
1160
|
+
);
|
|
1161
|
+
terminationFallbackTimer.unref?.();
|
|
1162
|
+
}, killGraceMs);
|
|
821
1163
|
forceKillTimer.unref?.();
|
|
822
1164
|
};
|
|
823
1165
|
const abort = () => {
|
|
824
|
-
if (settled) return;
|
|
825
|
-
settled = true;
|
|
1166
|
+
if (settled || terminationError) return;
|
|
826
1167
|
clearTimers();
|
|
827
|
-
|
|
828
|
-
const error = signal?.reason instanceof Error
|
|
829
|
-
? signal.reason
|
|
830
|
-
: new Error('The model process was cancelled.');
|
|
1168
|
+
const error = signal?.reason instanceof Error ? signal.reason : new Error('The model process was cancelled.');
|
|
831
1169
|
error.code = error.code || 'RUN_CANCELLED';
|
|
832
|
-
|
|
833
|
-
error
|
|
1170
|
+
terminationError = error;
|
|
1171
|
+
terminateChildTree(error);
|
|
834
1172
|
trace?.info('agent_process_cancelled', {
|
|
835
1173
|
command,
|
|
836
1174
|
durationMs: Date.now() - started,
|
|
837
1175
|
});
|
|
838
|
-
reject(error);
|
|
839
1176
|
};
|
|
840
|
-
const timeOut = (message) => {
|
|
841
|
-
if (settled) return;
|
|
842
|
-
settled = true;
|
|
1177
|
+
const timeOut = (code, message) => {
|
|
1178
|
+
if (settled || terminationError) return;
|
|
843
1179
|
clearTimers();
|
|
844
|
-
terminateChildTree();
|
|
845
1180
|
const error = new Error(message);
|
|
846
|
-
error.code =
|
|
847
|
-
error.
|
|
848
|
-
error.stderr = stderr;
|
|
1181
|
+
error.code = code;
|
|
1182
|
+
error.retryable = true;
|
|
849
1183
|
error.timedOut = true;
|
|
1184
|
+
terminationError = error;
|
|
1185
|
+
terminateChildTree(error);
|
|
850
1186
|
trace?.error('agent_process_timeout', {
|
|
851
1187
|
command,
|
|
852
1188
|
durationMs: Date.now() - started,
|
|
@@ -857,47 +1193,74 @@ function runProcess(command, args, stdin, {
|
|
|
857
1193
|
stdoutExcerpt: clip(stdout, 1000),
|
|
858
1194
|
stderrExcerpt: clip(stderr, 1000),
|
|
859
1195
|
});
|
|
860
|
-
reject(error);
|
|
861
1196
|
};
|
|
862
1197
|
const armInactivityTimer = () => {
|
|
863
1198
|
clearTimeout(inactivityTimer);
|
|
864
1199
|
inactivityTimer = setTimeout(() => {
|
|
865
|
-
timeOut(
|
|
1200
|
+
timeOut(
|
|
1201
|
+
'APP_AGENT_TURN_INACTIVITY_TIMEOUT',
|
|
1202
|
+
`${command} timed out after ${timeoutMs}ms without meaningful output.`,
|
|
1203
|
+
);
|
|
866
1204
|
}, timeoutMs);
|
|
867
1205
|
};
|
|
868
1206
|
const deadlineTimer = setTimeout(() => {
|
|
869
|
-
timeOut(
|
|
1207
|
+
timeOut(
|
|
1208
|
+
'APP_AGENT_TURN_HARD_TIMEOUT',
|
|
1209
|
+
`${command} timed out after ${hardDeadlineMs}ms at the safety limit.`,
|
|
1210
|
+
);
|
|
870
1211
|
}, hardDeadlineMs);
|
|
871
1212
|
signal?.addEventListener('abort', abort, { once: true });
|
|
872
1213
|
armInactivityTimer();
|
|
873
1214
|
|
|
874
1215
|
child.stdout.on('data', (chunk) => {
|
|
875
|
-
|
|
876
|
-
|
|
1216
|
+
const text = chunk.toString('utf8');
|
|
1217
|
+
stdout += text;
|
|
1218
|
+
if (!terminationError) {
|
|
1219
|
+
armInactivityTimer();
|
|
1220
|
+
stdoutProgress?.push(text);
|
|
1221
|
+
onProgress?.({
|
|
1222
|
+
kind: 'output_delta',
|
|
1223
|
+
provider: 'cli',
|
|
1224
|
+
occurredAt: new Date().toISOString(),
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
877
1227
|
});
|
|
878
1228
|
child.stderr.on('data', (chunk) => {
|
|
879
1229
|
stderr += chunk.toString('utf8');
|
|
880
|
-
|
|
1230
|
+
if (!terminationError) {
|
|
1231
|
+
armInactivityTimer();
|
|
1232
|
+
onProgress?.({
|
|
1233
|
+
kind: 'output_delta',
|
|
1234
|
+
provider: 'cli',
|
|
1235
|
+
occurredAt: new Date().toISOString(),
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
881
1238
|
});
|
|
882
1239
|
child.on('error', (error) => {
|
|
883
1240
|
if (settled) return;
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1241
|
+
if (terminationError) {
|
|
1242
|
+
settleWithError(terminationError);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
888
1245
|
trace?.error('agent_process_error', {
|
|
889
1246
|
command,
|
|
890
1247
|
durationMs: Date.now() - started,
|
|
891
1248
|
error: errorMeta(error),
|
|
892
1249
|
});
|
|
893
|
-
|
|
1250
|
+
settleWithError(error);
|
|
894
1251
|
});
|
|
895
1252
|
child.on('close', (code) => {
|
|
896
1253
|
if (settled) return;
|
|
1254
|
+
if (terminationError) {
|
|
1255
|
+
settleWithError(terminationError);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
897
1258
|
settled = true;
|
|
898
1259
|
clearTimers();
|
|
899
1260
|
clearTimeout(forceKillTimer);
|
|
1261
|
+
clearTimeout(terminationFallbackTimer);
|
|
900
1262
|
signal?.removeEventListener('abort', abort);
|
|
1263
|
+
stdoutProgress?.flush();
|
|
901
1264
|
const meta = {
|
|
902
1265
|
command,
|
|
903
1266
|
code,
|
|
@@ -912,9 +1275,14 @@ function runProcess(command, args, stdin, {
|
|
|
912
1275
|
resolve({ stdout, stderr, code });
|
|
913
1276
|
return;
|
|
914
1277
|
}
|
|
915
|
-
const
|
|
916
|
-
trace?.error('agent_process_exit_nonzero', {
|
|
917
|
-
|
|
1278
|
+
const failure = agentFailureDetails(command, code, stdout, stderr);
|
|
1279
|
+
trace?.error('agent_process_exit_nonzero', {
|
|
1280
|
+
...meta,
|
|
1281
|
+
failureCode: failure.code,
|
|
1282
|
+
failureMessage: failure.message,
|
|
1283
|
+
});
|
|
1284
|
+
const error = new Error(failure.message);
|
|
1285
|
+
error.code = failure.code;
|
|
918
1286
|
error.stdout = stdout;
|
|
919
1287
|
error.stderr = stderr;
|
|
920
1288
|
error.exitCode = code;
|
|
@@ -929,11 +1297,26 @@ function definitionForAgent(agent) {
|
|
|
929
1297
|
return AGENT_DEFINITIONS[normalizeAgentName(agent)] || AGENT_DEFINITIONS[DEFAULT_BRIDGE_AGENT];
|
|
930
1298
|
}
|
|
931
1299
|
|
|
1300
|
+
function definitionForRuntimeProfile(profile, fallbackAgent) {
|
|
1301
|
+
if (!profile) return definitionForAgent(fallbackAgent);
|
|
1302
|
+
if (profile.driver === 'claude-code-cli') return AGENT_DEFINITIONS['claude-code'];
|
|
1303
|
+
if (profile.driver === 'codex-app-server') return AGENT_DEFINITIONS.codex;
|
|
1304
|
+
return {
|
|
1305
|
+
id: profile.id || profile.driver,
|
|
1306
|
+
label: profile.label || profile.driver || 'Local runtime',
|
|
1307
|
+
model: profile.models?.[0]?.id || 'local-runtime',
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
|
|
932
1311
|
function adapterSessionId(run) {
|
|
933
1312
|
return run?.turnId || run?.runId;
|
|
934
1313
|
}
|
|
935
1314
|
|
|
936
1315
|
function adapterInvocationModel(run, selectedModel, agent) {
|
|
1316
|
+
const runtimeInvocation = run?.runtimeProfile?.model?.invocationName;
|
|
1317
|
+
if (typeof runtimeInvocation === 'string' && runtimeInvocation.trim()) {
|
|
1318
|
+
return runtimeInvocation.trim();
|
|
1319
|
+
}
|
|
937
1320
|
const explicit = run?.companion?.model?.invocationName;
|
|
938
1321
|
if (typeof explicit === 'string' && explicit.trim()) return explicit.trim();
|
|
939
1322
|
if (selectedModel?.invocationName) return selectedModel.invocationName;
|
|
@@ -945,9 +1328,7 @@ function adapterInvocationModel(run, selectedModel, agent) {
|
|
|
945
1328
|
|
|
946
1329
|
function controlRequestsCancellation(response) {
|
|
947
1330
|
return Boolean(
|
|
948
|
-
response?.control?.cancelRequested
|
|
949
|
-
|| response?.status === 'cancelled'
|
|
950
|
-
|| response?.status === 'expired',
|
|
1331
|
+
response?.control?.cancelRequested || response?.status === 'cancelled' || response?.status === 'expired',
|
|
951
1332
|
);
|
|
952
1333
|
}
|
|
953
1334
|
|
|
@@ -964,26 +1345,38 @@ function waitForControl(delayMs, signal) {
|
|
|
964
1345
|
return;
|
|
965
1346
|
}
|
|
966
1347
|
const timer = setTimeout(resolve, delayMs);
|
|
967
|
-
signal?.addEventListener(
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1348
|
+
signal?.addEventListener(
|
|
1349
|
+
'abort',
|
|
1350
|
+
() => {
|
|
1351
|
+
clearTimeout(timer);
|
|
1352
|
+
reject(signal.reason || new Error('Control monitor stopped.'));
|
|
1353
|
+
},
|
|
1354
|
+
{ once: true },
|
|
1355
|
+
);
|
|
971
1356
|
});
|
|
972
1357
|
}
|
|
973
1358
|
|
|
974
1359
|
class CompanionRunCancelledError extends Error {
|
|
975
|
-
constructor(message = 'The
|
|
1360
|
+
constructor(message = 'The local bridge model turn was cancelled.') {
|
|
976
1361
|
super(message);
|
|
977
1362
|
this.name = 'CompanionRunCancelledError';
|
|
978
1363
|
this.code = 'RUN_CANCELLED';
|
|
979
1364
|
}
|
|
980
1365
|
}
|
|
981
1366
|
|
|
982
|
-
async function callProviderAdapter(
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1367
|
+
async function callProviderAdapter(
|
|
1368
|
+
adapter,
|
|
1369
|
+
input,
|
|
1370
|
+
{
|
|
1371
|
+
send,
|
|
1372
|
+
trace,
|
|
1373
|
+
controlPollMs = 5_000,
|
|
1374
|
+
providerProgressIntervalMs = 2_000,
|
|
1375
|
+
cancellationGraceMs = 15_000,
|
|
1376
|
+
product,
|
|
1377
|
+
} = {},
|
|
1378
|
+
) {
|
|
1379
|
+
const productName = normalizeBridgeProduct(product).name;
|
|
987
1380
|
if (!adapter || typeof adapter.runModelTurn !== 'function') {
|
|
988
1381
|
throw new Error('The selected provider adapter cannot execute model turns.');
|
|
989
1382
|
}
|
|
@@ -991,12 +1384,81 @@ async function callProviderAdapter(adapter, input, {
|
|
|
991
1384
|
let finished = false;
|
|
992
1385
|
let cancelRequested = false;
|
|
993
1386
|
let cancelSent = false;
|
|
1387
|
+
let cancelTimer = null;
|
|
1388
|
+
let cancelActionPromise = null;
|
|
1389
|
+
let rejectCancellation;
|
|
1390
|
+
let lastProviderProgressSentAt = 0;
|
|
1391
|
+
let activityChain = Promise.resolve();
|
|
1392
|
+
const cancellation = new Promise((_, reject) => {
|
|
1393
|
+
rejectCancellation = reject;
|
|
1394
|
+
});
|
|
994
1395
|
|
|
995
|
-
|
|
1396
|
+
function requestCancel() {
|
|
996
1397
|
cancelRequested = true;
|
|
997
|
-
if (cancelSent) return;
|
|
1398
|
+
if (cancelSent) return cancelActionPromise;
|
|
998
1399
|
cancelSent = true;
|
|
999
|
-
|
|
1400
|
+
cancelTimer = setTimeout(() => {
|
|
1401
|
+
void adapter.resetSession?.(input.sessionId);
|
|
1402
|
+
rejectCancellation(new CompanionRunCancelledError(
|
|
1403
|
+
'The provider did not finish cancelling before the cleanup deadline.',
|
|
1404
|
+
));
|
|
1405
|
+
}, cancellationGraceMs);
|
|
1406
|
+
cancelTimer.unref?.();
|
|
1407
|
+
cancelActionPromise = Promise.resolve()
|
|
1408
|
+
.then(() => adapter.cancel?.(input.sessionId))
|
|
1409
|
+
.catch((error) => {
|
|
1410
|
+
trace?.warn('adapter_cancel_failed', {
|
|
1411
|
+
error: errorMeta(error),
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
return cancelActionPromise;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function waitForCancellationAcknowledgement(providerTurn) {
|
|
1418
|
+
return Promise.race([
|
|
1419
|
+
Promise.allSettled([
|
|
1420
|
+
providerTurn,
|
|
1421
|
+
cancelActionPromise || Promise.resolve(),
|
|
1422
|
+
]),
|
|
1423
|
+
cancellation,
|
|
1424
|
+
]);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function finishCancellation() {
|
|
1428
|
+
return new CompanionRunCancelledError();
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function postActivity(payload) {
|
|
1432
|
+
activityChain = activityChain
|
|
1433
|
+
.then(async () => {
|
|
1434
|
+
if (finished) return;
|
|
1435
|
+
const response = await send('activity', payload);
|
|
1436
|
+
if (controlRequestsCancellation(response)) await requestCancel();
|
|
1437
|
+
})
|
|
1438
|
+
.catch((error) => {
|
|
1439
|
+
if (!finished) {
|
|
1440
|
+
trace?.warn('adapter_activity_post_failed', {
|
|
1441
|
+
error: errorMeta(error),
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
return activityChain;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function reportProviderProgress(progress = {}) {
|
|
1449
|
+
if (finished || cancelRequested) return;
|
|
1450
|
+
const now = Date.now();
|
|
1451
|
+
if (now - lastProviderProgressSentAt < providerProgressIntervalMs) return;
|
|
1452
|
+
lastProviderProgressSentAt = now;
|
|
1453
|
+
void postActivity({
|
|
1454
|
+
stage: 'model_turn',
|
|
1455
|
+
source: 'provider',
|
|
1456
|
+
meaningful: true,
|
|
1457
|
+
kind: progress.kind || 'output_delta',
|
|
1458
|
+
provider: progress.provider || adapter.id || null,
|
|
1459
|
+
occurredAt: progress.occurredAt || new Date(now).toISOString(),
|
|
1460
|
+
message: `${adapter.label || adapter.id || 'Provider'} is actively generating the next ${productName} action.`,
|
|
1461
|
+
});
|
|
1000
1462
|
}
|
|
1001
1463
|
|
|
1002
1464
|
async function monitorControl() {
|
|
@@ -1004,11 +1466,12 @@ async function callProviderAdapter(adapter, input, {
|
|
|
1004
1466
|
try {
|
|
1005
1467
|
await waitForControl(controlPollMs, monitorAbort.signal);
|
|
1006
1468
|
if (finished || monitorAbort.signal.aborted) return;
|
|
1007
|
-
|
|
1469
|
+
await postActivity({
|
|
1008
1470
|
stage: 'model_turn',
|
|
1009
|
-
|
|
1471
|
+
source: 'bridge',
|
|
1472
|
+
meaningful: false,
|
|
1473
|
+
message: `${adapter.label || adapter.id || 'Provider'} is still generating the next ${productName} action.`,
|
|
1010
1474
|
});
|
|
1011
|
-
if (controlRequestsCancellation(response)) await requestCancel();
|
|
1012
1475
|
} catch (error) {
|
|
1013
1476
|
if (monitorAbort.signal.aborted) return;
|
|
1014
1477
|
trace?.warn('adapter_control_poll_failed', { error: errorMeta(error) });
|
|
@@ -1017,15 +1480,28 @@ async function callProviderAdapter(adapter, input, {
|
|
|
1017
1480
|
}
|
|
1018
1481
|
|
|
1019
1482
|
const controlMonitor = monitorControl();
|
|
1483
|
+
const providerTurn = Promise.resolve().then(() =>
|
|
1484
|
+
adapter.runModelTurn({
|
|
1485
|
+
...input,
|
|
1486
|
+
onProgress: reportProviderProgress,
|
|
1487
|
+
}));
|
|
1020
1488
|
try {
|
|
1021
|
-
const result = await
|
|
1022
|
-
|
|
1489
|
+
const result = await Promise.race([providerTurn, cancellation]);
|
|
1490
|
+
await Promise.race([activityChain, cancellation]);
|
|
1491
|
+
if (cancelRequested) {
|
|
1492
|
+
await waitForCancellationAcknowledgement(providerTurn);
|
|
1493
|
+
throw finishCancellation();
|
|
1494
|
+
}
|
|
1023
1495
|
return result;
|
|
1024
1496
|
} catch (error) {
|
|
1025
|
-
if (cancelRequested)
|
|
1497
|
+
if (cancelRequested) {
|
|
1498
|
+
await waitForCancellationAcknowledgement(providerTurn).catch(() => undefined);
|
|
1499
|
+
throw finishCancellation();
|
|
1500
|
+
}
|
|
1026
1501
|
throw error;
|
|
1027
1502
|
} finally {
|
|
1028
1503
|
finished = true;
|
|
1504
|
+
clearTimeout(cancelTimer);
|
|
1029
1505
|
monitorAbort.abort();
|
|
1030
1506
|
await controlMonitor.catch(() => undefined);
|
|
1031
1507
|
}
|
|
@@ -1035,35 +1511,32 @@ export async function resolveAgentRuntime(definition, modelDefinition, options =
|
|
|
1035
1511
|
const platform = options.platform || process.platform;
|
|
1036
1512
|
const childEnv = processEnvWithCliPath(options.env || process.env, platform);
|
|
1037
1513
|
const configuredCommand = commandFromEnv(definition.commandEnv, definition.fallbackCommand, childEnv);
|
|
1038
|
-
const candidates = executableCandidates(
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1514
|
+
const candidates = executableCandidates(configuredCommand, childEnv, platform, options.existsSync || fs.existsSync);
|
|
1515
|
+
const inspect =
|
|
1516
|
+
options.inspect
|
|
1517
|
+
|| (async (command) => {
|
|
1518
|
+
try {
|
|
1519
|
+
const result = await runProcess(command, ['--version'], '', {
|
|
1520
|
+
timeoutMs: 10000,
|
|
1521
|
+
childEnv,
|
|
1522
|
+
platform,
|
|
1523
|
+
});
|
|
1524
|
+
const output = (result.stdout || result.stderr || '').trim();
|
|
1525
|
+
return {
|
|
1526
|
+
ok: true,
|
|
1527
|
+
command,
|
|
1528
|
+
output,
|
|
1529
|
+
version: parseAgentVersion(output),
|
|
1530
|
+
};
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
return {
|
|
1533
|
+
ok: false,
|
|
1534
|
+
command,
|
|
1535
|
+
code: error?.code,
|
|
1536
|
+
error: error?.message || String(error || ''),
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1067
1540
|
const inspections = await Promise.all(candidates.map((candidate) => inspect(candidate)));
|
|
1068
1541
|
return selectAgentRuntime(inspections, definition, modelDefinition);
|
|
1069
1542
|
}
|
|
@@ -1122,7 +1595,10 @@ export async function checkAgentAuthentication(agent, runtime, options = {}) {
|
|
|
1122
1595
|
const childEnv = processEnvWithCliPath(options.env || process.env, platform);
|
|
1123
1596
|
const inspect = options.inspect || inspectClaudeAuthentication;
|
|
1124
1597
|
try {
|
|
1125
|
-
const authentication = await inspect(runtime.command, {
|
|
1598
|
+
const authentication = await inspect(runtime.command, {
|
|
1599
|
+
childEnv,
|
|
1600
|
+
platform,
|
|
1601
|
+
});
|
|
1126
1602
|
if (authentication?.loggedIn === true) {
|
|
1127
1603
|
return {
|
|
1128
1604
|
ok: true,
|
|
@@ -1152,7 +1628,13 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
1152
1628
|
const args = buildAgentArgs(definition, modelDefinition, process.env, {
|
|
1153
1629
|
resumeSessionId: options.resumeSessionId,
|
|
1154
1630
|
outputSchema: options.outputSchema,
|
|
1631
|
+
responseContract: options.responseContract,
|
|
1155
1632
|
step: options.step,
|
|
1633
|
+
product: options.product,
|
|
1634
|
+
nativeSkillsEnabled: options.nativeSkillsEnabled,
|
|
1635
|
+
harnessMode: options.harnessMode,
|
|
1636
|
+
mcpConfig: options.mcpConfig,
|
|
1637
|
+
harnessToolNames: options.harnessToolNames,
|
|
1156
1638
|
});
|
|
1157
1639
|
const timeoutMs = options.timeoutMs || Number(process.env.DEXTER_BRIDGE_AGENT_TIMEOUT_MS || 120000);
|
|
1158
1640
|
const maxDurationMs = boundedDurationMs(
|
|
@@ -1178,13 +1660,17 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
1178
1660
|
maxDurationMs,
|
|
1179
1661
|
trace: options.trace,
|
|
1180
1662
|
signal: options.signal,
|
|
1663
|
+
cwd: options.cwd,
|
|
1664
|
+
onProgress: options.onProgress,
|
|
1665
|
+
stdoutProgress:
|
|
1666
|
+
definition.id === 'claude-code'
|
|
1667
|
+
? createClaudeProgressParser(options.onProgress, { cwd: options.cwd })
|
|
1668
|
+
: undefined,
|
|
1181
1669
|
});
|
|
1182
1670
|
}
|
|
1183
1671
|
|
|
1184
1672
|
function agentErrorWithOutputUsage(agent, error) {
|
|
1185
|
-
const failure = error instanceof Error
|
|
1186
|
-
? error
|
|
1187
|
-
: new Error(String(error || 'The companion model call failed.'));
|
|
1673
|
+
const failure = error instanceof Error ? error : new Error(String(error || 'The companion model call failed.'));
|
|
1188
1674
|
const parsed = parseAgentOutput(agent, failure.stdout || '');
|
|
1189
1675
|
if (!parsed.usageAvailable) return failure;
|
|
1190
1676
|
const accumulator = createCompanionUsageAccumulator(agent);
|
|
@@ -1195,7 +1681,9 @@ function agentErrorWithOutputUsage(agent, error) {
|
|
|
1195
1681
|
}
|
|
1196
1682
|
|
|
1197
1683
|
async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
1198
|
-
|
|
1684
|
+
const product = normalizeBridgeProduct(run?.product || options.product);
|
|
1685
|
+
const productName = product.name;
|
|
1686
|
+
if (normalizeAgentName(agent) === 'dry-run' && !options.providerAdapter) {
|
|
1199
1687
|
await send('done', {
|
|
1200
1688
|
operationType: 'chat',
|
|
1201
1689
|
outcome: 'answer',
|
|
@@ -1210,30 +1698,50 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1210
1698
|
});
|
|
1211
1699
|
return;
|
|
1212
1700
|
}
|
|
1213
|
-
const definition =
|
|
1214
|
-
const requestedModel =
|
|
1215
|
-
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1701
|
+
const definition = definitionForRuntimeProfile(run?.runtimeProfile, agent);
|
|
1702
|
+
const requestedModel =
|
|
1703
|
+
run?.runtimeProfile?.model?.id || run?.companion?.model?.id || run?.model || options.selectedModel;
|
|
1704
|
+
const selectedModel = run?.runtimeProfile?.model || companionModelDefinition(requestedModel, definition.id);
|
|
1705
|
+
const selectedModelId =
|
|
1706
|
+
typeof requestedModel === 'string' && requestedModel.trim()
|
|
1707
|
+
? requestedModel.trim()
|
|
1708
|
+
: selectedModel?.id || definition.model;
|
|
1219
1709
|
const adapter = options.providerAdapter || null;
|
|
1220
1710
|
const sessionKey = companionSessionKey(run);
|
|
1221
1711
|
const rememberedSession = rememberedCompanionSession(sessionKey);
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
|
|
1712
|
+
const nativeSkills = normalizeNativeSkills(run?.modelTurn?.nativeSkills);
|
|
1713
|
+
const nativeProvider =
|
|
1714
|
+
definition.id === 'claude-code'
|
|
1715
|
+
? 'claude-code'
|
|
1716
|
+
: adapter?.id === 'codex' || definition.id === 'codex'
|
|
1717
|
+
? 'codex'
|
|
1718
|
+
: null;
|
|
1719
|
+
const materializedNativeSkills =
|
|
1720
|
+
nativeProvider === 'claude-code'
|
|
1721
|
+
? materializeNativeSkills(nativeSkills, {
|
|
1722
|
+
provider: nativeProvider,
|
|
1723
|
+
sessionId: sessionKey,
|
|
1724
|
+
})
|
|
1725
|
+
: null;
|
|
1726
|
+
const deltaRequested = deltaModelSessionsEnabled(options.env) && run?.modelTurn?.session?.contextMode === 'delta';
|
|
1225
1727
|
let callContextMode = deltaRequested && rememberedSession ? 'delta' : 'full';
|
|
1226
|
-
let prompt =
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1728
|
+
let prompt =
|
|
1729
|
+
callContextMode === 'delta'
|
|
1730
|
+
? buildModelTurnDeltaPrompt(run.modelTurn, product)
|
|
1731
|
+
: deltaRequested
|
|
1732
|
+
? buildModelTurnFallbackPrompt(run.modelTurn, product)
|
|
1733
|
+
: buildModelTurnPrompt(run.modelTurn, product);
|
|
1734
|
+
if (materializedNativeSkills) {
|
|
1735
|
+
prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
|
|
1736
|
+
}
|
|
1231
1737
|
const callStartedAt = Date.now();
|
|
1232
1738
|
let providerSessionId;
|
|
1233
1739
|
let fallbackAfterResumeFailure = false;
|
|
1740
|
+
let providerAttempts = 0;
|
|
1741
|
+
let retryReason = null;
|
|
1234
1742
|
const statusResponse = await send('status', {
|
|
1235
1743
|
stage: 'model_turn',
|
|
1236
|
-
message: `${definition.label} is generating the next
|
|
1744
|
+
message: `${definition.label} is generating the next ${productName} action.`,
|
|
1237
1745
|
});
|
|
1238
1746
|
if (controlRequestsCancellation(statusResponse)) {
|
|
1239
1747
|
if (adapter) await adapter.cancel?.(adapterSessionId(run));
|
|
@@ -1241,32 +1749,35 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1241
1749
|
}
|
|
1242
1750
|
const usageAccumulator = createCompanionUsageAccumulator(definition.id);
|
|
1243
1751
|
const failedModelCall = (error) => {
|
|
1244
|
-
const failure = error instanceof Error
|
|
1245
|
-
? error
|
|
1246
|
-
: new Error(String(error || 'The companion model call failed.'));
|
|
1752
|
+
const failure = error instanceof Error ? error : new Error(String(error || 'The companion model call failed.'));
|
|
1247
1753
|
usageAccumulator.add(failure.companionUsage || {});
|
|
1248
1754
|
const snapshot = usageAccumulator.snapshot();
|
|
1249
1755
|
failure.companionUsage = {
|
|
1250
1756
|
...snapshot,
|
|
1251
|
-
modelCalls: [
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1757
|
+
modelCalls: [
|
|
1758
|
+
{
|
|
1759
|
+
callId: run.runId,
|
|
1760
|
+
step: Number(run?.modelTurn?.step ?? 0),
|
|
1761
|
+
model: selectedModelId,
|
|
1762
|
+
status: 'failed',
|
|
1763
|
+
durationMs: Date.now() - callStartedAt,
|
|
1764
|
+
promptChars: prompt.length,
|
|
1765
|
+
requestedContextMode: deltaRequested ? 'delta' : 'full',
|
|
1766
|
+
contextMode: callContextMode,
|
|
1767
|
+
resumed: callContextMode === 'delta',
|
|
1768
|
+
resumeSessionAvailable: Boolean(rememberedSession),
|
|
1769
|
+
fallbackAfterResumeFailure,
|
|
1770
|
+
providerAttemptCount: Math.max(1, providerAttempts),
|
|
1771
|
+
retryReason,
|
|
1772
|
+
...snapshot.tokenUsage,
|
|
1773
|
+
estimatedCostUSD: snapshot.estimatedCostUSD,
|
|
1774
|
+
},
|
|
1775
|
+
],
|
|
1266
1776
|
};
|
|
1267
1777
|
return failure;
|
|
1268
1778
|
};
|
|
1269
1779
|
let resultText;
|
|
1780
|
+
let nativeSkillStatus = null;
|
|
1270
1781
|
if (adapter) {
|
|
1271
1782
|
const sessionId = sessionKey || adapterSessionId(run);
|
|
1272
1783
|
const invocationModel = adapterInvocationModel(run, selectedModel, definition.id);
|
|
@@ -1287,22 +1798,19 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1287
1798
|
prompt,
|
|
1288
1799
|
model: invocationModel,
|
|
1289
1800
|
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
120000,
|
|
1293
|
-
1000,
|
|
1294
|
-
),
|
|
1801
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
1802
|
+
timeoutMs: boundedDurationMs(options.timeoutMs ?? options.env?.DEXTER_BRIDGE_AGENT_TIMEOUT_MS, 120000, 1000),
|
|
1295
1803
|
maxDurationMs: boundedDurationMs(
|
|
1296
|
-
run?.modelTurn?.maxDurationMs
|
|
1297
|
-
?? options.maxDurationMs
|
|
1298
|
-
?? options.env?.DEXTER_BRIDGE_AGENT_MAX_DURATION_MS,
|
|
1804
|
+
run?.modelTurn?.maxDurationMs ?? options.maxDurationMs ?? options.env?.DEXTER_BRIDGE_AGENT_MAX_DURATION_MS,
|
|
1299
1805
|
600000,
|
|
1300
1806
|
1000,
|
|
1301
1807
|
),
|
|
1808
|
+
nativeSkills,
|
|
1302
1809
|
};
|
|
1303
1810
|
const adapterOptions = {
|
|
1304
1811
|
send,
|
|
1305
1812
|
trace: options.trace,
|
|
1813
|
+
product,
|
|
1306
1814
|
controlPollMs: boundedDurationMs(
|
|
1307
1815
|
options.controlPollMs ?? options.env?.DEXTER_BRIDGE_CONTROL_POLL_MS,
|
|
1308
1816
|
5_000,
|
|
@@ -1313,36 +1821,72 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1313
1821
|
let result;
|
|
1314
1822
|
try {
|
|
1315
1823
|
result = await callProviderAdapter(adapter, adapterInput, adapterOptions);
|
|
1824
|
+
providerAttempts += providerAttemptCount(result);
|
|
1825
|
+
retryReason = result?.retryReason || retryReason;
|
|
1316
1826
|
} catch (error) {
|
|
1317
|
-
|
|
1827
|
+
const retryResumeFailure = callContextMode === 'delta' && resumeFailure(error);
|
|
1828
|
+
const retryFreshSession = freshSessionRetryFailure(error, adapter);
|
|
1829
|
+
providerAttempts += providerAttemptCount(error);
|
|
1830
|
+
if (!retryResumeFailure && !retryFreshSession) {
|
|
1831
|
+
throw failedModelCall(error);
|
|
1832
|
+
}
|
|
1318
1833
|
usageAccumulator.add(error?.companionUsage || {});
|
|
1319
1834
|
fallbackAfterResumeFailure = true;
|
|
1835
|
+
retryReason =
|
|
1836
|
+
error?.code === 'APP_AGENT_TURN_INACTIVITY_TIMEOUT'
|
|
1837
|
+
? 'timeout'
|
|
1838
|
+
: retryFreshSession
|
|
1839
|
+
? 'transport_error'
|
|
1840
|
+
: 'resume_failure';
|
|
1320
1841
|
callContextMode = 'full';
|
|
1321
|
-
prompt = buildModelTurnFallbackPrompt(run.modelTurn);
|
|
1842
|
+
prompt = buildModelTurnFallbackPrompt(run.modelTurn, product);
|
|
1843
|
+
if (materializedNativeSkills) {
|
|
1844
|
+
prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
|
|
1845
|
+
}
|
|
1322
1846
|
await adapter.resetSession?.(sessionId);
|
|
1323
1847
|
options.trace?.warn('agent_adapter_resume_fallback', {
|
|
1324
1848
|
sessionId,
|
|
1325
1849
|
error: errorMeta(error),
|
|
1326
1850
|
fallbackPromptChars: prompt.length,
|
|
1327
1851
|
});
|
|
1328
|
-
|
|
1852
|
+
const retryTimeoutMs = retryFreshSession
|
|
1853
|
+
? boundedDurationMs(options.env?.DEXTER_BRIDGE_CODEX_RETRY_TIMEOUT_MS, 180000, 1000)
|
|
1854
|
+
: adapterInput.timeoutMs;
|
|
1855
|
+
result = await callProviderAdapter(
|
|
1856
|
+
adapter,
|
|
1857
|
+
{
|
|
1858
|
+
...adapterInput,
|
|
1859
|
+
prompt,
|
|
1860
|
+
timeoutMs: retryTimeoutMs,
|
|
1861
|
+
maxTransportCorrectionAttempts: 1,
|
|
1862
|
+
},
|
|
1863
|
+
adapterOptions,
|
|
1864
|
+
)
|
|
1865
|
+
.then((result) => {
|
|
1866
|
+
providerAttempts += providerAttemptCount(result);
|
|
1867
|
+
retryReason = retryReason || result?.retryReason || null;
|
|
1868
|
+
return result;
|
|
1869
|
+
})
|
|
1329
1870
|
.catch((error) => {
|
|
1871
|
+
providerAttempts += providerAttemptCount(error);
|
|
1330
1872
|
throw failedModelCall(error);
|
|
1331
1873
|
});
|
|
1332
1874
|
}
|
|
1333
1875
|
usageAccumulator.add(result);
|
|
1334
1876
|
resultText = result?.text;
|
|
1335
1877
|
providerSessionId = result?.threadId || result?.sessionId;
|
|
1878
|
+
nativeSkillStatus = result?.nativeSkillLifecycle || null;
|
|
1336
1879
|
} else {
|
|
1337
|
-
const runtime =
|
|
1338
|
-
|
|
1339
|
-
|
|
1880
|
+
const runtime =
|
|
1881
|
+
options.runtime
|
|
1882
|
+
|| (await resolveAgentRuntime(definition, selectedModel, {
|
|
1883
|
+
env: options.env,
|
|
1884
|
+
}));
|
|
1340
1885
|
if (!runtime.ok) {
|
|
1341
1886
|
throw new Error(runtime.error || `${definition.label} is not available.`);
|
|
1342
1887
|
}
|
|
1343
1888
|
const resumeSessionId =
|
|
1344
|
-
callContextMode === 'delta'
|
|
1345
|
-
&& agentSessionResumeEnabled(definition, options.env)
|
|
1889
|
+
callContextMode === 'delta' && agentSessionResumeEnabled(definition, options.env)
|
|
1346
1890
|
? rememberedSession?.providerSessionId
|
|
1347
1891
|
: undefined;
|
|
1348
1892
|
const cliAbort = new AbortController();
|
|
@@ -1355,23 +1899,60 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1355
1899
|
);
|
|
1356
1900
|
let cliFinished = false;
|
|
1357
1901
|
let cliCancelled = false;
|
|
1902
|
+
let lastProviderProgressSentAt = 0;
|
|
1903
|
+
let activityChain = Promise.resolve();
|
|
1904
|
+
const requestCliCancel = () => {
|
|
1905
|
+
if (cliCancelled) return;
|
|
1906
|
+
cliCancelled = true;
|
|
1907
|
+
cliAbort.abort(
|
|
1908
|
+
Object.assign(
|
|
1909
|
+
new Error(`The ${productName} bridge model turn was cancelled.`),
|
|
1910
|
+
{ code: 'RUN_CANCELLED' },
|
|
1911
|
+
),
|
|
1912
|
+
);
|
|
1913
|
+
};
|
|
1914
|
+
const postCliActivity = (payload) => {
|
|
1915
|
+
activityChain = activityChain
|
|
1916
|
+
.then(async () => {
|
|
1917
|
+
const response = await send('activity', payload);
|
|
1918
|
+
if (controlRequestsCancellation(response)) requestCliCancel();
|
|
1919
|
+
})
|
|
1920
|
+
.catch((error) => {
|
|
1921
|
+
if (!cliFinished) {
|
|
1922
|
+
options.trace?.warn('cli_activity_post_failed', {
|
|
1923
|
+
error: errorMeta(error),
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
});
|
|
1927
|
+
return activityChain;
|
|
1928
|
+
};
|
|
1929
|
+
const reportCliProgress = (progress = {}) => {
|
|
1930
|
+
if (cliFinished || cliCancelled) return;
|
|
1931
|
+
const now = Date.now();
|
|
1932
|
+
if (now - lastProviderProgressSentAt < 2_000) return;
|
|
1933
|
+
lastProviderProgressSentAt = now;
|
|
1934
|
+
void postCliActivity({
|
|
1935
|
+
stage: 'model_turn',
|
|
1936
|
+
source: 'provider',
|
|
1937
|
+
meaningful: true,
|
|
1938
|
+
kind: progress.kind || 'output_delta',
|
|
1939
|
+
provider: definition.id,
|
|
1940
|
+
occurredAt: progress.occurredAt || new Date(now).toISOString(),
|
|
1941
|
+
message: `${definition.label} is actively generating the next ${productName} action.`,
|
|
1942
|
+
});
|
|
1943
|
+
};
|
|
1358
1944
|
const controlMonitor = (async () => {
|
|
1359
1945
|
while (!cliFinished && !monitorAbort.signal.aborted) {
|
|
1360
1946
|
try {
|
|
1361
1947
|
await waitForControl(controlPollMs, monitorAbort.signal);
|
|
1362
1948
|
if (cliFinished || monitorAbort.signal.aborted) return;
|
|
1363
|
-
|
|
1949
|
+
await postCliActivity({
|
|
1364
1950
|
stage: 'model_turn',
|
|
1365
|
-
|
|
1951
|
+
source: 'bridge',
|
|
1952
|
+
meaningful: false,
|
|
1953
|
+
message: `${definition.label} is still generating the next ${productName} action.`,
|
|
1366
1954
|
});
|
|
1367
|
-
if (
|
|
1368
|
-
cliCancelled = true;
|
|
1369
|
-
cliAbort.abort(Object.assign(
|
|
1370
|
-
new Error('The Dexter companion model turn was cancelled.'),
|
|
1371
|
-
{ code: 'RUN_CANCELLED' },
|
|
1372
|
-
));
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1955
|
+
if (cliCancelled) return;
|
|
1375
1956
|
} catch (error) {
|
|
1376
1957
|
if (monitorAbort.signal.aborted) return;
|
|
1377
1958
|
options.trace?.warn('cli_control_poll_failed', {
|
|
@@ -1388,9 +1969,14 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1388
1969
|
runtime,
|
|
1389
1970
|
resumeSessionId,
|
|
1390
1971
|
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
1972
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
1391
1973
|
step: run?.modelTurn?.step,
|
|
1974
|
+
product,
|
|
1392
1975
|
maxDurationMs: run?.modelTurn?.maxDurationMs ?? options.maxDurationMs,
|
|
1393
1976
|
signal: cliAbort.signal,
|
|
1977
|
+
cwd: materializedNativeSkills?.workspace,
|
|
1978
|
+
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
1979
|
+
onProgress: reportCliProgress,
|
|
1394
1980
|
});
|
|
1395
1981
|
} catch (error) {
|
|
1396
1982
|
if (cliCancelled || error?.code === 'RUN_CANCELLED') {
|
|
@@ -1401,7 +1987,10 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1401
1987
|
usageAccumulator.add(failure.companionUsage || {});
|
|
1402
1988
|
fallbackAfterResumeFailure = true;
|
|
1403
1989
|
callContextMode = 'full';
|
|
1404
|
-
prompt = buildModelTurnFallbackPrompt(run.modelTurn);
|
|
1990
|
+
prompt = buildModelTurnFallbackPrompt(run.modelTurn, product);
|
|
1991
|
+
if (materializedNativeSkills) {
|
|
1992
|
+
prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
|
|
1993
|
+
}
|
|
1405
1994
|
options.trace?.warn('agent_cli_resume_fallback', {
|
|
1406
1995
|
resumeSessionId,
|
|
1407
1996
|
error: errorMeta(failure),
|
|
@@ -1413,9 +2002,14 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1413
2002
|
runtime,
|
|
1414
2003
|
resumeSessionId: undefined,
|
|
1415
2004
|
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
2005
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
1416
2006
|
step: run?.modelTurn?.step,
|
|
2007
|
+
product,
|
|
1417
2008
|
maxDurationMs: run?.modelTurn?.maxDurationMs ?? options.maxDurationMs,
|
|
1418
2009
|
signal: cliAbort.signal,
|
|
2010
|
+
cwd: materializedNativeSkills?.workspace,
|
|
2011
|
+
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2012
|
+
onProgress: reportCliProgress,
|
|
1419
2013
|
}).catch((error) => {
|
|
1420
2014
|
if (cliCancelled || error?.code === 'RUN_CANCELLED') {
|
|
1421
2015
|
throw new CompanionRunCancelledError();
|
|
@@ -1427,10 +2021,19 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1427
2021
|
monitorAbort.abort();
|
|
1428
2022
|
await controlMonitor.catch(() => undefined);
|
|
1429
2023
|
}
|
|
2024
|
+
await activityChain;
|
|
2025
|
+
if (cliCancelled) throw new CompanionRunCancelledError();
|
|
1430
2026
|
const parsed = parseAgentOutput(definition.id, result.stdout);
|
|
1431
2027
|
usageAccumulator.add(parsed);
|
|
1432
2028
|
resultText = parsed.resultText;
|
|
1433
2029
|
providerSessionId = parsed.sessionId;
|
|
2030
|
+
nativeSkillStatus = materializedNativeSkills
|
|
2031
|
+
? nativeSkillLifecycle(
|
|
2032
|
+
materializedNativeSkills,
|
|
2033
|
+
'slash-command',
|
|
2034
|
+
callContextMode === 'delta',
|
|
2035
|
+
)
|
|
2036
|
+
: null;
|
|
1434
2037
|
}
|
|
1435
2038
|
if (providerSessionId || adapter) {
|
|
1436
2039
|
rememberCompanionSession(sessionKey, {
|
|
@@ -1451,6 +2054,8 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1451
2054
|
resumed: callContextMode === 'delta',
|
|
1452
2055
|
resumeSessionAvailable: Boolean(rememberedSession),
|
|
1453
2056
|
fallbackAfterResumeFailure,
|
|
2057
|
+
providerAttemptCount: Math.max(1, providerAttempts),
|
|
2058
|
+
retryReason,
|
|
1454
2059
|
...usageSnapshot.tokenUsage,
|
|
1455
2060
|
estimatedCostUSD: usageSnapshot.estimatedCostUSD,
|
|
1456
2061
|
};
|
|
@@ -1460,10 +2065,7 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1460
2065
|
};
|
|
1461
2066
|
let completion;
|
|
1462
2067
|
try {
|
|
1463
|
-
completion = normalizeModelTurnCompletion(
|
|
1464
|
-
extractJsonObject(resultText),
|
|
1465
|
-
selectedModelId,
|
|
1466
|
-
);
|
|
2068
|
+
completion = normalizeModelTurnCompletion(extractJsonObject(resultText), selectedModelId);
|
|
1467
2069
|
} catch (error) {
|
|
1468
2070
|
error.companionUsage = {
|
|
1469
2071
|
...usage,
|
|
@@ -1476,30 +2078,507 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1476
2078
|
outcome: 'answer',
|
|
1477
2079
|
completion,
|
|
1478
2080
|
model: selectedModelId,
|
|
2081
|
+
...(nativeSkillStatus
|
|
2082
|
+
? { nativeSkillLifecycle: nativeSkillStatus }
|
|
2083
|
+
: {}),
|
|
1479
2084
|
...usage,
|
|
1480
2085
|
});
|
|
1481
2086
|
}
|
|
1482
2087
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
2088
|
+
async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
2089
|
+
const product = normalizeBridgeProduct(run?.product || options.product);
|
|
2090
|
+
const definition = definitionForRuntimeProfile(run?.runtimeProfile, agent);
|
|
2091
|
+
const requestedModel =
|
|
2092
|
+
run?.runtimeProfile?.model?.id
|
|
2093
|
+
|| run?.companion?.model?.id
|
|
2094
|
+
|| run?.model
|
|
2095
|
+
|| options.selectedModel;
|
|
2096
|
+
const selectedModel =
|
|
2097
|
+
run?.runtimeProfile?.model
|
|
2098
|
+
|| companionModelDefinition(requestedModel, definition.id);
|
|
2099
|
+
const selectedModelId =
|
|
2100
|
+
typeof requestedModel === 'string' && requestedModel.trim()
|
|
2101
|
+
? requestedModel.trim()
|
|
2102
|
+
: selectedModel?.id || definition.model;
|
|
2103
|
+
const outcomeMaxDurationMs = boundedDurationMs(
|
|
2104
|
+
run?.outcome?.maxDurationMs
|
|
2105
|
+
?? options.maxDurationMs
|
|
2106
|
+
?? options.env?.DEXTER_BRIDGE_AGENT_MAX_DURATION_MS,
|
|
2107
|
+
20 * 60_000,
|
|
2108
|
+
1_000,
|
|
2109
|
+
);
|
|
2110
|
+
const outcomeInactivityTimeoutMs = Math.min(
|
|
2111
|
+
outcomeMaxDurationMs,
|
|
2112
|
+
boundedDurationMs(
|
|
2113
|
+
options.timeoutMs ?? options.env?.DEXTER_BRIDGE_AGENT_TIMEOUT_MS,
|
|
2114
|
+
10 * 60_000,
|
|
2115
|
+
1_000,
|
|
2116
|
+
),
|
|
2117
|
+
);
|
|
2118
|
+
const sessionKey = companionSessionKey(run);
|
|
2119
|
+
const infrastructureAbortController = new AbortController();
|
|
2120
|
+
const controlMonitorAbort = new AbortController();
|
|
2121
|
+
let finished = false;
|
|
2122
|
+
let cancelRequested = false;
|
|
2123
|
+
let cancelAction = null;
|
|
2124
|
+
let rejectCancellation;
|
|
2125
|
+
let latestTokenUsage = normalizeCompanionTokenUsage();
|
|
2126
|
+
let activityChain = Promise.resolve();
|
|
2127
|
+
const cancellation = new Promise((_, reject) => {
|
|
2128
|
+
rejectCancellation = reject;
|
|
2129
|
+
});
|
|
2130
|
+
const cancellationError = () => {
|
|
2131
|
+
const error = new CompanionRunCancelledError();
|
|
2132
|
+
error.companionUsage = {
|
|
2133
|
+
tokenUsage: latestTokenUsage,
|
|
2134
|
+
usageAvailable: latestTokenUsage.totalTokens > 0,
|
|
2135
|
+
usageSource: definition.id,
|
|
2136
|
+
usageAccuracy: latestTokenUsage.totalTokens > 0 ? 'reported' : 'unavailable',
|
|
2137
|
+
};
|
|
2138
|
+
return error;
|
|
2139
|
+
};
|
|
2140
|
+
const requestCancel = () => {
|
|
2141
|
+
if (cancelRequested) return cancelAction;
|
|
2142
|
+
cancelRequested = true;
|
|
2143
|
+
infrastructureAbortController.abort();
|
|
2144
|
+
cancelAction = Promise.resolve(options.providerAdapter?.cancel?.(sessionKey))
|
|
2145
|
+
.catch((error) => {
|
|
2146
|
+
options.trace?.warn?.('outcome_cancel_failed', { error: errorMeta(error) });
|
|
2147
|
+
});
|
|
2148
|
+
rejectCancellation(cancellationError());
|
|
2149
|
+
return cancelAction;
|
|
2150
|
+
};
|
|
2151
|
+
const postActivity = (payload) => {
|
|
2152
|
+
activityChain = activityChain
|
|
2153
|
+
.then(async () => {
|
|
2154
|
+
if (finished) return;
|
|
2155
|
+
const response = await send('activity', payload);
|
|
2156
|
+
if (controlRequestsCancellation(response)) await requestCancel();
|
|
2157
|
+
})
|
|
2158
|
+
.catch((error) => {
|
|
2159
|
+
if (!finished) {
|
|
2160
|
+
options.trace?.warn?.('outcome_activity_post_failed', {
|
|
2161
|
+
error: errorMeta(error),
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
return activityChain;
|
|
2166
|
+
};
|
|
2167
|
+
const progress = (event = {}) => {
|
|
2168
|
+
if (event.tokenUsage) {
|
|
2169
|
+
latestTokenUsage = normalizeCompanionTokenUsage(event.tokenUsage);
|
|
2170
|
+
}
|
|
2171
|
+
if (event.kind === 'token_usage') {
|
|
2172
|
+
void postActivity({
|
|
2173
|
+
stage: 'outcome',
|
|
2174
|
+
outcomeId: run?.outcome?.outcomeId,
|
|
2175
|
+
source: event.source || 'provider',
|
|
2176
|
+
meaningful: false,
|
|
2177
|
+
kind: 'token_usage',
|
|
2178
|
+
provider: definition.id,
|
|
2179
|
+
tokenUsage: event.tokenUsage || undefined,
|
|
2180
|
+
occurredAt: event.occurredAt || new Date().toISOString(),
|
|
2181
|
+
});
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
const meaningful =
|
|
2185
|
+
Boolean(event.message || event.name) ||
|
|
2186
|
+
event.kind === 'turn_started';
|
|
2187
|
+
if (!meaningful) return;
|
|
2188
|
+
const userFacingUpdate = event.kind === 'status_update';
|
|
2189
|
+
void postActivity({
|
|
2190
|
+
stage: 'outcome',
|
|
2191
|
+
outcomeId: run?.outcome?.outcomeId,
|
|
2192
|
+
source: event.source || 'provider',
|
|
2193
|
+
meaningful: true,
|
|
2194
|
+
kind: userFacingUpdate ? 'output_delta' : event.kind || 'output_delta',
|
|
2195
|
+
provider: definition.id,
|
|
2196
|
+
tool: event.name || undefined,
|
|
2197
|
+
toolPhase: event.phase || undefined,
|
|
2198
|
+
activity:
|
|
2199
|
+
event.activity ||
|
|
2200
|
+
(userFacingUpdate ? `assistant-progress:${Date.now()}` : undefined),
|
|
2201
|
+
paths: event.paths || undefined,
|
|
2202
|
+
tokenUsage: event.tokenUsage || undefined,
|
|
2203
|
+
occurredAt: event.occurredAt || new Date().toISOString(),
|
|
2204
|
+
message:
|
|
2205
|
+
event.message ||
|
|
2206
|
+
(event.name
|
|
2207
|
+
? `${definition.label} is using ${event.name}.`
|
|
2208
|
+
: `${definition.label} started implementing your request.`),
|
|
2209
|
+
});
|
|
2210
|
+
};
|
|
2211
|
+
const monitorControl = async () => {
|
|
2212
|
+
while (!finished && !controlMonitorAbort.signal.aborted) {
|
|
2213
|
+
try {
|
|
2214
|
+
await waitForControl(
|
|
2215
|
+
Math.max(500, Number(options.controlPollMs) || 5_000),
|
|
2216
|
+
controlMonitorAbort.signal,
|
|
2217
|
+
);
|
|
2218
|
+
if (finished || controlMonitorAbort.signal.aborted) return;
|
|
2219
|
+
const response = await send('activity', {
|
|
2220
|
+
stage: 'outcome',
|
|
2221
|
+
outcomeId: run?.outcome?.outcomeId,
|
|
2222
|
+
source: 'bridge',
|
|
2223
|
+
meaningful: false,
|
|
2224
|
+
kind: 'heartbeat',
|
|
2225
|
+
provider: definition.id,
|
|
2226
|
+
occurredAt: new Date().toISOString(),
|
|
2227
|
+
message: `${definition.label} is still working.`,
|
|
2228
|
+
});
|
|
2229
|
+
if (controlRequestsCancellation(response)) await requestCancel();
|
|
2230
|
+
} catch (error) {
|
|
2231
|
+
if (!controlMonitorAbort.signal.aborted && !cancelRequested) {
|
|
2232
|
+
options.trace?.warn?.('outcome_control_poll_failed', {
|
|
2233
|
+
error: errorMeta(error),
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
const controlMonitor = monitorControl();
|
|
2240
|
+
const rememberedSession =
|
|
2241
|
+
rememberedCompanionSession(sessionKey) ||
|
|
2242
|
+
(run?.outcome?.session?.providerSessionId
|
|
2243
|
+
? {
|
|
2244
|
+
providerSessionId: run.outcome.session.providerSessionId,
|
|
2245
|
+
agent: definition.id,
|
|
2246
|
+
updatedAt: Date.now(),
|
|
2247
|
+
}
|
|
2248
|
+
: null);
|
|
2249
|
+
const framerAgentOutcome =
|
|
2250
|
+
run?.outcome?.kind === 'framer-project'
|
|
2251
|
+
|| run?.outcome?.context?.executionMode === 'framer-agent';
|
|
2252
|
+
const materialized = materializeOutcomeWorkspace(run?.outcome, {
|
|
2253
|
+
sessionId: run?.outcome?.context?.harnessSessionId || sessionKey,
|
|
2254
|
+
});
|
|
2255
|
+
const nativeSkills = normalizeNativeSkills(run?.outcome?.nativeSkills);
|
|
2256
|
+
const nativeProvider =
|
|
2257
|
+
definition.id === 'claude-code'
|
|
2258
|
+
? 'claude-code'
|
|
2259
|
+
: definition.id === 'opencode' ||
|
|
2260
|
+
options.providerAdapter?.driverKind === 'opencode'
|
|
2261
|
+
? 'opencode'
|
|
2262
|
+
: options.providerAdapter?.id === 'codex' || definition.id === 'codex'
|
|
2263
|
+
? 'codex'
|
|
2264
|
+
: null;
|
|
2265
|
+
const materializedNativeSkills =
|
|
2266
|
+
nativeProvider === 'claude-code' || nativeProvider === 'opencode'
|
|
2267
|
+
? materializeNativeSkills(nativeSkills, {
|
|
2268
|
+
provider: nativeProvider,
|
|
2269
|
+
sessionId: run?.outcome?.context?.harnessSessionId || sessionKey,
|
|
2270
|
+
projectDirectory: materialized.directory,
|
|
2271
|
+
})
|
|
2272
|
+
: null;
|
|
2273
|
+
const harnessTools = framerAgentOutcome
|
|
2274
|
+
? createFramerAgentToolRuntime({
|
|
2275
|
+
assignment: run?.outcome,
|
|
2276
|
+
cwd: materialized.directory,
|
|
2277
|
+
env: options.env,
|
|
2278
|
+
trace: options.trace,
|
|
2279
|
+
onActivity: progress,
|
|
2280
|
+
runCli: options.framerAgentRunCli,
|
|
2281
|
+
authorizeProject:
|
|
2282
|
+
options.apiBaseUrl && options.deviceToken
|
|
2283
|
+
? ({ initiate, forceRefresh, signal }) =>
|
|
2284
|
+
getFramerProjectAuthorization(options.apiBaseUrl, {
|
|
2285
|
+
deviceToken: options.deviceToken,
|
|
2286
|
+
runId: run.runId,
|
|
2287
|
+
initiate,
|
|
2288
|
+
forceRefresh,
|
|
2289
|
+
fetchImpl: options.fetchImpl,
|
|
2290
|
+
signal,
|
|
2291
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
2292
|
+
})
|
|
2293
|
+
: undefined,
|
|
2294
|
+
openAuthorizationUrl: options.openAuthorizationUrl,
|
|
2295
|
+
})
|
|
2296
|
+
: createHarnessToolRuntime({
|
|
2297
|
+
assignment: run?.outcome,
|
|
2298
|
+
materialized,
|
|
2299
|
+
send,
|
|
2300
|
+
trace: options.trace,
|
|
2301
|
+
onActivity: progress,
|
|
2302
|
+
});
|
|
2303
|
+
let prompt = buildOutcomePrompt(run.outcome, product);
|
|
2304
|
+
if (materializedNativeSkills) {
|
|
2305
|
+
prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
|
|
2306
|
+
}
|
|
2307
|
+
const schema = outcomeOutputSchema();
|
|
2308
|
+
assertOutcomeResponseContract(
|
|
2309
|
+
STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
2310
|
+
schema,
|
|
2311
|
+
);
|
|
2312
|
+
const callStartedAt = Date.now();
|
|
2313
|
+
const usageAccumulator = createCompanionUsageAccumulator(definition.id);
|
|
2314
|
+
let providerAttempts = 0;
|
|
2315
|
+
let result;
|
|
2316
|
+
try {
|
|
2317
|
+
if (framerAgentOutcome) {
|
|
2318
|
+
await Promise.race([harnessTools.preflight(), cancellation]);
|
|
2319
|
+
}
|
|
2320
|
+
const gateway =
|
|
2321
|
+
definition.id === 'codex' ? null : await harnessTools.startGateway();
|
|
2322
|
+
const mcpServer = gateway
|
|
2323
|
+
? {
|
|
2324
|
+
command: process.execPath,
|
|
2325
|
+
args: [HARNESS_MCP_SERVER_PATH],
|
|
2326
|
+
cwd: materialized.directory,
|
|
2327
|
+
environment: harnessMcpProcessEnvironment(
|
|
2328
|
+
gateway,
|
|
2329
|
+
process.versions,
|
|
2330
|
+
harnessTools.toolNames,
|
|
2331
|
+
),
|
|
2332
|
+
}
|
|
2333
|
+
: null;
|
|
2334
|
+
const mcpConfig = mcpServer
|
|
2335
|
+
? {
|
|
2336
|
+
mcpServers: {
|
|
2337
|
+
instawebai: {
|
|
2338
|
+
command: mcpServer.command,
|
|
2339
|
+
args: mcpServer.args,
|
|
2340
|
+
env: mcpServer.environment,
|
|
2341
|
+
},
|
|
2342
|
+
},
|
|
2343
|
+
}
|
|
2344
|
+
: null;
|
|
2345
|
+
const statusResponse = await send('status', {
|
|
2346
|
+
stage: 'outcome',
|
|
2347
|
+
outcomeId: run?.outcome?.outcomeId,
|
|
2348
|
+
message: `${definition.label} is implementing ${run?.outcome?.title || 'the requested outcome'}.`,
|
|
2349
|
+
});
|
|
2350
|
+
if (controlRequestsCancellation(statusResponse)) {
|
|
2351
|
+
await options.providerAdapter?.cancel?.(sessionKey);
|
|
2352
|
+
throw new CompanionRunCancelledError();
|
|
2353
|
+
}
|
|
2354
|
+
const providerExecution = (async () => {
|
|
2355
|
+
if (options.providerAdapter?.runOutcome) {
|
|
2356
|
+
const providerResult = await options.providerAdapter.runOutcome({
|
|
2357
|
+
runId: run.runId,
|
|
2358
|
+
sessionId: sessionKey,
|
|
2359
|
+
resumeSessionId: rememberedSession?.providerSessionId,
|
|
2360
|
+
prompt,
|
|
2361
|
+
model: adapterInvocationModel(run, selectedModel, definition.id),
|
|
2362
|
+
outputSchema: schema,
|
|
2363
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
2364
|
+
timeoutMs: outcomeInactivityTimeoutMs,
|
|
2365
|
+
maxDurationMs: outcomeMaxDurationMs,
|
|
2366
|
+
cwd: materialized.directory,
|
|
2367
|
+
nativeSkills,
|
|
2368
|
+
framerHarness: framerAgentOutcome,
|
|
2369
|
+
harnessTools: {
|
|
2370
|
+
definitions: codexDynamicToolSpecs(harnessTools.definitions),
|
|
2371
|
+
invoke: harnessTools.invoke,
|
|
2372
|
+
...(mcpConfig ? { mcpConfig } : {}),
|
|
2373
|
+
...(mcpServer ? { mcpServer } : {}),
|
|
2374
|
+
},
|
|
2375
|
+
onProgress: progress,
|
|
2376
|
+
});
|
|
2377
|
+
providerAttempts += providerAttemptCount(providerResult);
|
|
2378
|
+
return providerResult;
|
|
2379
|
+
}
|
|
2380
|
+
const runtime =
|
|
2381
|
+
options.runtime
|
|
2382
|
+
|| (await resolveAgentRuntime(definition, selectedModel, {
|
|
2383
|
+
env: options.env,
|
|
2384
|
+
}));
|
|
2385
|
+
if (!runtime.ok) {
|
|
2386
|
+
throw new Error(runtime.error || `${definition.label} is not available.`);
|
|
2387
|
+
}
|
|
2388
|
+
const resumeSessionId =
|
|
2389
|
+
agentSessionResumeEnabled(definition, options.env)
|
|
2390
|
+
? rememberedSession?.providerSessionId
|
|
2391
|
+
: undefined;
|
|
2392
|
+
let processResult;
|
|
2393
|
+
try {
|
|
2394
|
+
providerAttempts += 1;
|
|
2395
|
+
processResult = await callLocalJsonAgent(definition.id, prompt, {
|
|
2396
|
+
...options,
|
|
2397
|
+
model: selectedModelId,
|
|
2398
|
+
timeoutMs: outcomeInactivityTimeoutMs,
|
|
2399
|
+
runtime,
|
|
2400
|
+
resumeSessionId,
|
|
2401
|
+
outputSchema: schema,
|
|
2402
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
2403
|
+
product,
|
|
2404
|
+
step: 0,
|
|
2405
|
+
maxDurationMs: outcomeMaxDurationMs,
|
|
2406
|
+
cwd: materialized.directory,
|
|
2407
|
+
harnessMode: true,
|
|
2408
|
+
framerHarness: framerAgentOutcome,
|
|
2409
|
+
harnessToolNames: harnessTools.toolNames,
|
|
2410
|
+
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2411
|
+
signal: infrastructureAbortController.signal,
|
|
2412
|
+
...(mcpConfig ? { mcpConfig } : {}),
|
|
2413
|
+
onProgress: progress,
|
|
2414
|
+
});
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
if (!resumeSessionId || !resumeFailure(error)) throw error;
|
|
2417
|
+
providerAttempts += 1;
|
|
2418
|
+
processResult = await callLocalJsonAgent(definition.id, prompt, {
|
|
2419
|
+
...options,
|
|
2420
|
+
model: selectedModelId,
|
|
2421
|
+
timeoutMs: outcomeInactivityTimeoutMs,
|
|
2422
|
+
runtime,
|
|
2423
|
+
outputSchema: schema,
|
|
2424
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
|
|
2425
|
+
product,
|
|
2426
|
+
step: 0,
|
|
2427
|
+
maxDurationMs: outcomeMaxDurationMs,
|
|
2428
|
+
cwd: materialized.directory,
|
|
2429
|
+
harnessMode: true,
|
|
2430
|
+
framerHarness: framerAgentOutcome,
|
|
2431
|
+
harnessToolNames: harnessTools.toolNames,
|
|
2432
|
+
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2433
|
+
signal: infrastructureAbortController.signal,
|
|
2434
|
+
...(mcpConfig ? { mcpConfig } : {}),
|
|
2435
|
+
onProgress: progress,
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
const parsedOutput = parseAgentOutput(definition.id, processResult.stdout);
|
|
2439
|
+
return {
|
|
2440
|
+
...parsedOutput,
|
|
2441
|
+
text: parsedOutput.resultText,
|
|
2442
|
+
sessionId: parsedOutput.sessionId,
|
|
2443
|
+
};
|
|
2444
|
+
})();
|
|
2445
|
+
const infrastructureFailure = harnessTools
|
|
2446
|
+
.waitForFatalInfrastructure()
|
|
2447
|
+
.then(async (error) => {
|
|
2448
|
+
options.trace?.error?.('harness_infrastructure_failed', {
|
|
2449
|
+
code: error?.code,
|
|
2450
|
+
message: error?.message,
|
|
2451
|
+
});
|
|
2452
|
+
infrastructureAbortController.abort(error);
|
|
2453
|
+
await Promise.resolve(
|
|
2454
|
+
options.providerAdapter?.cancel?.(sessionKey),
|
|
2455
|
+
).catch(() => undefined);
|
|
2456
|
+
throw error;
|
|
2457
|
+
});
|
|
2458
|
+
try {
|
|
2459
|
+
result = await Promise.race([
|
|
2460
|
+
providerExecution,
|
|
2461
|
+
infrastructureFailure,
|
|
2462
|
+
cancellation,
|
|
2463
|
+
]);
|
|
2464
|
+
await Promise.race([activityChain, cancellation]);
|
|
2465
|
+
} catch (error) {
|
|
2466
|
+
if (cancelRequested) throw cancellationError();
|
|
2467
|
+
throw error;
|
|
2468
|
+
}
|
|
2469
|
+
const providerSessionId = result?.threadId || result?.sessionId;
|
|
2470
|
+
if (providerSessionId || options.providerAdapter) {
|
|
2471
|
+
rememberCompanionSession(sessionKey, {
|
|
2472
|
+
providerSessionId:
|
|
2473
|
+
providerSessionId ||
|
|
2474
|
+
rememberedSession?.providerSessionId ||
|
|
2475
|
+
sessionKey,
|
|
2476
|
+
agent: definition.id,
|
|
2477
|
+
updatedAt: Date.now(),
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
usageAccumulator.add(result);
|
|
2481
|
+
const completion = normalizeOutcomeCompletion(
|
|
2482
|
+
extractJsonObject(result?.text || result?.resultText || ''),
|
|
2483
|
+
run?.outcome?.outcomeId,
|
|
2484
|
+
);
|
|
2485
|
+
let synchronization = null;
|
|
2486
|
+
if (harnessTools.directWorkspace) {
|
|
2487
|
+
synchronization = await harnessTools.inspect();
|
|
2488
|
+
} else if (
|
|
2489
|
+
completion.status === 'ready_for_verification'
|
|
2490
|
+
|| harnessTools.hasPendingChanges()
|
|
2491
|
+
) {
|
|
2492
|
+
synchronization = await harnessTools.synchronize();
|
|
2493
|
+
}
|
|
2494
|
+
const files = harnessTools.directWorkspace
|
|
2495
|
+
? []
|
|
2496
|
+
: collectOutcomeWorkspaceChanges(run.outcome, materialized);
|
|
2497
|
+
const normalized = synchronization
|
|
2498
|
+
? {
|
|
2499
|
+
...completion,
|
|
2500
|
+
synchronized: synchronization.synchronized !== false,
|
|
2501
|
+
sourceHash:
|
|
2502
|
+
synchronization?.sourceHash || harnessTools.sourceHash() || null,
|
|
2503
|
+
...(typeof harnessTools.summary === 'function'
|
|
2504
|
+
? { framer: harnessTools.summary() }
|
|
2505
|
+
: {}),
|
|
2506
|
+
}
|
|
2507
|
+
: completion;
|
|
2508
|
+
const usageSnapshot = usageAccumulator.snapshot();
|
|
2509
|
+
await send('done', {
|
|
2510
|
+
operationType: 'outcome',
|
|
2511
|
+
outcome: {
|
|
2512
|
+
...normalized,
|
|
2513
|
+
files,
|
|
2514
|
+
},
|
|
2515
|
+
model: selectedModelId,
|
|
2516
|
+
providerSessionId:
|
|
2517
|
+
providerSessionId ||
|
|
2518
|
+
rememberedSession?.providerSessionId ||
|
|
2519
|
+
sessionKey,
|
|
2520
|
+
...usageSnapshot,
|
|
2521
|
+
modelCalls: [
|
|
2522
|
+
{
|
|
2523
|
+
callId: run.runId,
|
|
2524
|
+
outcomeId: run?.outcome?.outcomeId,
|
|
2525
|
+
model: selectedModelId,
|
|
2526
|
+
status:
|
|
2527
|
+
normalized.status === 'ready_for_verification'
|
|
2528
|
+
? 'succeeded'
|
|
2529
|
+
: normalized.status,
|
|
2530
|
+
durationMs: Date.now() - callStartedAt,
|
|
2531
|
+
providerAttemptCount: Math.max(1, providerAttempts),
|
|
2532
|
+
...usageSnapshot.tokenUsage,
|
|
2533
|
+
estimatedCostUSD: usageSnapshot.estimatedCostUSD,
|
|
2534
|
+
},
|
|
2535
|
+
],
|
|
2536
|
+
});
|
|
2537
|
+
} finally {
|
|
2538
|
+
finished = true;
|
|
2539
|
+
controlMonitorAbort.abort();
|
|
2540
|
+
await controlMonitor.catch(() => undefined);
|
|
2541
|
+
await harnessTools
|
|
2542
|
+
.close({ waitForPending: !cancelRequested })
|
|
2543
|
+
.catch(() => undefined);
|
|
2544
|
+
removeOutcomeWorkspace(materialized);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
export async function executeRun(
|
|
2549
|
+
run,
|
|
2550
|
+
{
|
|
2551
|
+
apiBaseUrl,
|
|
2552
|
+
deviceToken,
|
|
2553
|
+
agent = process.env.DEXTER_BRIDGE_AGENT || DEFAULT_BRIDGE_AGENT,
|
|
2554
|
+
product: configuredProduct,
|
|
2555
|
+
fetchImpl,
|
|
2556
|
+
log = console.log,
|
|
2557
|
+
logDir,
|
|
2558
|
+
selectedModel,
|
|
2559
|
+
providerAdapter,
|
|
2560
|
+
adapterOptions,
|
|
2561
|
+
env = process.env,
|
|
2562
|
+
controlPollMs,
|
|
2563
|
+
inspectAgentAuthentication,
|
|
2564
|
+
runtimeProfile: suppliedRuntimeProfile,
|
|
2565
|
+
framerAgentRunCli,
|
|
2566
|
+
openAuthorizationUrl,
|
|
2567
|
+
allowInsecureHttp = false,
|
|
2568
|
+
} = {},
|
|
2569
|
+
) {
|
|
2570
|
+
const product = normalizeBridgeProduct(run?.product || configuredProduct);
|
|
2571
|
+
const productName = product.name;
|
|
1497
2572
|
if (!run?.runId) throw new Error('Companion run payload is missing runId.');
|
|
1498
|
-
if (
|
|
1499
|
-
throw new Error(`Unsupported
|
|
2573
|
+
if (!['dexter-companion-v4', 'dexter-companion-v5', 'dexter-companion-v6', 'dexter-companion-v7', 'dexter-companion-v8'].includes(run?.protocol?.version)) {
|
|
2574
|
+
throw new Error(`Unsupported bridge protocol ${run.protocol.version}. Update the local bridge and reconnect.`);
|
|
1500
2575
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
2576
|
+
const modelTurnRun =
|
|
2577
|
+
run?.kind === 'model_turn' && run?.protocol?.mode === 'model_turn';
|
|
2578
|
+
const outcomeRun =
|
|
2579
|
+
run?.kind === 'outcome' && run?.protocol?.mode === 'outcome';
|
|
2580
|
+
if (!modelTurnRun && !outcomeRun) {
|
|
2581
|
+
throw new Error('The local bridge only accepts server-owned model_turn or outcome runs.');
|
|
1503
2582
|
}
|
|
1504
2583
|
const writeLine = (message) => {
|
|
1505
2584
|
if (typeof log !== 'function') return;
|
|
@@ -1507,40 +2586,59 @@ export async function executeRun(run, {
|
|
|
1507
2586
|
else log(message);
|
|
1508
2587
|
};
|
|
1509
2588
|
const trace = createRunLogger({ runId: run.runId, logDir, mirror: log });
|
|
1510
|
-
const send = createEventPoster({
|
|
1511
|
-
|
|
1512
|
-
|
|
2589
|
+
const send = createEventPoster({
|
|
2590
|
+
apiBaseUrl,
|
|
2591
|
+
deviceToken,
|
|
2592
|
+
run,
|
|
2593
|
+
fetchImpl,
|
|
2594
|
+
trace,
|
|
2595
|
+
allowInsecureHttp,
|
|
2596
|
+
});
|
|
2597
|
+
const runtimeProfile = run?.runtimeProfile || suppliedRuntimeProfile || null;
|
|
2598
|
+
const normalizedAgent = normalizeAgentName(
|
|
2599
|
+
run?.companion?.agent || (runtimeProfile?.driver === 'claude-code-cli' ? 'claude-code' : agent),
|
|
2600
|
+
);
|
|
2601
|
+
const runModel = runtimeProfile?.model?.id || run?.companion?.model?.id || run?.model || selectedModel;
|
|
1513
2602
|
const usageAccumulator = createCompanionUsageAccumulator(normalizedAgent);
|
|
1514
2603
|
const adapterProvided = providerAdapter !== undefined;
|
|
1515
|
-
const shareAdapter =
|
|
2604
|
+
const shareAdapter =
|
|
2605
|
+
!adapterProvided &&
|
|
2606
|
+
(
|
|
2607
|
+
(modelTurnRun && deltaModelSessionsEnabled(env)) ||
|
|
2608
|
+
(outcomeRun && runtimeProfile?.capabilities?.sessionResume === true)
|
|
2609
|
+
);
|
|
2610
|
+
const sharedAdapterKey = `${product.id}:${runtimeProfile?.id || normalizedAgent}`;
|
|
1516
2611
|
let activeAdapter = adapterProvided ? providerAdapter : null;
|
|
1517
2612
|
if (!adapterProvided && shareAdapter) {
|
|
1518
|
-
activeAdapter = sharedProviderAdapters.get(
|
|
2613
|
+
activeAdapter = sharedProviderAdapters.get(sharedAdapterKey) || null;
|
|
1519
2614
|
if (!activeAdapter) {
|
|
1520
|
-
activeAdapter = createLocalAgentAdapter(normalizedAgent, {
|
|
2615
|
+
activeAdapter = createLocalAgentAdapter(runtimeProfile || normalizedAgent, {
|
|
1521
2616
|
...adapterOptions,
|
|
1522
2617
|
env: adapterOptions?.env || env,
|
|
2618
|
+
product,
|
|
1523
2619
|
trace,
|
|
1524
2620
|
});
|
|
1525
|
-
if (activeAdapter) sharedProviderAdapters.set(
|
|
2621
|
+
if (activeAdapter) sharedProviderAdapters.set(sharedAdapterKey, activeAdapter);
|
|
1526
2622
|
}
|
|
1527
2623
|
} else if (!adapterProvided) {
|
|
1528
|
-
activeAdapter = createLocalAgentAdapter(normalizedAgent, {
|
|
2624
|
+
activeAdapter = createLocalAgentAdapter(runtimeProfile || normalizedAgent, {
|
|
1529
2625
|
...adapterOptions,
|
|
1530
2626
|
env: adapterOptions?.env || env,
|
|
2627
|
+
product,
|
|
1531
2628
|
trace,
|
|
1532
2629
|
});
|
|
1533
2630
|
}
|
|
1534
2631
|
const ownsAdapter = !adapterProvided && !shareAdapter && Boolean(activeAdapter);
|
|
1535
|
-
if (
|
|
1536
|
-
activeAdapter?.
|
|
1537
|
-
|
|
1538
|
-
|
|
2632
|
+
if (activeAdapter?.profileId && runtimeProfile?.id && activeAdapter.profileId !== runtimeProfile.id) {
|
|
2633
|
+
if (ownsAdapter) activeAdapter.close?.();
|
|
2634
|
+
throw new Error(`Provider adapter ${activeAdapter.profileId} cannot execute runtime profile ${runtimeProfile.id}.`);
|
|
2635
|
+
}
|
|
2636
|
+
if (!runtimeProfile && activeAdapter?.id && activeAdapter.id !== normalizedAgent) {
|
|
1539
2637
|
if (ownsAdapter) activeAdapter.close?.();
|
|
1540
2638
|
throw new Error(`Provider adapter ${activeAdapter.id} cannot execute ${normalizedAgent} runs.`);
|
|
1541
2639
|
}
|
|
1542
|
-
writeLine(`Running
|
|
1543
|
-
writeLine(
|
|
2640
|
+
writeLine(`Running ${productName} bridge run ${run.runId} (${normalizedAgent}, ${runModel || 'default model'}).`);
|
|
2641
|
+
writeLine(`${productName} Bridge run log: ${trace.filePath || logLocationHint(logDir)}`);
|
|
1544
2642
|
trace.info('run_start', {
|
|
1545
2643
|
apiBaseUrl,
|
|
1546
2644
|
agent: normalizedAgent,
|
|
@@ -1554,12 +2652,13 @@ export async function executeRun(run, {
|
|
|
1554
2652
|
if (!activeAdapter && normalizedAgent !== 'dry-run') {
|
|
1555
2653
|
const definition = definitionForAgent(normalizedAgent);
|
|
1556
2654
|
const selectedModelDefinition = companionModelDefinition(runModel, normalizedAgent);
|
|
1557
|
-
runtime = await resolveAgentRuntime(definition, selectedModelDefinition, {
|
|
2655
|
+
runtime = await resolveAgentRuntime(definition, selectedModelDefinition, {
|
|
2656
|
+
env,
|
|
2657
|
+
});
|
|
1558
2658
|
if (!runtime.ok) {
|
|
1559
|
-
throw Object.assign(
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
);
|
|
2659
|
+
throw Object.assign(new Error(runtime.error || `${definition.label} is not available.`), {
|
|
2660
|
+
code: runtime.code,
|
|
2661
|
+
});
|
|
1563
2662
|
}
|
|
1564
2663
|
const authentication = await checkAgentAuthentication(normalizedAgent, runtime, {
|
|
1565
2664
|
env,
|
|
@@ -1572,7 +2671,7 @@ export async function executeRun(run, {
|
|
|
1572
2671
|
});
|
|
1573
2672
|
}
|
|
1574
2673
|
}
|
|
1575
|
-
|
|
2674
|
+
const executionOptions = {
|
|
1576
2675
|
model: runModel || selectedModel,
|
|
1577
2676
|
selectedModel: runModel || selectedModel,
|
|
1578
2677
|
providerAdapter: activeAdapter,
|
|
@@ -1580,8 +2679,23 @@ export async function executeRun(run, {
|
|
|
1580
2679
|
trace,
|
|
1581
2680
|
env,
|
|
1582
2681
|
controlPollMs,
|
|
2682
|
+
product,
|
|
2683
|
+
runtimeProfile,
|
|
2684
|
+
framerAgentRunCli,
|
|
2685
|
+
apiBaseUrl,
|
|
2686
|
+
deviceToken,
|
|
2687
|
+
fetchImpl,
|
|
2688
|
+
openAuthorizationUrl,
|
|
2689
|
+
allowInsecureHttp,
|
|
2690
|
+
};
|
|
2691
|
+
if (outcomeRun) {
|
|
2692
|
+
await executeOutcomeRun(run, send, normalizedAgent, executionOptions);
|
|
2693
|
+
} else {
|
|
2694
|
+
await executeModelTurnRun(run, send, normalizedAgent, executionOptions);
|
|
2695
|
+
}
|
|
2696
|
+
trace.info('run_done', {
|
|
2697
|
+
status: outcomeRun ? 'outcome_sent' : 'model_turn_sent',
|
|
1583
2698
|
});
|
|
1584
|
-
trace.info('run_done', { status: 'model_turn_sent' });
|
|
1585
2699
|
return { cancelled: false };
|
|
1586
2700
|
} catch (error) {
|
|
1587
2701
|
if (error?.code === 'RUN_CANCELLED') {
|
|
@@ -1591,14 +2705,16 @@ export async function executeRun(run, {
|
|
|
1591
2705
|
outcome: 'cancelled',
|
|
1592
2706
|
message: error.message,
|
|
1593
2707
|
code: 'RUN_CANCELLED',
|
|
2708
|
+
...(error?.companionUsage || usageAccumulator.snapshot()),
|
|
1594
2709
|
}).catch(() => undefined);
|
|
1595
2710
|
return { cancelled: true };
|
|
1596
2711
|
}
|
|
1597
2712
|
trace.error('run_failed', { error: errorMeta(error) });
|
|
1598
2713
|
await send('error', {
|
|
1599
2714
|
stage: 'agent',
|
|
1600
|
-
message: error?.message || 'Local
|
|
2715
|
+
message: error?.message || 'Local bridge failed.',
|
|
1601
2716
|
code: error?.code,
|
|
2717
|
+
retryable: error?.retryable === true,
|
|
1602
2718
|
...(error?.companionUsage || usageAccumulator.snapshot()),
|
|
1603
2719
|
}).catch(() => undefined);
|
|
1604
2720
|
throw error;
|
|
@@ -1615,7 +2731,11 @@ export function __resetAgentSessionsForTests() {
|
|
|
1615
2731
|
|
|
1616
2732
|
export function checkCommand(command, args = ['--version'], timeoutMs = 10000) {
|
|
1617
2733
|
return runProcess(command, args, '', { timeoutMs })
|
|
1618
|
-
.then((result) => ({
|
|
2734
|
+
.then((result) => ({
|
|
2735
|
+
ok: true,
|
|
2736
|
+
command,
|
|
2737
|
+
output: (result.stdout || result.stderr || '').trim(),
|
|
2738
|
+
}))
|
|
1619
2739
|
.catch((error) => ({ ok: false, command, error: error.message }));
|
|
1620
2740
|
}
|
|
1621
2741
|
|
|
@@ -1642,20 +2762,64 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
1642
2762
|
existsSync: options.existsSync,
|
|
1643
2763
|
inspect: options.inspectRuntime,
|
|
1644
2764
|
});
|
|
1645
|
-
const supportedModels = companionModelsForAgent(normalizedAgent)
|
|
1646
|
-
|
|
1647
|
-
|
|
2765
|
+
const supportedModels = companionModelsForAgent(normalizedAgent).filter((candidate) =>
|
|
2766
|
+
modelSupportsAgentVersion(candidate, runtime.version),
|
|
2767
|
+
);
|
|
1648
2768
|
if (!runtime.ok) {
|
|
1649
2769
|
return {
|
|
1650
2770
|
...runtime,
|
|
1651
2771
|
agent: definition.id,
|
|
1652
2772
|
label: definition.label,
|
|
1653
|
-
models: supportedModels,
|
|
2773
|
+
models: supportedModels.map((candidate) => candidate.id),
|
|
2774
|
+
modelDetails: supportedModels,
|
|
1654
2775
|
installed: false,
|
|
1655
2776
|
signedIn: false,
|
|
1656
2777
|
status: 'unavailable',
|
|
1657
2778
|
};
|
|
1658
2779
|
}
|
|
2780
|
+
if (normalizedAgent === 'codex') {
|
|
2781
|
+
const adapter =
|
|
2782
|
+
options.providerAdapter
|
|
2783
|
+
|| createLocalAgentAdapter('codex', {
|
|
2784
|
+
env: options.env,
|
|
2785
|
+
});
|
|
2786
|
+
try {
|
|
2787
|
+
const detection = await adapter.detect();
|
|
2788
|
+
if (!detection.ok || !detection.signedIn) {
|
|
2789
|
+
return {
|
|
2790
|
+
...runtime,
|
|
2791
|
+
...detection,
|
|
2792
|
+
ok: false,
|
|
2793
|
+
agent: definition.id,
|
|
2794
|
+
label: definition.label,
|
|
2795
|
+
models: supportedModels.map((candidate) => candidate.id),
|
|
2796
|
+
modelDetails: supportedModels,
|
|
2797
|
+
installed: detection.installed !== false,
|
|
2798
|
+
signedIn: false,
|
|
2799
|
+
status: detection.installed === false ? 'unavailable' : 'authentication_required',
|
|
2800
|
+
code: detection.installed === false ? 'DEXTER_AGENT_NOT_FOUND' : AGENT_AUTHENTICATION_REQUIRED_CODE,
|
|
2801
|
+
error:
|
|
2802
|
+
detection.error
|
|
2803
|
+
|| 'Codex is not signed in. Run `codex login` on this computer, then run the bridge command again.',
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
const reportedModels = await adapter.models();
|
|
2807
|
+
return {
|
|
2808
|
+
...runtime,
|
|
2809
|
+
...detection,
|
|
2810
|
+
ok: true,
|
|
2811
|
+
agent: definition.id,
|
|
2812
|
+
label: definition.label,
|
|
2813
|
+
models: reportedModels.map((candidate) => candidate.id),
|
|
2814
|
+
modelDetails: reportedModels,
|
|
2815
|
+
installed: true,
|
|
2816
|
+
signedIn: true,
|
|
2817
|
+
status: 'ready',
|
|
2818
|
+
};
|
|
2819
|
+
} finally {
|
|
2820
|
+
if (!options.providerAdapter) adapter.close?.();
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
1659
2823
|
const authentication = await checkAgentAuthentication(normalizedAgent, runtime, {
|
|
1660
2824
|
env: options.env,
|
|
1661
2825
|
platform: options.platform,
|
|
@@ -1667,7 +2831,8 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
1667
2831
|
ok: runtime.ok && authentication.ok,
|
|
1668
2832
|
agent: definition.id,
|
|
1669
2833
|
label: definition.label,
|
|
1670
|
-
models: supportedModels,
|
|
2834
|
+
models: supportedModels.map((candidate) => candidate.id),
|
|
2835
|
+
modelDetails: supportedModels,
|
|
1671
2836
|
installed: true,
|
|
1672
2837
|
};
|
|
1673
2838
|
}
|