@gakim-digital/dexter-bridge 0.5.20 → 0.11.0

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