@gakim-digital/dexter-bridge 0.5.21 → 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 +116 -35
- package/package.json +19 -5
- package/src/agent.js +1351 -331
- package/src/agentOutput.js +209 -0
- package/src/api.js +48 -1
- package/src/cli.js +267 -39
- package/src/config.js +30 -7
- 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 +239 -0
- package/src/providers/acp.js +241 -0
- package/src/providers/codexAppServer.js +1050 -156
- package/src/providers/codexStructuredOutput.js +243 -16
- package/src/providers/directByok.js +197 -0
- package/src/providers/index.js +33 -7
- package/src/providers/openCode.js +607 -0
- package/src/runtimeProfiles.js +284 -0
- package/src/providers/claudeAgentSdk.js +0 -507
package/src/agent.js
CHANGED
|
@@ -2,9 +2,15 @@ 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,
|
|
@@ -14,30 +20,69 @@ import {
|
|
|
14
20
|
normalizeCompanionModelName,
|
|
15
21
|
} from './config.js';
|
|
16
22
|
import {
|
|
23
|
+
assertOutcomeResponseContract,
|
|
24
|
+
buildOutcomePrompt,
|
|
17
25
|
buildModelTurnDeltaPrompt,
|
|
18
26
|
buildModelTurnFallbackPrompt,
|
|
19
27
|
buildModelTurnPrompt,
|
|
20
28
|
extractJsonObject,
|
|
21
29
|
modelTurnOutputSchema,
|
|
22
30
|
normalizeModelTurnCompletion,
|
|
31
|
+
normalizeOutcomeCompletion,
|
|
32
|
+
normalizeStructuredResponseContract,
|
|
33
|
+
outcomeOutputSchema,
|
|
23
34
|
runSummary,
|
|
35
|
+
STRUCTURED_RESPONSE_CONTRACTS,
|
|
24
36
|
} from './protocol.js';
|
|
37
|
+
import { clip, createRunLogger, errorMeta, logLocationHint, summarizePayload, summarizeToolResult } from './logger.js';
|
|
25
38
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
summarizeToolResult,
|
|
32
|
-
} from './logger.js';
|
|
39
|
+
claudeNativeSkillPrompt,
|
|
40
|
+
materializeNativeSkills,
|
|
41
|
+
nativeSkillLifecycle,
|
|
42
|
+
normalizeNativeSkills,
|
|
43
|
+
} from './nativeSkills.js';
|
|
33
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
|
+
}
|
|
34
77
|
|
|
35
78
|
const companionModelSessions = new Map();
|
|
36
79
|
const sharedProviderAdapters = new Map();
|
|
37
80
|
const MAX_COMPANION_MODEL_SESSIONS = 64;
|
|
38
81
|
|
|
39
82
|
function enabledFlag(value, fallback = false) {
|
|
40
|
-
const raw = String(value ?? '')
|
|
83
|
+
const raw = String(value ?? '')
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase();
|
|
41
86
|
if (!raw) return fallback;
|
|
42
87
|
return !['0', 'false', 'no', 'off', 'disabled'].includes(raw);
|
|
43
88
|
}
|
|
@@ -47,7 +92,9 @@ function deltaModelSessionsEnabled(env = process.env) {
|
|
|
47
92
|
}
|
|
48
93
|
|
|
49
94
|
function companionSessionKey(run) {
|
|
50
|
-
return run?.
|
|
95
|
+
return run?.outcome?.session?.goalSessionId
|
|
96
|
+
|| run?.outcome?.session?.sessionId
|
|
97
|
+
|| run?.modelTurn?.session?.goalSessionId
|
|
51
98
|
|| run?.modelTurn?.session?.sessionId
|
|
52
99
|
|| run?.turnId
|
|
53
100
|
|| run?.runId;
|
|
@@ -77,6 +124,22 @@ function resumeFailure(error) {
|
|
|
77
124
|
return /resume|session|thread|conversation|not found|unknown id|expired/i.test(message);
|
|
78
125
|
}
|
|
79
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
|
+
|
|
80
143
|
export const AGENT_DEFINITIONS = {
|
|
81
144
|
'claude-code': {
|
|
82
145
|
id: 'claude-code',
|
|
@@ -110,6 +173,7 @@ function nowIso() {
|
|
|
110
173
|
|
|
111
174
|
const TERMINAL_EVENT_TYPES = new Set(['done', 'error']);
|
|
112
175
|
const TERMINAL_EVENT_MAX_ATTEMPTS = 5;
|
|
176
|
+
const TOOL_EVENT_MAX_ATTEMPTS = 3;
|
|
113
177
|
|
|
114
178
|
function waitForRetry(delayMs) {
|
|
115
179
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
@@ -117,25 +181,30 @@ function waitForRetry(delayMs) {
|
|
|
117
181
|
|
|
118
182
|
function terminalEventRetryDelay(error, attempt) {
|
|
119
183
|
const status = Number(error?.status);
|
|
120
|
-
const retryable =
|
|
121
|
-
!Number.isFinite(status)
|
|
122
|
-
|| status === 408
|
|
123
|
-
|| status === 429
|
|
124
|
-
|| status >= 500;
|
|
184
|
+
const retryable = !Number.isFinite(status) || status === 408 || status === 429 || status >= 500;
|
|
125
185
|
if (!retryable) return null;
|
|
126
186
|
if (Number.isFinite(error?.retryAfterMs) && error.retryAfterMs > 0) {
|
|
127
187
|
return Math.min(65_000, Math.max(250, error.retryAfterMs));
|
|
128
188
|
}
|
|
129
|
-
return Math.min(8_000, 500 *
|
|
189
|
+
return Math.min(8_000, 500 * 2 ** (attempt - 1));
|
|
130
190
|
}
|
|
131
191
|
|
|
132
|
-
function createEventPoster({
|
|
192
|
+
function createEventPoster({
|
|
193
|
+
apiBaseUrl,
|
|
194
|
+
deviceToken,
|
|
195
|
+
run,
|
|
196
|
+
fetchImpl,
|
|
197
|
+
trace,
|
|
198
|
+
allowInsecureHttp = false,
|
|
199
|
+
}) {
|
|
133
200
|
return async function send(type, payload = {}) {
|
|
134
201
|
const eventId = `${type}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
135
202
|
const started = Date.now();
|
|
136
203
|
const maxAttempts = TERMINAL_EVENT_TYPES.has(type)
|
|
137
204
|
? TERMINAL_EVENT_MAX_ATTEMPTS
|
|
138
|
-
:
|
|
205
|
+
: type === 'tool_call'
|
|
206
|
+
? TOOL_EVENT_MAX_ATTEMPTS
|
|
207
|
+
: 1;
|
|
139
208
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
140
209
|
trace?.info('event_post_start', {
|
|
141
210
|
type,
|
|
@@ -148,6 +217,7 @@ function createEventPoster({ apiBaseUrl, deviceToken, run, fetchImpl, trace }) {
|
|
|
148
217
|
deviceToken,
|
|
149
218
|
runId: run.runId,
|
|
150
219
|
fetchImpl,
|
|
220
|
+
allowInsecureHttp,
|
|
151
221
|
event: {
|
|
152
222
|
type,
|
|
153
223
|
eventId,
|
|
@@ -166,10 +236,7 @@ function createEventPoster({ apiBaseUrl, deviceToken, run, fetchImpl, trace }) {
|
|
|
166
236
|
});
|
|
167
237
|
return response;
|
|
168
238
|
} catch (error) {
|
|
169
|
-
const retryDelayMs =
|
|
170
|
-
attempt < maxAttempts
|
|
171
|
-
? terminalEventRetryDelay(error, attempt)
|
|
172
|
-
: null;
|
|
239
|
+
const retryDelayMs = attempt < maxAttempts ? terminalEventRetryDelay(error, attempt) : null;
|
|
173
240
|
trace?.error('event_post_failed', {
|
|
174
241
|
type,
|
|
175
242
|
eventId,
|
|
@@ -193,7 +260,10 @@ function commandFromEnv(name, fallback, env = process.env) {
|
|
|
193
260
|
function parseExtraArgs(value, fallback = '') {
|
|
194
261
|
const source = value === undefined || value === null || value === '' ? fallback : value;
|
|
195
262
|
if (!source) return [];
|
|
196
|
-
return String(source)
|
|
263
|
+
return String(source)
|
|
264
|
+
.split(/\s+/)
|
|
265
|
+
.map((item) => item.trim())
|
|
266
|
+
.filter(Boolean);
|
|
197
267
|
}
|
|
198
268
|
|
|
199
269
|
function argsAlreadySelectModel(args) {
|
|
@@ -205,7 +275,11 @@ function argsIncludeFlag(args, flag) {
|
|
|
205
275
|
}
|
|
206
276
|
|
|
207
277
|
function structuredUsageEnabled(env = process.env) {
|
|
208
|
-
return
|
|
278
|
+
return (
|
|
279
|
+
String(env.DEXTER_BRIDGE_STRUCTURED_USAGE || 'true')
|
|
280
|
+
.trim()
|
|
281
|
+
.toLowerCase() !== 'false'
|
|
282
|
+
);
|
|
209
283
|
}
|
|
210
284
|
|
|
211
285
|
function argsWithRequiredAgentFlags(args, definition) {
|
|
@@ -229,11 +303,37 @@ function claudeModelEngineSystemPrompt(product) {
|
|
|
229
303
|
'Never execute, simulate, or emit native Claude Code tool calls for those tool names.',
|
|
230
304
|
'The only allowed tool is StructuredOutput, supplied by the JSON schema.',
|
|
231
305
|
`Encode requested ${name} actions only inside StructuredOutput.toolCalls.`,
|
|
232
|
-
'
|
|
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.',
|
|
233
308
|
`Be concise: reason only as much as needed to choose the next remote ${name} action.`,
|
|
234
309
|
].join(' ');
|
|
235
310
|
}
|
|
236
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
|
+
}
|
|
336
|
+
|
|
237
337
|
export function mapClaudeCliModelId(value) {
|
|
238
338
|
const raw = String(value || '').trim();
|
|
239
339
|
return CLAUDE_CLI_MODEL_IDS[raw.toLowerCase()] || raw;
|
|
@@ -242,20 +342,17 @@ export function mapClaudeCliModelId(value) {
|
|
|
242
342
|
function argsWithSelectedModel(args, modelDefinition, definition) {
|
|
243
343
|
if (!modelDefinition?.invocationName || modelDefinition.invocationName === 'dry-run') return args;
|
|
244
344
|
if (argsAlreadySelectModel(args)) return args;
|
|
245
|
-
const invocationName =
|
|
246
|
-
|
|
247
|
-
|
|
345
|
+
const invocationName =
|
|
346
|
+
definition.id === 'claude-code'
|
|
347
|
+
? mapClaudeCliModelId(modelDefinition.invocationName)
|
|
348
|
+
: modelDefinition.invocationName;
|
|
248
349
|
return [...args, '--model', invocationName];
|
|
249
350
|
}
|
|
250
351
|
|
|
251
352
|
function argsWithStructuredOutput(args, definition, env = process.env) {
|
|
252
353
|
if (!structuredUsageEnabled(env)) return args;
|
|
253
354
|
if (definition.id === 'claude-code') {
|
|
254
|
-
const streamed = [
|
|
255
|
-
...argsWithoutFlagValue(args, '--output-format'),
|
|
256
|
-
'--output-format',
|
|
257
|
-
'stream-json',
|
|
258
|
-
];
|
|
355
|
+
const streamed = [...argsWithoutFlagValue(args, '--output-format'), '--output-format', 'stream-json'];
|
|
259
356
|
if (!argsIncludeFlag(streamed, '--include-partial-messages')) {
|
|
260
357
|
streamed.push('--include-partial-messages');
|
|
261
358
|
}
|
|
@@ -268,21 +365,72 @@ function argsWithStructuredOutput(args, definition, env = process.env) {
|
|
|
268
365
|
return args;
|
|
269
366
|
}
|
|
270
367
|
|
|
271
|
-
function argsWithClaudeIsolation(args, definition) {
|
|
368
|
+
function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
272
369
|
if (definition.id !== 'claude-code') return args;
|
|
273
370
|
const isolated = argsWithoutVariadicFlag(args, '--tools');
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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;
|
|
279
427
|
}
|
|
280
428
|
|
|
281
429
|
function normalizeClaudeEffort(value, fallback = 'medium') {
|
|
282
|
-
const effort = String(value || '')
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
430
|
+
const effort = String(value || '')
|
|
431
|
+
.trim()
|
|
432
|
+
.toLowerCase();
|
|
433
|
+
return ['low', 'medium', 'high', 'xhigh', 'max'].includes(effort) ? effort : fallback;
|
|
286
434
|
}
|
|
287
435
|
|
|
288
436
|
function claudeEffortForStep(step, env = process.env) {
|
|
@@ -307,7 +455,9 @@ function argsWithClaudeModelEngine(args, definition, options = {}, env = process
|
|
|
307
455
|
return [
|
|
308
456
|
...isolated,
|
|
309
457
|
'--system-prompt',
|
|
310
|
-
|
|
458
|
+
options.harnessMode
|
|
459
|
+
? claudeOutcomeSystemPrompt(options.product)
|
|
460
|
+
: claudeModelEngineSystemPrompt(options.product),
|
|
311
461
|
'--effort',
|
|
312
462
|
claudeEffortForStep(options.step, env),
|
|
313
463
|
];
|
|
@@ -341,33 +491,61 @@ function argsWithoutVariadicFlag(args, flag) {
|
|
|
341
491
|
return filtered;
|
|
342
492
|
}
|
|
343
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
|
+
|
|
344
528
|
function compactClaudeOutputSchema(outputSchema) {
|
|
345
529
|
const toolCalls = outputSchema?.properties?.toolCalls;
|
|
346
530
|
const itemSchema = toolCalls?.items;
|
|
347
|
-
const variants = Array.isArray(itemSchema?.anyOf)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
)),
|
|
358
|
-
));
|
|
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
|
+
);
|
|
359
541
|
return {
|
|
360
542
|
type: 'object',
|
|
361
543
|
properties: {
|
|
362
544
|
text: { type: 'string' },
|
|
363
545
|
toolCalls: {
|
|
364
546
|
type: 'array',
|
|
365
|
-
...(Number.isFinite(Number(toolCalls?.minItems))
|
|
366
|
-
|
|
367
|
-
: {}),
|
|
368
|
-
...(Number.isFinite(Number(toolCalls?.maxItems))
|
|
369
|
-
? { maxItems: Number(toolCalls.maxItems) }
|
|
370
|
-
: {}),
|
|
547
|
+
...(Number.isFinite(Number(toolCalls?.minItems)) ? { minItems: Number(toolCalls.minItems) } : {}),
|
|
548
|
+
...(Number.isFinite(Number(toolCalls?.maxItems)) ? { maxItems: Number(toolCalls.maxItems) } : {}),
|
|
371
549
|
items: {
|
|
372
550
|
type: 'object',
|
|
373
551
|
properties: {
|
|
@@ -392,16 +570,24 @@ function compactClaudeOutputSchema(outputSchema) {
|
|
|
392
570
|
};
|
|
393
571
|
}
|
|
394
572
|
|
|
395
|
-
function argsWithOutputSchema(
|
|
573
|
+
function argsWithOutputSchema(
|
|
574
|
+
args,
|
|
575
|
+
definition,
|
|
576
|
+
outputSchema,
|
|
577
|
+
responseContract = 'model-turn',
|
|
578
|
+
) {
|
|
396
579
|
if (definition.id !== 'claude-code' || !outputSchema) return args;
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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)];
|
|
405
591
|
}
|
|
406
592
|
|
|
407
593
|
function argsWithPromptInput(args, definition) {
|
|
@@ -456,7 +642,12 @@ function argsWithResumedSession(args, definition, sessionId) {
|
|
|
456
642
|
}
|
|
457
643
|
|
|
458
644
|
function agentSessionResumeEnabled(definition, env = process.env) {
|
|
459
|
-
if (
|
|
645
|
+
if (
|
|
646
|
+
String(env.DEXTER_BRIDGE_SESSION_RESUME || 'true')
|
|
647
|
+
.trim()
|
|
648
|
+
.toLowerCase() === 'false'
|
|
649
|
+
)
|
|
650
|
+
return false;
|
|
460
651
|
const baseArgs = parseExtraArgs(env[definition.argsEnv], definition.defaultArgs);
|
|
461
652
|
if (definition.id === 'claude-code' && argsIncludeFlag(baseArgs, '--no-session-persistence')) return false;
|
|
462
653
|
if (definition.id === 'codex' && argsIncludeFlag(baseArgs, '--ephemeral')) return false;
|
|
@@ -468,13 +659,17 @@ export function buildAgentArgs(definition, modelDefinition, env = process.env, o
|
|
|
468
659
|
const requiredArgs = argsWithRequiredAgentFlags(baseArgs, definition);
|
|
469
660
|
const modelArgs = argsWithSelectedModel(requiredArgs, modelDefinition, definition);
|
|
470
661
|
const structuredArgs = argsWithStructuredOutput(modelArgs, definition, env);
|
|
471
|
-
const isolatedArgs = argsWithClaudeIsolation(
|
|
662
|
+
const isolatedArgs = argsWithClaudeIsolation(
|
|
663
|
+
structuredArgs,
|
|
664
|
+
definition,
|
|
665
|
+
options,
|
|
666
|
+
);
|
|
472
667
|
const modelEngineArgs = argsWithClaudeModelEngine(isolatedArgs, definition, options, env);
|
|
473
668
|
const schemaArgs = argsWithOutputSchema(
|
|
474
669
|
modelEngineArgs,
|
|
475
670
|
definition,
|
|
476
671
|
options.outputSchema,
|
|
477
|
-
options.
|
|
672
|
+
options.responseContract,
|
|
478
673
|
);
|
|
479
674
|
const resumedArgs = argsWithResumedSession(schemaArgs, definition, options.resumeSessionId);
|
|
480
675
|
return argsWithPromptInput(resumedArgs, definition);
|
|
@@ -497,7 +692,12 @@ let cachedLoginShellEnvMeta = { attempted: false, loaded: false, keys: 0 };
|
|
|
497
692
|
|
|
498
693
|
function loginShellEnv() {
|
|
499
694
|
if (process.env.DEXTER_BRIDGE_DISABLE_LOGIN_SHELL_ENV === '1' || process.platform === 'win32') {
|
|
500
|
-
cachedLoginShellEnvMeta = {
|
|
695
|
+
cachedLoginShellEnvMeta = {
|
|
696
|
+
attempted: false,
|
|
697
|
+
loaded: false,
|
|
698
|
+
keys: 0,
|
|
699
|
+
disabled: true,
|
|
700
|
+
};
|
|
501
701
|
return {};
|
|
502
702
|
}
|
|
503
703
|
if (cachedLoginShellEnv) return cachedLoginShellEnv;
|
|
@@ -561,20 +761,18 @@ export function mergePathEntries(paths, platform = process.platform) {
|
|
|
561
761
|
|
|
562
762
|
export function processEnvWithCliPath(baseEnv = process.env, platform = process.platform) {
|
|
563
763
|
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
564
|
-
const homeDirectory =
|
|
565
|
-
|
|
566
|
-
baseEnv.USERPROFILE
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
]
|
|
577
|
-
: ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'];
|
|
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'];
|
|
578
776
|
const shellEnv = platform === 'win32' ? {} : loginShellEnv();
|
|
579
777
|
const existingPath = String(baseEnv.PATH || '');
|
|
580
778
|
const mergedPath = mergePathEntries([shellEnv.PATH, existingPath, ...fallbackPath], platform);
|
|
@@ -585,7 +783,11 @@ export function processEnvWithCliPath(baseEnv = process.env, platform = process.
|
|
|
585
783
|
};
|
|
586
784
|
}
|
|
587
785
|
|
|
588
|
-
export function resolveAgentCwd(
|
|
786
|
+
export function resolveAgentCwd(
|
|
787
|
+
env = process.env,
|
|
788
|
+
currentWorkingDirectory = process.cwd(),
|
|
789
|
+
platform = process.platform,
|
|
790
|
+
) {
|
|
589
791
|
const explicit = String(env.DEXTER_BRIDGE_AGENT_CWD || env.DEXTER_BRIDGE_WORKDIR || '').trim();
|
|
590
792
|
if (explicit) return explicit;
|
|
591
793
|
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
@@ -596,7 +798,9 @@ export function resolveAgentCwd(env = process.env, currentWorkingDirectory = pro
|
|
|
596
798
|
|
|
597
799
|
function processEnvSummary(env, platform = process.platform) {
|
|
598
800
|
const delimiter = platform === 'win32' ? ';' : ':';
|
|
599
|
-
const pathEntries = String(env.PATH || '')
|
|
801
|
+
const pathEntries = String(env.PATH || '')
|
|
802
|
+
.split(delimiter)
|
|
803
|
+
.filter(Boolean);
|
|
600
804
|
return {
|
|
601
805
|
hasAzureOpenAiApiKey: Boolean(env.AZURE_OPENAI_API_KEY),
|
|
602
806
|
hasOpenAiApiKey: Boolean(env.OPENAI_API_KEY),
|
|
@@ -608,18 +812,28 @@ function processEnvSummary(env, platform = process.platform) {
|
|
|
608
812
|
};
|
|
609
813
|
}
|
|
610
814
|
|
|
611
|
-
export function executableCandidates(
|
|
815
|
+
export function executableCandidates(
|
|
816
|
+
command,
|
|
817
|
+
env = process.env,
|
|
818
|
+
platform = process.platform,
|
|
819
|
+
existsSync = fs.existsSync,
|
|
820
|
+
) {
|
|
612
821
|
const raw = String(command || '').trim();
|
|
613
822
|
if (!raw) return [];
|
|
614
823
|
if (/[\\/]/.test(raw)) return [raw];
|
|
615
824
|
const delimiter = platform === 'win32' ? ';' : ':';
|
|
616
825
|
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
617
|
-
const pathEntries = String(env.PATH || '')
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
+
: [''];
|
|
623
837
|
const candidates = [];
|
|
624
838
|
for (const directory of pathEntries) {
|
|
625
839
|
for (const extension of extensions) {
|
|
@@ -634,7 +848,12 @@ export function executableCandidates(command, env = process.env, platform = proc
|
|
|
634
848
|
return [...new Set(candidates.length ? candidates : [raw])];
|
|
635
849
|
}
|
|
636
850
|
|
|
637
|
-
export function resolveExecutableCommand(
|
|
851
|
+
export function resolveExecutableCommand(
|
|
852
|
+
command,
|
|
853
|
+
env = process.env,
|
|
854
|
+
platform = process.platform,
|
|
855
|
+
existsSync = fs.existsSync,
|
|
856
|
+
) {
|
|
638
857
|
return executableCandidates(command, env, platform, existsSync)[0] || String(command || '').trim();
|
|
639
858
|
}
|
|
640
859
|
|
|
@@ -644,7 +863,9 @@ export function parseAgentVersion(output) {
|
|
|
644
863
|
}
|
|
645
864
|
|
|
646
865
|
function versionParts(value) {
|
|
647
|
-
const match = String(value || '')
|
|
866
|
+
const match = String(value || '')
|
|
867
|
+
.trim()
|
|
868
|
+
.match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
648
869
|
return match ? match.slice(1).map(Number) : null;
|
|
649
870
|
}
|
|
650
871
|
|
|
@@ -673,9 +894,7 @@ export function selectAgentRuntime(inspections, definition, modelDefinition) {
|
|
|
673
894
|
return {
|
|
674
895
|
ok: false,
|
|
675
896
|
command: first?.command || definition.fallbackCommand,
|
|
676
|
-
code: first?.code === 'ENOENT'
|
|
677
|
-
? 'DEXTER_AGENT_NOT_FOUND'
|
|
678
|
-
: first?.code,
|
|
897
|
+
code: first?.code === 'ENOENT' ? 'DEXTER_AGENT_NOT_FOUND' : first?.code,
|
|
679
898
|
error: first?.error || `${definition.label} is not installed or could not be started.`,
|
|
680
899
|
candidates: inspections,
|
|
681
900
|
};
|
|
@@ -683,7 +902,9 @@ export function selectAgentRuntime(inspections, definition, modelDefinition) {
|
|
|
683
902
|
|
|
684
903
|
const ranked = successful
|
|
685
904
|
.map((inspection, index) => ({ ...inspection, discoveryIndex: index }))
|
|
686
|
-
.sort(
|
|
905
|
+
.sort(
|
|
906
|
+
(left, right) => compareAgentVersions(right.version, left.version) || left.discoveryIndex - right.discoveryIndex,
|
|
907
|
+
);
|
|
687
908
|
const compatible = ranked.find((inspection) => modelSupportsAgentVersion(modelDefinition, inspection.version));
|
|
688
909
|
if (compatible) {
|
|
689
910
|
return {
|
|
@@ -708,7 +929,12 @@ export function selectAgentRuntime(inspections, definition, modelDefinition) {
|
|
|
708
929
|
|
|
709
930
|
export function processInvocation(command, args, env = process.env, platform = process.platform) {
|
|
710
931
|
const resolvedCommand = resolveExecutableCommand(command, env, platform);
|
|
711
|
-
return {
|
|
932
|
+
return {
|
|
933
|
+
command: resolvedCommand,
|
|
934
|
+
args,
|
|
935
|
+
resolvedCommand,
|
|
936
|
+
windowsHide: platform === 'win32',
|
|
937
|
+
};
|
|
712
938
|
}
|
|
713
939
|
|
|
714
940
|
function safeCommandArgs(args) {
|
|
@@ -734,9 +960,7 @@ function jsonDiagnosticEvents(value) {
|
|
|
734
960
|
.flatMap((line) => {
|
|
735
961
|
try {
|
|
736
962
|
const parsed = JSON.parse(line);
|
|
737
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
738
|
-
? [parsed]
|
|
739
|
-
: [];
|
|
963
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? [parsed] : [];
|
|
740
964
|
} catch {
|
|
741
965
|
return [];
|
|
742
966
|
}
|
|
@@ -745,15 +969,24 @@ function jsonDiagnosticEvents(value) {
|
|
|
745
969
|
|
|
746
970
|
function claudeFailureDetails(stdout) {
|
|
747
971
|
const events = jsonDiagnosticEvents(stdout);
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
+
);
|
|
752
980
|
const rateLimitInfo = rateLimit?.rate_limit_info;
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
+
}
|
|
757
990
|
return {
|
|
758
991
|
code: 'CLAUDE_CREDITS_REQUIRED',
|
|
759
992
|
message:
|
|
@@ -772,15 +1005,14 @@ function claudeFailureDetails(stdout) {
|
|
|
772
1005
|
};
|
|
773
1006
|
}
|
|
774
1007
|
|
|
775
|
-
const resultError = [...events]
|
|
776
|
-
|
|
777
|
-
&& (event.is_error === true || event.subtype === 'error'));
|
|
1008
|
+
const resultError = [...events]
|
|
1009
|
+
.reverse()
|
|
1010
|
+
.find((event) => event.type === 'result' && (event.is_error === true || event.subtype === 'error'));
|
|
778
1011
|
const resultMessages = Array.isArray(resultError?.errors)
|
|
779
1012
|
? resultError.errors.filter((message) => typeof message === 'string' && message.trim())
|
|
780
1013
|
: [];
|
|
781
1014
|
const resultMessage =
|
|
782
|
-
resultMessages.join(' ')
|
|
783
|
-
|| (typeof resultError?.result === 'string' ? resultError.result.trim() : '');
|
|
1015
|
+
resultMessages.join(' ') || (typeof resultError?.result === 'string' ? resultError.result.trim() : '');
|
|
784
1016
|
if (resultMessage) {
|
|
785
1017
|
return {
|
|
786
1018
|
code: 'CLAUDE_RUN_FAILED',
|
|
@@ -798,10 +1030,7 @@ export function agentFailureDetails(command, code, stdout = '', stderr = '') {
|
|
|
798
1030
|
}
|
|
799
1031
|
|
|
800
1032
|
const combined = [stderr, stdout].filter(Boolean).join('\n').trim();
|
|
801
|
-
const lines = combined
|
|
802
|
-
.split(/\r?\n/)
|
|
803
|
-
.map(normalizeDiagnosticLine)
|
|
804
|
-
.filter(Boolean);
|
|
1033
|
+
const lines = combined.split(/\r?\n/).map(normalizeDiagnosticLine).filter(Boolean);
|
|
805
1034
|
const diagnosticPatterns = [
|
|
806
1035
|
/missing environment variable/i,
|
|
807
1036
|
/not inside a trusted directory/i,
|
|
@@ -816,9 +1045,7 @@ export function agentFailureDetails(command, code, stdout = '', stderr = '') {
|
|
|
816
1045
|
/^failed\b/i,
|
|
817
1046
|
];
|
|
818
1047
|
const diagnostics = lines.filter((line) => diagnosticPatterns.some((pattern) => pattern.test(line)));
|
|
819
|
-
const detail = diagnostics.length
|
|
820
|
-
? diagnostics.slice(-3).join(' ')
|
|
821
|
-
: clip(combined, 1000);
|
|
1048
|
+
const detail = diagnostics.length ? diagnostics.slice(-3).join(' ') : clip(combined, 1000);
|
|
822
1049
|
return {
|
|
823
1050
|
code: 'AGENT_PROCESS_FAILED',
|
|
824
1051
|
message: `${command} exited with code ${code}.${detail ? ` ${clip(detail, 1000)}` : ''}`.trim(),
|
|
@@ -829,20 +1056,26 @@ export function agentFailureMessage(command, code, stdout = '', stderr = '') {
|
|
|
829
1056
|
return agentFailureDetails(command, code, stdout, stderr).message;
|
|
830
1057
|
}
|
|
831
1058
|
|
|
832
|
-
function runProcess(
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
+
) {
|
|
841
1076
|
return new Promise((resolve, reject) => {
|
|
842
1077
|
if (signal?.aborted) {
|
|
843
|
-
const error = signal.reason instanceof Error
|
|
844
|
-
? signal.reason
|
|
845
|
-
: new Error('The model process was cancelled.');
|
|
1078
|
+
const error = signal.reason instanceof Error ? signal.reason : new Error('The model process was cancelled.');
|
|
846
1079
|
error.code = error.code || 'RUN_CANCELLED';
|
|
847
1080
|
reject(error);
|
|
848
1081
|
return;
|
|
@@ -853,7 +1086,10 @@ function runProcess(command, args, stdin, {
|
|
|
853
1086
|
// hardDeadlineMs bounds total wall-clock time regardless of activity.
|
|
854
1087
|
const hardDeadlineMs = Math.max(maxDurationMs || timeoutMs * 5, 10);
|
|
855
1088
|
const childEnv = processEnvWithCliPath(providedChildEnv || process.env, platform);
|
|
856
|
-
const cwd =
|
|
1089
|
+
const cwd =
|
|
1090
|
+
typeof providedCwd === 'string' && providedCwd.trim()
|
|
1091
|
+
? providedCwd
|
|
1092
|
+
: resolveAgentCwd(childEnv, process.cwd(), platform);
|
|
857
1093
|
const invocation = processInvocation(command, args, childEnv, platform);
|
|
858
1094
|
trace?.info('agent_process_spawn', {
|
|
859
1095
|
command,
|
|
@@ -872,11 +1108,18 @@ function runProcess(command, args, stdin, {
|
|
|
872
1108
|
windowsHide: invocation.windowsHide,
|
|
873
1109
|
detached: platform !== 'win32',
|
|
874
1110
|
});
|
|
1111
|
+
onProgress?.({
|
|
1112
|
+
kind: 'turn_started',
|
|
1113
|
+
provider: 'cli',
|
|
1114
|
+
occurredAt: new Date().toISOString(),
|
|
1115
|
+
});
|
|
875
1116
|
let stdout = '';
|
|
876
1117
|
let stderr = '';
|
|
877
1118
|
let settled = false;
|
|
1119
|
+
let terminationError = null;
|
|
878
1120
|
let inactivityTimer = null;
|
|
879
1121
|
let forceKillTimer = null;
|
|
1122
|
+
let terminationFallbackTimer = null;
|
|
880
1123
|
const killChildTree = (killSignal) => {
|
|
881
1124
|
if (platform !== 'win32' && child.pid) {
|
|
882
1125
|
try {
|
|
@@ -896,38 +1139,50 @@ function runProcess(command, args, stdin, {
|
|
|
896
1139
|
clearTimeout(inactivityTimer);
|
|
897
1140
|
clearTimeout(deadlineTimer);
|
|
898
1141
|
};
|
|
899
|
-
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) => {
|
|
900
1154
|
killChildTree('SIGTERM');
|
|
901
|
-
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);
|
|
902
1163
|
forceKillTimer.unref?.();
|
|
903
1164
|
};
|
|
904
1165
|
const abort = () => {
|
|
905
|
-
if (settled) return;
|
|
906
|
-
settled = true;
|
|
1166
|
+
if (settled || terminationError) return;
|
|
907
1167
|
clearTimers();
|
|
908
|
-
|
|
909
|
-
const error = signal?.reason instanceof Error
|
|
910
|
-
? signal.reason
|
|
911
|
-
: new Error('The model process was cancelled.');
|
|
1168
|
+
const error = signal?.reason instanceof Error ? signal.reason : new Error('The model process was cancelled.');
|
|
912
1169
|
error.code = error.code || 'RUN_CANCELLED';
|
|
913
|
-
|
|
914
|
-
error
|
|
1170
|
+
terminationError = error;
|
|
1171
|
+
terminateChildTree(error);
|
|
915
1172
|
trace?.info('agent_process_cancelled', {
|
|
916
1173
|
command,
|
|
917
1174
|
durationMs: Date.now() - started,
|
|
918
1175
|
});
|
|
919
|
-
reject(error);
|
|
920
1176
|
};
|
|
921
|
-
const timeOut = (message) => {
|
|
922
|
-
if (settled) return;
|
|
923
|
-
settled = true;
|
|
1177
|
+
const timeOut = (code, message) => {
|
|
1178
|
+
if (settled || terminationError) return;
|
|
924
1179
|
clearTimers();
|
|
925
|
-
terminateChildTree();
|
|
926
1180
|
const error = new Error(message);
|
|
927
|
-
error.code =
|
|
928
|
-
error.
|
|
929
|
-
error.stderr = stderr;
|
|
1181
|
+
error.code = code;
|
|
1182
|
+
error.retryable = true;
|
|
930
1183
|
error.timedOut = true;
|
|
1184
|
+
terminationError = error;
|
|
1185
|
+
terminateChildTree(error);
|
|
931
1186
|
trace?.error('agent_process_timeout', {
|
|
932
1187
|
command,
|
|
933
1188
|
durationMs: Date.now() - started,
|
|
@@ -938,47 +1193,74 @@ function runProcess(command, args, stdin, {
|
|
|
938
1193
|
stdoutExcerpt: clip(stdout, 1000),
|
|
939
1194
|
stderrExcerpt: clip(stderr, 1000),
|
|
940
1195
|
});
|
|
941
|
-
reject(error);
|
|
942
1196
|
};
|
|
943
1197
|
const armInactivityTimer = () => {
|
|
944
1198
|
clearTimeout(inactivityTimer);
|
|
945
1199
|
inactivityTimer = setTimeout(() => {
|
|
946
|
-
timeOut(
|
|
1200
|
+
timeOut(
|
|
1201
|
+
'APP_AGENT_TURN_INACTIVITY_TIMEOUT',
|
|
1202
|
+
`${command} timed out after ${timeoutMs}ms without meaningful output.`,
|
|
1203
|
+
);
|
|
947
1204
|
}, timeoutMs);
|
|
948
1205
|
};
|
|
949
1206
|
const deadlineTimer = setTimeout(() => {
|
|
950
|
-
timeOut(
|
|
1207
|
+
timeOut(
|
|
1208
|
+
'APP_AGENT_TURN_HARD_TIMEOUT',
|
|
1209
|
+
`${command} timed out after ${hardDeadlineMs}ms at the safety limit.`,
|
|
1210
|
+
);
|
|
951
1211
|
}, hardDeadlineMs);
|
|
952
1212
|
signal?.addEventListener('abort', abort, { once: true });
|
|
953
1213
|
armInactivityTimer();
|
|
954
1214
|
|
|
955
1215
|
child.stdout.on('data', (chunk) => {
|
|
956
|
-
|
|
957
|
-
|
|
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
|
+
}
|
|
958
1227
|
});
|
|
959
1228
|
child.stderr.on('data', (chunk) => {
|
|
960
1229
|
stderr += chunk.toString('utf8');
|
|
961
|
-
|
|
1230
|
+
if (!terminationError) {
|
|
1231
|
+
armInactivityTimer();
|
|
1232
|
+
onProgress?.({
|
|
1233
|
+
kind: 'output_delta',
|
|
1234
|
+
provider: 'cli',
|
|
1235
|
+
occurredAt: new Date().toISOString(),
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
962
1238
|
});
|
|
963
1239
|
child.on('error', (error) => {
|
|
964
1240
|
if (settled) return;
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1241
|
+
if (terminationError) {
|
|
1242
|
+
settleWithError(terminationError);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
969
1245
|
trace?.error('agent_process_error', {
|
|
970
1246
|
command,
|
|
971
1247
|
durationMs: Date.now() - started,
|
|
972
1248
|
error: errorMeta(error),
|
|
973
1249
|
});
|
|
974
|
-
|
|
1250
|
+
settleWithError(error);
|
|
975
1251
|
});
|
|
976
1252
|
child.on('close', (code) => {
|
|
977
1253
|
if (settled) return;
|
|
1254
|
+
if (terminationError) {
|
|
1255
|
+
settleWithError(terminationError);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
978
1258
|
settled = true;
|
|
979
1259
|
clearTimers();
|
|
980
1260
|
clearTimeout(forceKillTimer);
|
|
1261
|
+
clearTimeout(terminationFallbackTimer);
|
|
981
1262
|
signal?.removeEventListener('abort', abort);
|
|
1263
|
+
stdoutProgress?.flush();
|
|
982
1264
|
const meta = {
|
|
983
1265
|
command,
|
|
984
1266
|
code,
|
|
@@ -1015,11 +1297,26 @@ function definitionForAgent(agent) {
|
|
|
1015
1297
|
return AGENT_DEFINITIONS[normalizeAgentName(agent)] || AGENT_DEFINITIONS[DEFAULT_BRIDGE_AGENT];
|
|
1016
1298
|
}
|
|
1017
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
|
+
|
|
1018
1311
|
function adapterSessionId(run) {
|
|
1019
1312
|
return run?.turnId || run?.runId;
|
|
1020
1313
|
}
|
|
1021
1314
|
|
|
1022
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
|
+
}
|
|
1023
1320
|
const explicit = run?.companion?.model?.invocationName;
|
|
1024
1321
|
if (typeof explicit === 'string' && explicit.trim()) return explicit.trim();
|
|
1025
1322
|
if (selectedModel?.invocationName) return selectedModel.invocationName;
|
|
@@ -1031,9 +1328,7 @@ function adapterInvocationModel(run, selectedModel, agent) {
|
|
|
1031
1328
|
|
|
1032
1329
|
function controlRequestsCancellation(response) {
|
|
1033
1330
|
return Boolean(
|
|
1034
|
-
response?.control?.cancelRequested
|
|
1035
|
-
|| response?.status === 'cancelled'
|
|
1036
|
-
|| response?.status === 'expired',
|
|
1331
|
+
response?.control?.cancelRequested || response?.status === 'cancelled' || response?.status === 'expired',
|
|
1037
1332
|
);
|
|
1038
1333
|
}
|
|
1039
1334
|
|
|
@@ -1050,10 +1345,14 @@ function waitForControl(delayMs, signal) {
|
|
|
1050
1345
|
return;
|
|
1051
1346
|
}
|
|
1052
1347
|
const timer = setTimeout(resolve, delayMs);
|
|
1053
|
-
signal?.addEventListener(
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1348
|
+
signal?.addEventListener(
|
|
1349
|
+
'abort',
|
|
1350
|
+
() => {
|
|
1351
|
+
clearTimeout(timer);
|
|
1352
|
+
reject(signal.reason || new Error('Control monitor stopped.'));
|
|
1353
|
+
},
|
|
1354
|
+
{ once: true },
|
|
1355
|
+
);
|
|
1057
1356
|
});
|
|
1058
1357
|
}
|
|
1059
1358
|
|
|
@@ -1065,12 +1364,18 @@ class CompanionRunCancelledError extends Error {
|
|
|
1065
1364
|
}
|
|
1066
1365
|
}
|
|
1067
1366
|
|
|
1068
|
-
async function callProviderAdapter(
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
+
) {
|
|
1074
1379
|
const productName = normalizeBridgeProduct(product).name;
|
|
1075
1380
|
if (!adapter || typeof adapter.runModelTurn !== 'function') {
|
|
1076
1381
|
throw new Error('The selected provider adapter cannot execute model turns.');
|
|
@@ -1079,12 +1384,81 @@ async function callProviderAdapter(adapter, input, {
|
|
|
1079
1384
|
let finished = false;
|
|
1080
1385
|
let cancelRequested = false;
|
|
1081
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
|
+
});
|
|
1082
1395
|
|
|
1083
|
-
|
|
1396
|
+
function requestCancel() {
|
|
1084
1397
|
cancelRequested = true;
|
|
1085
|
-
if (cancelSent) return;
|
|
1398
|
+
if (cancelSent) return cancelActionPromise;
|
|
1086
1399
|
cancelSent = true;
|
|
1087
|
-
|
|
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
|
+
});
|
|
1088
1462
|
}
|
|
1089
1463
|
|
|
1090
1464
|
async function monitorControl() {
|
|
@@ -1092,11 +1466,12 @@ async function callProviderAdapter(adapter, input, {
|
|
|
1092
1466
|
try {
|
|
1093
1467
|
await waitForControl(controlPollMs, monitorAbort.signal);
|
|
1094
1468
|
if (finished || monitorAbort.signal.aborted) return;
|
|
1095
|
-
|
|
1469
|
+
await postActivity({
|
|
1096
1470
|
stage: 'model_turn',
|
|
1471
|
+
source: 'bridge',
|
|
1472
|
+
meaningful: false,
|
|
1097
1473
|
message: `${adapter.label || adapter.id || 'Provider'} is still generating the next ${productName} action.`,
|
|
1098
1474
|
});
|
|
1099
|
-
if (controlRequestsCancellation(response)) await requestCancel();
|
|
1100
1475
|
} catch (error) {
|
|
1101
1476
|
if (monitorAbort.signal.aborted) return;
|
|
1102
1477
|
trace?.warn('adapter_control_poll_failed', { error: errorMeta(error) });
|
|
@@ -1105,15 +1480,28 @@ async function callProviderAdapter(adapter, input, {
|
|
|
1105
1480
|
}
|
|
1106
1481
|
|
|
1107
1482
|
const controlMonitor = monitorControl();
|
|
1483
|
+
const providerTurn = Promise.resolve().then(() =>
|
|
1484
|
+
adapter.runModelTurn({
|
|
1485
|
+
...input,
|
|
1486
|
+
onProgress: reportProviderProgress,
|
|
1487
|
+
}));
|
|
1108
1488
|
try {
|
|
1109
|
-
const result = await
|
|
1110
|
-
|
|
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
|
+
}
|
|
1111
1495
|
return result;
|
|
1112
1496
|
} catch (error) {
|
|
1113
|
-
if (cancelRequested)
|
|
1497
|
+
if (cancelRequested) {
|
|
1498
|
+
await waitForCancellationAcknowledgement(providerTurn).catch(() => undefined);
|
|
1499
|
+
throw finishCancellation();
|
|
1500
|
+
}
|
|
1114
1501
|
throw error;
|
|
1115
1502
|
} finally {
|
|
1116
1503
|
finished = true;
|
|
1504
|
+
clearTimeout(cancelTimer);
|
|
1117
1505
|
monitorAbort.abort();
|
|
1118
1506
|
await controlMonitor.catch(() => undefined);
|
|
1119
1507
|
}
|
|
@@ -1123,35 +1511,32 @@ export async function resolveAgentRuntime(definition, modelDefinition, options =
|
|
|
1123
1511
|
const platform = options.platform || process.platform;
|
|
1124
1512
|
const childEnv = processEnvWithCliPath(options.env || process.env, platform);
|
|
1125
1513
|
const configuredCommand = commandFromEnv(definition.commandEnv, definition.fallbackCommand, childEnv);
|
|
1126
|
-
const candidates = executableCandidates(
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
});
|
|
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
|
+
});
|
|
1155
1540
|
const inspections = await Promise.all(candidates.map((candidate) => inspect(candidate)));
|
|
1156
1541
|
return selectAgentRuntime(inspections, definition, modelDefinition);
|
|
1157
1542
|
}
|
|
@@ -1210,7 +1595,10 @@ export async function checkAgentAuthentication(agent, runtime, options = {}) {
|
|
|
1210
1595
|
const childEnv = processEnvWithCliPath(options.env || process.env, platform);
|
|
1211
1596
|
const inspect = options.inspect || inspectClaudeAuthentication;
|
|
1212
1597
|
try {
|
|
1213
|
-
const authentication = await inspect(runtime.command, {
|
|
1598
|
+
const authentication = await inspect(runtime.command, {
|
|
1599
|
+
childEnv,
|
|
1600
|
+
platform,
|
|
1601
|
+
});
|
|
1214
1602
|
if (authentication?.loggedIn === true) {
|
|
1215
1603
|
return {
|
|
1216
1604
|
ok: true,
|
|
@@ -1240,8 +1628,13 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
1240
1628
|
const args = buildAgentArgs(definition, modelDefinition, process.env, {
|
|
1241
1629
|
resumeSessionId: options.resumeSessionId,
|
|
1242
1630
|
outputSchema: options.outputSchema,
|
|
1631
|
+
responseContract: options.responseContract,
|
|
1243
1632
|
step: options.step,
|
|
1244
1633
|
product: options.product,
|
|
1634
|
+
nativeSkillsEnabled: options.nativeSkillsEnabled,
|
|
1635
|
+
harnessMode: options.harnessMode,
|
|
1636
|
+
mcpConfig: options.mcpConfig,
|
|
1637
|
+
harnessToolNames: options.harnessToolNames,
|
|
1245
1638
|
});
|
|
1246
1639
|
const timeoutMs = options.timeoutMs || Number(process.env.DEXTER_BRIDGE_AGENT_TIMEOUT_MS || 120000);
|
|
1247
1640
|
const maxDurationMs = boundedDurationMs(
|
|
@@ -1267,13 +1660,17 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
1267
1660
|
maxDurationMs,
|
|
1268
1661
|
trace: options.trace,
|
|
1269
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,
|
|
1270
1669
|
});
|
|
1271
1670
|
}
|
|
1272
1671
|
|
|
1273
1672
|
function agentErrorWithOutputUsage(agent, error) {
|
|
1274
|
-
const failure = error instanceof Error
|
|
1275
|
-
? error
|
|
1276
|
-
: 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.'));
|
|
1277
1674
|
const parsed = parseAgentOutput(agent, failure.stdout || '');
|
|
1278
1675
|
if (!parsed.usageAvailable) return failure;
|
|
1279
1676
|
const accumulator = createCompanionUsageAccumulator(agent);
|
|
@@ -1286,7 +1683,7 @@ function agentErrorWithOutputUsage(agent, error) {
|
|
|
1286
1683
|
async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
1287
1684
|
const product = normalizeBridgeProduct(run?.product || options.product);
|
|
1288
1685
|
const productName = product.name;
|
|
1289
|
-
if (normalizeAgentName(agent) === 'dry-run') {
|
|
1686
|
+
if (normalizeAgentName(agent) === 'dry-run' && !options.providerAdapter) {
|
|
1290
1687
|
await send('done', {
|
|
1291
1688
|
operationType: 'chat',
|
|
1292
1689
|
outcome: 'answer',
|
|
@@ -1301,27 +1698,47 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1301
1698
|
});
|
|
1302
1699
|
return;
|
|
1303
1700
|
}
|
|
1304
|
-
const definition =
|
|
1305
|
-
const requestedModel =
|
|
1306
|
-
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
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;
|
|
1310
1709
|
const adapter = options.providerAdapter || null;
|
|
1311
1710
|
const sessionKey = companionSessionKey(run);
|
|
1312
1711
|
const rememberedSession = rememberedCompanionSession(sessionKey);
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
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';
|
|
1316
1727
|
let callContextMode = deltaRequested && rememberedSession ? 'delta' : 'full';
|
|
1317
|
-
let prompt =
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|
+
}
|
|
1322
1737
|
const callStartedAt = Date.now();
|
|
1323
1738
|
let providerSessionId;
|
|
1324
1739
|
let fallbackAfterResumeFailure = false;
|
|
1740
|
+
let providerAttempts = 0;
|
|
1741
|
+
let retryReason = null;
|
|
1325
1742
|
const statusResponse = await send('status', {
|
|
1326
1743
|
stage: 'model_turn',
|
|
1327
1744
|
message: `${definition.label} is generating the next ${productName} action.`,
|
|
@@ -1332,32 +1749,35 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1332
1749
|
}
|
|
1333
1750
|
const usageAccumulator = createCompanionUsageAccumulator(definition.id);
|
|
1334
1751
|
const failedModelCall = (error) => {
|
|
1335
|
-
const failure = error instanceof Error
|
|
1336
|
-
? error
|
|
1337
|
-
: 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.'));
|
|
1338
1753
|
usageAccumulator.add(failure.companionUsage || {});
|
|
1339
1754
|
const snapshot = usageAccumulator.snapshot();
|
|
1340
1755
|
failure.companionUsage = {
|
|
1341
1756
|
...snapshot,
|
|
1342
|
-
modelCalls: [
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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
|
+
],
|
|
1357
1776
|
};
|
|
1358
1777
|
return failure;
|
|
1359
1778
|
};
|
|
1360
1779
|
let resultText;
|
|
1780
|
+
let nativeSkillStatus = null;
|
|
1361
1781
|
if (adapter) {
|
|
1362
1782
|
const sessionId = sessionKey || adapterSessionId(run);
|
|
1363
1783
|
const invocationModel = adapterInvocationModel(run, selectedModel, definition.id);
|
|
@@ -1378,18 +1798,14 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1378
1798
|
prompt,
|
|
1379
1799
|
model: invocationModel,
|
|
1380
1800
|
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
120000,
|
|
1384
|
-
1000,
|
|
1385
|
-
),
|
|
1801
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
1802
|
+
timeoutMs: boundedDurationMs(options.timeoutMs ?? options.env?.DEXTER_BRIDGE_AGENT_TIMEOUT_MS, 120000, 1000),
|
|
1386
1803
|
maxDurationMs: boundedDurationMs(
|
|
1387
|
-
run?.modelTurn?.maxDurationMs
|
|
1388
|
-
?? options.maxDurationMs
|
|
1389
|
-
?? options.env?.DEXTER_BRIDGE_AGENT_MAX_DURATION_MS,
|
|
1804
|
+
run?.modelTurn?.maxDurationMs ?? options.maxDurationMs ?? options.env?.DEXTER_BRIDGE_AGENT_MAX_DURATION_MS,
|
|
1390
1805
|
600000,
|
|
1391
1806
|
1000,
|
|
1392
1807
|
),
|
|
1808
|
+
nativeSkills,
|
|
1393
1809
|
};
|
|
1394
1810
|
const adapterOptions = {
|
|
1395
1811
|
send,
|
|
@@ -1405,36 +1821,72 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1405
1821
|
let result;
|
|
1406
1822
|
try {
|
|
1407
1823
|
result = await callProviderAdapter(adapter, adapterInput, adapterOptions);
|
|
1824
|
+
providerAttempts += providerAttemptCount(result);
|
|
1825
|
+
retryReason = result?.retryReason || retryReason;
|
|
1408
1826
|
} catch (error) {
|
|
1409
|
-
|
|
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
|
+
}
|
|
1410
1833
|
usageAccumulator.add(error?.companionUsage || {});
|
|
1411
1834
|
fallbackAfterResumeFailure = true;
|
|
1835
|
+
retryReason =
|
|
1836
|
+
error?.code === 'APP_AGENT_TURN_INACTIVITY_TIMEOUT'
|
|
1837
|
+
? 'timeout'
|
|
1838
|
+
: retryFreshSession
|
|
1839
|
+
? 'transport_error'
|
|
1840
|
+
: 'resume_failure';
|
|
1412
1841
|
callContextMode = 'full';
|
|
1413
1842
|
prompt = buildModelTurnFallbackPrompt(run.modelTurn, product);
|
|
1843
|
+
if (materializedNativeSkills) {
|
|
1844
|
+
prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
|
|
1845
|
+
}
|
|
1414
1846
|
await adapter.resetSession?.(sessionId);
|
|
1415
1847
|
options.trace?.warn('agent_adapter_resume_fallback', {
|
|
1416
1848
|
sessionId,
|
|
1417
1849
|
error: errorMeta(error),
|
|
1418
1850
|
fallbackPromptChars: prompt.length,
|
|
1419
1851
|
});
|
|
1420
|
-
|
|
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
|
+
})
|
|
1421
1870
|
.catch((error) => {
|
|
1871
|
+
providerAttempts += providerAttemptCount(error);
|
|
1422
1872
|
throw failedModelCall(error);
|
|
1423
1873
|
});
|
|
1424
1874
|
}
|
|
1425
1875
|
usageAccumulator.add(result);
|
|
1426
1876
|
resultText = result?.text;
|
|
1427
1877
|
providerSessionId = result?.threadId || result?.sessionId;
|
|
1878
|
+
nativeSkillStatus = result?.nativeSkillLifecycle || null;
|
|
1428
1879
|
} else {
|
|
1429
|
-
const runtime =
|
|
1430
|
-
|
|
1431
|
-
|
|
1880
|
+
const runtime =
|
|
1881
|
+
options.runtime
|
|
1882
|
+
|| (await resolveAgentRuntime(definition, selectedModel, {
|
|
1883
|
+
env: options.env,
|
|
1884
|
+
}));
|
|
1432
1885
|
if (!runtime.ok) {
|
|
1433
1886
|
throw new Error(runtime.error || `${definition.label} is not available.`);
|
|
1434
1887
|
}
|
|
1435
1888
|
const resumeSessionId =
|
|
1436
|
-
callContextMode === 'delta'
|
|
1437
|
-
&& agentSessionResumeEnabled(definition, options.env)
|
|
1889
|
+
callContextMode === 'delta' && agentSessionResumeEnabled(definition, options.env)
|
|
1438
1890
|
? rememberedSession?.providerSessionId
|
|
1439
1891
|
: undefined;
|
|
1440
1892
|
const cliAbort = new AbortController();
|
|
@@ -1447,23 +1899,60 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1447
1899
|
);
|
|
1448
1900
|
let cliFinished = false;
|
|
1449
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
|
+
};
|
|
1450
1944
|
const controlMonitor = (async () => {
|
|
1451
1945
|
while (!cliFinished && !monitorAbort.signal.aborted) {
|
|
1452
1946
|
try {
|
|
1453
1947
|
await waitForControl(controlPollMs, monitorAbort.signal);
|
|
1454
1948
|
if (cliFinished || monitorAbort.signal.aborted) return;
|
|
1455
|
-
|
|
1949
|
+
await postCliActivity({
|
|
1456
1950
|
stage: 'model_turn',
|
|
1951
|
+
source: 'bridge',
|
|
1952
|
+
meaningful: false,
|
|
1457
1953
|
message: `${definition.label} is still generating the next ${productName} action.`,
|
|
1458
1954
|
});
|
|
1459
|
-
if (
|
|
1460
|
-
cliCancelled = true;
|
|
1461
|
-
cliAbort.abort(Object.assign(
|
|
1462
|
-
new Error(`The ${productName} bridge model turn was cancelled.`),
|
|
1463
|
-
{ code: 'RUN_CANCELLED' },
|
|
1464
|
-
));
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
1955
|
+
if (cliCancelled) return;
|
|
1467
1956
|
} catch (error) {
|
|
1468
1957
|
if (monitorAbort.signal.aborted) return;
|
|
1469
1958
|
options.trace?.warn('cli_control_poll_failed', {
|
|
@@ -1480,10 +1969,14 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1480
1969
|
runtime,
|
|
1481
1970
|
resumeSessionId,
|
|
1482
1971
|
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
1972
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
1483
1973
|
step: run?.modelTurn?.step,
|
|
1484
1974
|
product,
|
|
1485
1975
|
maxDurationMs: run?.modelTurn?.maxDurationMs ?? options.maxDurationMs,
|
|
1486
1976
|
signal: cliAbort.signal,
|
|
1977
|
+
cwd: materializedNativeSkills?.workspace,
|
|
1978
|
+
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
1979
|
+
onProgress: reportCliProgress,
|
|
1487
1980
|
});
|
|
1488
1981
|
} catch (error) {
|
|
1489
1982
|
if (cliCancelled || error?.code === 'RUN_CANCELLED') {
|
|
@@ -1495,6 +1988,9 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1495
1988
|
fallbackAfterResumeFailure = true;
|
|
1496
1989
|
callContextMode = 'full';
|
|
1497
1990
|
prompt = buildModelTurnFallbackPrompt(run.modelTurn, product);
|
|
1991
|
+
if (materializedNativeSkills) {
|
|
1992
|
+
prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
|
|
1993
|
+
}
|
|
1498
1994
|
options.trace?.warn('agent_cli_resume_fallback', {
|
|
1499
1995
|
resumeSessionId,
|
|
1500
1996
|
error: errorMeta(failure),
|
|
@@ -1506,10 +2002,14 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1506
2002
|
runtime,
|
|
1507
2003
|
resumeSessionId: undefined,
|
|
1508
2004
|
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
2005
|
+
responseContract: STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
|
|
1509
2006
|
step: run?.modelTurn?.step,
|
|
1510
2007
|
product,
|
|
1511
2008
|
maxDurationMs: run?.modelTurn?.maxDurationMs ?? options.maxDurationMs,
|
|
1512
2009
|
signal: cliAbort.signal,
|
|
2010
|
+
cwd: materializedNativeSkills?.workspace,
|
|
2011
|
+
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2012
|
+
onProgress: reportCliProgress,
|
|
1513
2013
|
}).catch((error) => {
|
|
1514
2014
|
if (cliCancelled || error?.code === 'RUN_CANCELLED') {
|
|
1515
2015
|
throw new CompanionRunCancelledError();
|
|
@@ -1521,10 +2021,19 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1521
2021
|
monitorAbort.abort();
|
|
1522
2022
|
await controlMonitor.catch(() => undefined);
|
|
1523
2023
|
}
|
|
2024
|
+
await activityChain;
|
|
2025
|
+
if (cliCancelled) throw new CompanionRunCancelledError();
|
|
1524
2026
|
const parsed = parseAgentOutput(definition.id, result.stdout);
|
|
1525
2027
|
usageAccumulator.add(parsed);
|
|
1526
2028
|
resultText = parsed.resultText;
|
|
1527
2029
|
providerSessionId = parsed.sessionId;
|
|
2030
|
+
nativeSkillStatus = materializedNativeSkills
|
|
2031
|
+
? nativeSkillLifecycle(
|
|
2032
|
+
materializedNativeSkills,
|
|
2033
|
+
'slash-command',
|
|
2034
|
+
callContextMode === 'delta',
|
|
2035
|
+
)
|
|
2036
|
+
: null;
|
|
1528
2037
|
}
|
|
1529
2038
|
if (providerSessionId || adapter) {
|
|
1530
2039
|
rememberCompanionSession(sessionKey, {
|
|
@@ -1545,6 +2054,8 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1545
2054
|
resumed: callContextMode === 'delta',
|
|
1546
2055
|
resumeSessionAvailable: Boolean(rememberedSession),
|
|
1547
2056
|
fallbackAfterResumeFailure,
|
|
2057
|
+
providerAttemptCount: Math.max(1, providerAttempts),
|
|
2058
|
+
retryReason,
|
|
1548
2059
|
...usageSnapshot.tokenUsage,
|
|
1549
2060
|
estimatedCostUSD: usageSnapshot.estimatedCostUSD,
|
|
1550
2061
|
};
|
|
@@ -1554,10 +2065,7 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1554
2065
|
};
|
|
1555
2066
|
let completion;
|
|
1556
2067
|
try {
|
|
1557
|
-
completion = normalizeModelTurnCompletion(
|
|
1558
|
-
extractJsonObject(resultText),
|
|
1559
|
-
selectedModelId,
|
|
1560
|
-
);
|
|
2068
|
+
completion = normalizeModelTurnCompletion(extractJsonObject(resultText), selectedModelId);
|
|
1561
2069
|
} catch (error) {
|
|
1562
2070
|
error.companionUsage = {
|
|
1563
2071
|
...usage,
|
|
@@ -1570,33 +2078,507 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1570
2078
|
outcome: 'answer',
|
|
1571
2079
|
completion,
|
|
1572
2080
|
model: selectedModelId,
|
|
2081
|
+
...(nativeSkillStatus
|
|
2082
|
+
? { nativeSkillLifecycle: nativeSkillStatus }
|
|
2083
|
+
: {}),
|
|
1573
2084
|
...usage,
|
|
1574
2085
|
});
|
|
1575
2086
|
}
|
|
1576
2087
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
selectedModel
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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
|
+
) {
|
|
1592
2570
|
const product = normalizeBridgeProduct(run?.product || configuredProduct);
|
|
1593
2571
|
const productName = product.name;
|
|
1594
2572
|
if (!run?.runId) throw new Error('Companion run payload is missing runId.');
|
|
1595
|
-
if (
|
|
2573
|
+
if (!['dexter-companion-v4', 'dexter-companion-v5', 'dexter-companion-v6', 'dexter-companion-v7', 'dexter-companion-v8'].includes(run?.protocol?.version)) {
|
|
1596
2574
|
throw new Error(`Unsupported bridge protocol ${run.protocol.version}. Update the local bridge and reconnect.`);
|
|
1597
2575
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
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.');
|
|
1600
2582
|
}
|
|
1601
2583
|
const writeLine = (message) => {
|
|
1602
2584
|
if (typeof log !== 'function') return;
|
|
@@ -1604,18 +2586,33 @@ export async function executeRun(run, {
|
|
|
1604
2586
|
else log(message);
|
|
1605
2587
|
};
|
|
1606
2588
|
const trace = createRunLogger({ runId: run.runId, logDir, mirror: log });
|
|
1607
|
-
const send = createEventPoster({
|
|
1608
|
-
|
|
1609
|
-
|
|
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;
|
|
1610
2602
|
const usageAccumulator = createCompanionUsageAccumulator(normalizedAgent);
|
|
1611
2603
|
const adapterProvided = providerAdapter !== undefined;
|
|
1612
|
-
const shareAdapter =
|
|
1613
|
-
|
|
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}`;
|
|
1614
2611
|
let activeAdapter = adapterProvided ? providerAdapter : null;
|
|
1615
2612
|
if (!adapterProvided && shareAdapter) {
|
|
1616
2613
|
activeAdapter = sharedProviderAdapters.get(sharedAdapterKey) || null;
|
|
1617
2614
|
if (!activeAdapter) {
|
|
1618
|
-
activeAdapter = createLocalAgentAdapter(normalizedAgent, {
|
|
2615
|
+
activeAdapter = createLocalAgentAdapter(runtimeProfile || normalizedAgent, {
|
|
1619
2616
|
...adapterOptions,
|
|
1620
2617
|
env: adapterOptions?.env || env,
|
|
1621
2618
|
product,
|
|
@@ -1624,7 +2621,7 @@ export async function executeRun(run, {
|
|
|
1624
2621
|
if (activeAdapter) sharedProviderAdapters.set(sharedAdapterKey, activeAdapter);
|
|
1625
2622
|
}
|
|
1626
2623
|
} else if (!adapterProvided) {
|
|
1627
|
-
activeAdapter = createLocalAgentAdapter(normalizedAgent, {
|
|
2624
|
+
activeAdapter = createLocalAgentAdapter(runtimeProfile || normalizedAgent, {
|
|
1628
2625
|
...adapterOptions,
|
|
1629
2626
|
env: adapterOptions?.env || env,
|
|
1630
2627
|
product,
|
|
@@ -1632,10 +2629,11 @@ export async function executeRun(run, {
|
|
|
1632
2629
|
});
|
|
1633
2630
|
}
|
|
1634
2631
|
const ownsAdapter = !adapterProvided && !shareAdapter && Boolean(activeAdapter);
|
|
1635
|
-
if (
|
|
1636
|
-
activeAdapter?.
|
|
1637
|
-
|
|
1638
|
-
|
|
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) {
|
|
1639
2637
|
if (ownsAdapter) activeAdapter.close?.();
|
|
1640
2638
|
throw new Error(`Provider adapter ${activeAdapter.id} cannot execute ${normalizedAgent} runs.`);
|
|
1641
2639
|
}
|
|
@@ -1654,12 +2652,13 @@ export async function executeRun(run, {
|
|
|
1654
2652
|
if (!activeAdapter && normalizedAgent !== 'dry-run') {
|
|
1655
2653
|
const definition = definitionForAgent(normalizedAgent);
|
|
1656
2654
|
const selectedModelDefinition = companionModelDefinition(runModel, normalizedAgent);
|
|
1657
|
-
runtime = await resolveAgentRuntime(definition, selectedModelDefinition, {
|
|
2655
|
+
runtime = await resolveAgentRuntime(definition, selectedModelDefinition, {
|
|
2656
|
+
env,
|
|
2657
|
+
});
|
|
1658
2658
|
if (!runtime.ok) {
|
|
1659
|
-
throw Object.assign(
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
);
|
|
2659
|
+
throw Object.assign(new Error(runtime.error || `${definition.label} is not available.`), {
|
|
2660
|
+
code: runtime.code,
|
|
2661
|
+
});
|
|
1663
2662
|
}
|
|
1664
2663
|
const authentication = await checkAgentAuthentication(normalizedAgent, runtime, {
|
|
1665
2664
|
env,
|
|
@@ -1672,7 +2671,7 @@ export async function executeRun(run, {
|
|
|
1672
2671
|
});
|
|
1673
2672
|
}
|
|
1674
2673
|
}
|
|
1675
|
-
|
|
2674
|
+
const executionOptions = {
|
|
1676
2675
|
model: runModel || selectedModel,
|
|
1677
2676
|
selectedModel: runModel || selectedModel,
|
|
1678
2677
|
providerAdapter: activeAdapter,
|
|
@@ -1681,8 +2680,22 @@ export async function executeRun(run, {
|
|
|
1681
2680
|
env,
|
|
1682
2681
|
controlPollMs,
|
|
1683
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',
|
|
1684
2698
|
});
|
|
1685
|
-
trace.info('run_done', { status: 'model_turn_sent' });
|
|
1686
2699
|
return { cancelled: false };
|
|
1687
2700
|
} catch (error) {
|
|
1688
2701
|
if (error?.code === 'RUN_CANCELLED') {
|
|
@@ -1692,6 +2705,7 @@ export async function executeRun(run, {
|
|
|
1692
2705
|
outcome: 'cancelled',
|
|
1693
2706
|
message: error.message,
|
|
1694
2707
|
code: 'RUN_CANCELLED',
|
|
2708
|
+
...(error?.companionUsage || usageAccumulator.snapshot()),
|
|
1695
2709
|
}).catch(() => undefined);
|
|
1696
2710
|
return { cancelled: true };
|
|
1697
2711
|
}
|
|
@@ -1700,6 +2714,7 @@ export async function executeRun(run, {
|
|
|
1700
2714
|
stage: 'agent',
|
|
1701
2715
|
message: error?.message || 'Local bridge failed.',
|
|
1702
2716
|
code: error?.code,
|
|
2717
|
+
retryable: error?.retryable === true,
|
|
1703
2718
|
...(error?.companionUsage || usageAccumulator.snapshot()),
|
|
1704
2719
|
}).catch(() => undefined);
|
|
1705
2720
|
throw error;
|
|
@@ -1716,7 +2731,11 @@ export function __resetAgentSessionsForTests() {
|
|
|
1716
2731
|
|
|
1717
2732
|
export function checkCommand(command, args = ['--version'], timeoutMs = 10000) {
|
|
1718
2733
|
return runProcess(command, args, '', { timeoutMs })
|
|
1719
|
-
.then((result) => ({
|
|
2734
|
+
.then((result) => ({
|
|
2735
|
+
ok: true,
|
|
2736
|
+
command,
|
|
2737
|
+
output: (result.stdout || result.stderr || '').trim(),
|
|
2738
|
+
}))
|
|
1720
2739
|
.catch((error) => ({ ok: false, command, error: error.message }));
|
|
1721
2740
|
}
|
|
1722
2741
|
|
|
@@ -1743,8 +2762,9 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
1743
2762
|
existsSync: options.existsSync,
|
|
1744
2763
|
inspect: options.inspectRuntime,
|
|
1745
2764
|
});
|
|
1746
|
-
const supportedModels = companionModelsForAgent(normalizedAgent)
|
|
1747
|
-
|
|
2765
|
+
const supportedModels = companionModelsForAgent(normalizedAgent).filter((candidate) =>
|
|
2766
|
+
modelSupportsAgentVersion(candidate, runtime.version),
|
|
2767
|
+
);
|
|
1748
2768
|
if (!runtime.ok) {
|
|
1749
2769
|
return {
|
|
1750
2770
|
...runtime,
|
|
@@ -1759,8 +2779,8 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
1759
2779
|
}
|
|
1760
2780
|
if (normalizedAgent === 'codex') {
|
|
1761
2781
|
const adapter =
|
|
1762
|
-
options.providerAdapter
|
|
1763
|
-
createLocalAgentAdapter('codex', {
|
|
2782
|
+
options.providerAdapter
|
|
2783
|
+
|| createLocalAgentAdapter('codex', {
|
|
1764
2784
|
env: options.env,
|
|
1765
2785
|
});
|
|
1766
2786
|
try {
|
|
@@ -1772,15 +2792,15 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
1772
2792
|
ok: false,
|
|
1773
2793
|
agent: definition.id,
|
|
1774
2794
|
label: definition.label,
|
|
1775
|
-
models:
|
|
1776
|
-
modelDetails:
|
|
2795
|
+
models: supportedModels.map((candidate) => candidate.id),
|
|
2796
|
+
modelDetails: supportedModels,
|
|
1777
2797
|
installed: detection.installed !== false,
|
|
1778
2798
|
signedIn: false,
|
|
1779
2799
|
status: detection.installed === false ? 'unavailable' : 'authentication_required',
|
|
1780
2800
|
code: detection.installed === false ? 'DEXTER_AGENT_NOT_FOUND' : AGENT_AUTHENTICATION_REQUIRED_CODE,
|
|
1781
2801
|
error:
|
|
1782
|
-
detection.error
|
|
1783
|
-
'Codex is not signed in. Run `codex login` on this computer, then run the bridge command again.',
|
|
2802
|
+
detection.error
|
|
2803
|
+
|| 'Codex is not signed in. Run `codex login` on this computer, then run the bridge command again.',
|
|
1784
2804
|
};
|
|
1785
2805
|
}
|
|
1786
2806
|
const reportedModels = await adapter.models();
|