@gakim-digital/dexter-bridge 0.5.21 → 0.11.0

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