@gakim-digital/dexter-bridge 0.5.21 → 0.11.1

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