@gakim-digital/dexter-bridge 0.5.21 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,67 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { jsonrepair } from 'jsonrepair';
3
+ import {
4
+ assertOutcomeResponseContract,
5
+ normalizeStructuredResponseContract,
6
+ STRUCTURED_RESPONSE_CONTRACTS,
7
+ } from '../protocol.js';
8
+
9
+ export { STRUCTURED_RESPONSE_CONTRACTS } from '../protocol.js';
10
+
1
11
  function isRecord(value) {
2
12
  return value && typeof value === 'object' && !Array.isArray(value);
3
13
  }
4
14
 
15
+ const CODEX_TRANSPORT_ERROR_CODES = new Set([
16
+ 'CODEX_STRUCTURED_OUTPUT_MALFORMED',
17
+ 'CODEX_STRUCTURED_OUTPUT_INVALID',
18
+ 'CODEX_TOOL_ARGUMENTS_TRANSPORT_INVALID',
19
+ 'CODEX_TOOL_ARGUMENTS_MALFORMED',
20
+ 'CODEX_TOOL_ARGUMENTS_NOT_OBJECT',
21
+ 'APP_HARNESS_RESULT_INVALID',
22
+ ]);
23
+
24
+ function sha256(value) {
25
+ return createHash('sha256')
26
+ .update(String(value || ''))
27
+ .digest('hex');
28
+ }
29
+
30
+ function diagnosticPreview(value, limit = 1_000) {
31
+ return String(value || '')
32
+ .slice(0, limit)
33
+ .replace(
34
+ /((?:api[-_]?key|authorization|bearer|secret|password|token|credential)(?:\\?"?\s*[:=]\s*\\?"?))[^,}\n"]+/gi,
35
+ '$1[redacted]',
36
+ );
37
+ }
38
+
39
+ function transportError(code, message, details = {}) {
40
+ return Object.assign(new Error(message), {
41
+ code,
42
+ retryable: true,
43
+ codexTransportRecoverable: true,
44
+ ...details,
45
+ });
46
+ }
47
+
48
+ function parseJsonObject(source) {
49
+ try {
50
+ return JSON.parse(source);
51
+ } catch (cause) {
52
+ const trimmed = String(source || '').trim();
53
+ const completeContainer =
54
+ (trimmed.startsWith('{') && trimmed.endsWith('}'))
55
+ || (trimmed.startsWith('[') && trimmed.endsWith(']'));
56
+ if (!completeContainer) throw cause;
57
+ try {
58
+ return JSON.parse(jsonrepair(trimmed));
59
+ } catch {
60
+ throw cause;
61
+ }
62
+ }
63
+ }
64
+
5
65
  function finiteInteger(value) {
6
66
  const parsed = Number(value);
7
67
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
@@ -36,7 +96,7 @@ function toolNames(outputSchema) {
36
96
  * tool catalog remains in the prompt and the server validates decoded arguments
37
97
  * before execution.
38
98
  */
39
- export function codexTransportOutputSchema(outputSchema) {
99
+ export function codexTransportOutputSchema(outputSchema, { toolName, requireSingleToolCall = false } = {}) {
40
100
  if (!isRecord(outputSchema)) {
41
101
  throw new Error('Codex model turn requires an output schema.');
42
102
  }
@@ -47,7 +107,8 @@ export function codexTransportOutputSchema(outputSchema) {
47
107
  throw new Error('Codex model turn output schema is missing its response envelope.');
48
108
  }
49
109
 
50
- const names = toolNames(outputSchema);
110
+ const availableNames = toolNames(outputSchema);
111
+ const names = typeof toolName === 'string' && availableNames.includes(toolName) ? [toolName] : availableNames;
51
112
  const minItems = finiteInteger(sourceToolCalls.minItems);
52
113
  const maxItems = finiteInteger(sourceToolCalls.maxItems);
53
114
  const finishReasonEnum = Array.isArray(sourceFinishReason.enum)
@@ -60,8 +121,12 @@ export function codexTransportOutputSchema(outputSchema) {
60
121
  text: { type: 'string' },
61
122
  toolCalls: {
62
123
  type: 'array',
63
- ...(minItems === undefined ? {} : { minItems }),
64
- ...(maxItems === undefined ? {} : { maxItems }),
124
+ ...(requireSingleToolCall
125
+ ? { minItems: 1, maxItems: 1 }
126
+ : {
127
+ ...(minItems === undefined ? {} : { minItems }),
128
+ ...(maxItems === undefined ? {} : { maxItems }),
129
+ }),
65
130
  items: {
66
131
  type: 'object',
67
132
  properties: {
@@ -82,9 +147,11 @@ export function codexTransportOutputSchema(outputSchema) {
82
147
  },
83
148
  finishReason: {
84
149
  type: 'string',
85
- enum: finishReasonEnum.length
86
- ? finishReasonEnum
87
- : ['tool_calls', 'stop', 'length'],
150
+ enum: requireSingleToolCall
151
+ ? ['tool_calls']
152
+ : finishReasonEnum.length
153
+ ? finishReasonEnum
154
+ : ['tool_calls', 'stop', 'length'],
88
155
  },
89
156
  },
90
157
  required: ['text', 'toolCalls', 'finishReason'],
@@ -92,6 +159,22 @@ export function codexTransportOutputSchema(outputSchema) {
92
159
  };
93
160
  }
94
161
 
162
+ export function codexStructuredOutputSchema(
163
+ outputSchema,
164
+ {
165
+ responseContract = STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
166
+ toolName,
167
+ requireSingleToolCall = false,
168
+ } = {},
169
+ ) {
170
+ return normalizeStructuredResponseContract(responseContract) === STRUCTURED_RESPONSE_CONTRACTS.OUTCOME
171
+ ? assertOutcomeResponseContract(responseContract, outputSchema)
172
+ : codexTransportOutputSchema(outputSchema, {
173
+ toolName,
174
+ requireSingleToolCall,
175
+ });
176
+ }
177
+
95
178
  export function codexTransportPrompt(prompt) {
96
179
  return [
97
180
  String(prompt || ''),
@@ -99,36 +182,137 @@ export function codexTransportPrompt(prompt) {
99
182
  'Codex structured-output transport:',
100
183
  'In the final JSON response, encode each toolCalls[].arguments value as a JSON string.',
101
184
  'The decoded string must be one JSON object matching the exact selected tool schema in the Tools catalog.',
185
+ 'Keep large code mutations bounded: prefer one cohesive file per tool call and continue remaining files in later model turns.',
102
186
  'Do not omit the id, name, arguments, text, toolCalls, or finishReason fields.',
103
187
  ].join('\n');
104
188
  }
105
189
 
190
+ export function codexStructuredOutputPrompt(
191
+ prompt,
192
+ responseContract = STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
193
+ ) {
194
+ return normalizeStructuredResponseContract(responseContract) === STRUCTURED_RESPONSE_CONTRACTS.OUTCOME
195
+ ? String(prompt || '')
196
+ : codexTransportPrompt(prompt);
197
+ }
198
+
199
+ export function codexTransportCorrectionPrompt(error) {
200
+ const callNumber = Number.isInteger(error?.toolCallIndex) ? error.toolCallIndex + 1 : 1;
201
+ const toolName = typeof error?.toolName === 'string' && error.toolName ? ` for the "${error.toolName}" tool` : '';
202
+ return [
203
+ 'Transport correction only.',
204
+ `Your previous structured response was rejected before any tool executed because tool call ${callNumber}${toolName} could not be decoded.`,
205
+ ...(typeof error?.parseMessage === 'string' && error.parseMessage ? [`Parser error: ${error.parseMessage}`] : []),
206
+ 'Regenerate only the same intended tool call. Do not redesign, reread files, source images, or choose a different action.',
207
+ 'If the intended call contains source code, return one cohesive file mutation only; later files can be sent in later model turns.',
208
+ 'Return exactly one JSON response object with text:"", one toolCalls item, and finishReason:"tool_calls".',
209
+ 'toolCalls[0].arguments must be a JSON string produced by JSON.stringify on one argument object.',
210
+ 'Escape every quote, backslash, newline, and control character inside that string. Do not use markdown or commentary.',
211
+ ].join('\n');
212
+ }
213
+
214
+ export function codexStructuredOutputCorrectionPrompt(
215
+ error,
216
+ responseContract = STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
217
+ ) {
218
+ if (normalizeStructuredResponseContract(responseContract) !== STRUCTURED_RESPONSE_CONTRACTS.OUTCOME) {
219
+ return codexTransportCorrectionPrompt(error);
220
+ }
221
+ return [
222
+ 'Transport correction only.',
223
+ 'Your previous harness outcome response could not be decoded.',
224
+ ...(typeof error?.parseMessage === 'string' && error.parseMessage
225
+ ? [`Parser error: ${error.parseMessage}`]
226
+ : []),
227
+ 'Continue from the existing workspace. Do not redesign or repeat completed work.',
228
+ 'Return exactly one JSON outcome object with status, summary, checks, and blockedReason.',
229
+ 'Do not use markdown or a model-turn toolCalls envelope.',
230
+ ].join('\n');
231
+ }
232
+
233
+ export function isCodexTransportRecoverableError(error) {
234
+ return Boolean(error?.codexTransportRecoverable === true || CODEX_TRANSPORT_ERROR_CODES.has(error?.code));
235
+ }
236
+
237
+ export function codexTransportErrorDiagnostics(error) {
238
+ return {
239
+ code: error?.code,
240
+ toolCallIndex: error?.toolCallIndex,
241
+ toolName: error?.toolName,
242
+ argumentsLength: error?.argumentsLength,
243
+ argumentsSha256: error?.argumentsSha256,
244
+ argumentsPreview: error?.argumentsPreview,
245
+ outputLength: error?.outputLength,
246
+ outputSha256: error?.outputSha256,
247
+ outputPreview: error?.outputPreview,
248
+ parseMessage: error?.parseMessage,
249
+ };
250
+ }
251
+
106
252
  export function decodeCodexTransportOutput(text) {
253
+ const source = String(text || '').trim();
254
+ const outputDiagnostics = {
255
+ outputLength: source.length,
256
+ outputSha256: sha256(source),
257
+ outputPreview: diagnosticPreview(source),
258
+ };
107
259
  let parsed;
108
260
  try {
109
- parsed = JSON.parse(String(text || '').trim());
110
- } catch {
111
- throw new Error('Codex returned malformed structured output.');
261
+ parsed = parseJsonObject(source);
262
+ } catch (cause) {
263
+ throw transportError('CODEX_STRUCTURED_OUTPUT_MALFORMED', 'Codex returned malformed structured output.', {
264
+ ...outputDiagnostics,
265
+ parseMessage: cause instanceof Error ? cause.message.slice(0, 500) : undefined,
266
+ });
112
267
  }
113
268
  if (!isRecord(parsed) || !Array.isArray(parsed.toolCalls)) {
114
- throw new Error('Codex returned an invalid model-turn response envelope.');
269
+ throw transportError(
270
+ 'CODEX_STRUCTURED_OUTPUT_INVALID',
271
+ 'Codex returned an invalid model-turn response envelope.',
272
+ outputDiagnostics,
273
+ );
115
274
  }
116
275
 
117
276
  const toolCalls = parsed.toolCalls.map((call, index) => {
118
277
  if (!isRecord(call) || typeof call.arguments !== 'string') {
119
- throw new Error(
278
+ throw transportError(
279
+ 'CODEX_TOOL_ARGUMENTS_TRANSPORT_INVALID',
120
280
  `Codex tool call ${index + 1} did not use the JSON-string arguments transport.`,
281
+ {
282
+ ...outputDiagnostics,
283
+ toolCallIndex: index,
284
+ toolName: isRecord(call) ? call.name : undefined,
285
+ },
121
286
  );
122
287
  }
123
288
 
289
+ const argumentDiagnostics = {
290
+ ...outputDiagnostics,
291
+ toolCallIndex: index,
292
+ toolName: typeof call.name === 'string' ? call.name : undefined,
293
+ argumentsLength: call.arguments.length,
294
+ argumentsSha256: sha256(call.arguments),
295
+ argumentsPreview: diagnosticPreview(call.arguments),
296
+ };
124
297
  let args;
125
298
  try {
126
- args = JSON.parse(call.arguments);
127
- } catch {
128
- throw new Error(`Codex tool call ${index + 1} contained malformed JSON arguments.`);
299
+ args = parseJsonObject(call.arguments);
300
+ } catch (cause) {
301
+ throw transportError(
302
+ 'CODEX_TOOL_ARGUMENTS_MALFORMED',
303
+ `Codex tool call ${index + 1} contained malformed JSON arguments.`,
304
+ {
305
+ ...argumentDiagnostics,
306
+ parseMessage: cause instanceof Error ? cause.message.slice(0, 500) : undefined,
307
+ },
308
+ );
129
309
  }
130
310
  if (!isRecord(args)) {
131
- throw new Error(`Codex tool call ${index + 1} arguments must decode to an object.`);
311
+ throw transportError(
312
+ 'CODEX_TOOL_ARGUMENTS_NOT_OBJECT',
313
+ `Codex tool call ${index + 1} arguments must decode to an object.`,
314
+ argumentDiagnostics,
315
+ );
132
316
  }
133
317
  return {
134
318
  ...call,
@@ -141,3 +325,46 @@ export function decodeCodexTransportOutput(text) {
141
325
  toolCalls,
142
326
  });
143
327
  }
328
+
329
+ export function decodeCodexStructuredOutput(
330
+ text,
331
+ responseContract = STRUCTURED_RESPONSE_CONTRACTS.MODEL_TURN,
332
+ ) {
333
+ if (normalizeStructuredResponseContract(responseContract) !== STRUCTURED_RESPONSE_CONTRACTS.OUTCOME) {
334
+ return decodeCodexTransportOutput(text);
335
+ }
336
+ const source = String(text || '').trim();
337
+ const outputDiagnostics = {
338
+ outputLength: source.length,
339
+ outputSha256: sha256(source),
340
+ outputPreview: diagnosticPreview(source),
341
+ };
342
+ let parsed;
343
+ try {
344
+ parsed = parseJsonObject(source);
345
+ } catch (cause) {
346
+ throw transportError(
347
+ 'APP_HARNESS_RESULT_INVALID',
348
+ 'Codex returned malformed harness outcome JSON.',
349
+ {
350
+ ...outputDiagnostics,
351
+ parseMessage:
352
+ cause instanceof Error ? cause.message.slice(0, 500) : undefined,
353
+ },
354
+ );
355
+ }
356
+ if (
357
+ !isRecord(parsed)
358
+ || typeof parsed.status !== 'string'
359
+ || typeof parsed.summary !== 'string'
360
+ || !Array.isArray(parsed.checks)
361
+ || !Object.prototype.hasOwnProperty.call(parsed, 'blockedReason')
362
+ ) {
363
+ throw transportError(
364
+ 'APP_HARNESS_RESULT_INVALID',
365
+ 'Codex returned an invalid harness outcome response.',
366
+ outputDiagnostics,
367
+ );
368
+ }
369
+ return JSON.stringify(parsed);
370
+ }
@@ -0,0 +1,197 @@
1
+ import { streamText } from 'ai';
2
+ import { createAnthropic } from '@ai-sdk/anthropic';
3
+ import { createDeepSeek } from '@ai-sdk/deepseek';
4
+ import { createGoogleGenerativeAI } from '@ai-sdk/google';
5
+ import { createOpenAI } from '@ai-sdk/openai';
6
+ import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
7
+ import { createXai } from '@ai-sdk/xai';
8
+ import { createOpenRouter } from '@openrouter/ai-sdk-provider';
9
+ import { normalizeCompanionTokenUsage } from '../agentOutput.js';
10
+
11
+ const PROVIDERS = new Set([
12
+ 'openai',
13
+ 'anthropic',
14
+ 'google',
15
+ 'openrouter',
16
+ 'deepseek',
17
+ 'xai',
18
+ 'openai-compatible',
19
+ ]);
20
+
21
+ function text(value, maximum = 4096) {
22
+ return typeof value === 'string' && value.trim()
23
+ ? value.trim().slice(0, maximum)
24
+ : '';
25
+ }
26
+
27
+ function normalizeBaseUrl(value) {
28
+ const raw = text(value, 2048);
29
+ if (!raw) return undefined;
30
+ const parsed = new URL(raw);
31
+ const hostname = parsed.hostname.replace(/^\[|\]$/g, '').toLowerCase();
32
+ const loopback =
33
+ hostname === 'localhost'
34
+ || hostname === 'localhost.'
35
+ || hostname === '127.0.0.1'
36
+ || hostname === '::1';
37
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
38
+ throw new Error('Local provider URLs cannot contain credentials, query strings, or fragments.');
39
+ }
40
+ if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && loopback)) {
41
+ throw new Error('Local provider URLs must use HTTPS, except for loopback development endpoints.');
42
+ }
43
+ return parsed.toString().replace(/\/+$/, '');
44
+ }
45
+
46
+ function languageModel(credentials, model) {
47
+ const provider = text(credentials?.provider, 80).toLowerCase();
48
+ const apiKey = text(credentials?.apiKey, 8192);
49
+ const baseURL = normalizeBaseUrl(credentials?.baseURL);
50
+ if (!PROVIDERS.has(provider)) {
51
+ throw new Error(`Unsupported local BYOK provider "${provider || 'unknown'}".`);
52
+ }
53
+ if (!apiKey) throw new Error('The local BYOK profile has no API key.');
54
+ if (!text(model, 255)) throw new Error('The local BYOK profile has no model.');
55
+
56
+ switch (provider) {
57
+ case 'openai':
58
+ return createOpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) }).chat(model);
59
+ case 'anthropic':
60
+ return createAnthropic({ apiKey, ...(baseURL ? { baseURL } : {}) }).messages(model);
61
+ case 'google':
62
+ return createGoogleGenerativeAI({ apiKey, ...(baseURL ? { baseURL } : {}) }).chat(model);
63
+ case 'openrouter':
64
+ return createOpenRouter({
65
+ apiKey,
66
+ appName: 'InstaWebAI',
67
+ compatibility: 'strict',
68
+ }).chat(model);
69
+ case 'deepseek':
70
+ return createDeepSeek({ apiKey, ...(baseURL ? { baseURL } : {}) }).chat(model);
71
+ case 'xai':
72
+ return createXai({ apiKey, ...(baseURL ? { baseURL } : {}) }).chat(model);
73
+ case 'openai-compatible':
74
+ if (!baseURL) throw new Error('OpenAI-compatible local BYOK profiles require a base URL.');
75
+ return createOpenAICompatible({
76
+ name: 'instawebai-local-byok',
77
+ apiKey,
78
+ baseURL,
79
+ }).chatModel(model);
80
+ default:
81
+ throw new Error(`Unsupported local BYOK provider "${provider}".`);
82
+ }
83
+ }
84
+
85
+ export function createDirectByokAdapter({
86
+ profile,
87
+ credentials,
88
+ trace,
89
+ } = {}) {
90
+ if (!profile?.id) throw new Error('A local BYOK runtime profile is required.');
91
+ const activeTurns = new Map();
92
+
93
+ async function runModelTurn({
94
+ runId,
95
+ sessionId,
96
+ prompt,
97
+ model,
98
+ timeoutMs = 180_000,
99
+ maxDurationMs = 15 * 60_000,
100
+ onProgress,
101
+ } = {}) {
102
+ const turnId = sessionId || runId;
103
+ if (!turnId) throw new Error('Local BYOK model turn requires a session id.');
104
+ if (activeTurns.has(turnId)) throw new Error('Local BYOK model turn is already active.');
105
+ const abortController = new AbortController();
106
+ let deadlineReached = false;
107
+ const timer = setTimeout(() => {
108
+ deadlineReached = true;
109
+ abortController.abort();
110
+ }, maxDurationMs);
111
+ timer.unref?.();
112
+ activeTurns.set(turnId, abortController);
113
+ try {
114
+ const selectedModel = text(model, 255) || profile.models?.[0]?.invocationName || profile.models?.[0]?.id;
115
+ onProgress?.({
116
+ kind: 'turn_started',
117
+ provider: `local-byok:${credentials.provider}`,
118
+ occurredAt: new Date().toISOString(),
119
+ });
120
+ const result = streamText({
121
+ model: languageModel(credentials, selectedModel),
122
+ prompt: String(prompt || ''),
123
+ maxOutputTokens: 6_000,
124
+ abortSignal: abortController.signal,
125
+ onChunk({ chunk }) {
126
+ const type = String(chunk?.type || '');
127
+ onProgress?.({
128
+ kind: /reasoning/i.test(type) ? 'reasoning_delta' : 'output_delta',
129
+ provider: `local-byok:${credentials.provider}`,
130
+ occurredAt: new Date().toISOString(),
131
+ });
132
+ },
133
+ });
134
+ const [resultText, usage, response] = await Promise.all([
135
+ result.text,
136
+ result.usage,
137
+ result.response,
138
+ ]);
139
+ return {
140
+ text: resultText || '',
141
+ model: response?.modelId || selectedModel,
142
+ tokenUsage: normalizeCompanionTokenUsage(usage || {}),
143
+ usageAvailable: Boolean(usage),
144
+ usageSource: `local-byok:${credentials.provider}`,
145
+ usageAccuracy: 'reported',
146
+ };
147
+ } catch (error) {
148
+ if (deadlineReached) {
149
+ throw Object.assign(
150
+ new Error(`The model turn exceeded the ${maxDurationMs}ms safety limit.`),
151
+ {
152
+ code: 'APP_AGENT_TURN_HARD_TIMEOUT',
153
+ retryable: true,
154
+ },
155
+ );
156
+ }
157
+ if (abortController.signal.aborted) {
158
+ throw Object.assign(new Error('Local BYOK model turn was cancelled.'), {
159
+ code: 'RUN_CANCELLED',
160
+ });
161
+ }
162
+ throw error;
163
+ } finally {
164
+ clearTimeout(timer);
165
+ activeTurns.delete(turnId);
166
+ trace?.info?.('local_byok_turn_closed', {
167
+ profileId: profile.id,
168
+ sessionId: turnId,
169
+ });
170
+ }
171
+ }
172
+
173
+ async function cancel(sessionId) {
174
+ activeTurns.get(sessionId)?.abort();
175
+ }
176
+
177
+ function close() {
178
+ for (const controller of activeTurns.values()) controller.abort();
179
+ activeTurns.clear();
180
+ }
181
+
182
+ return {
183
+ id: profile.id,
184
+ driverKind: 'direct-byok',
185
+ label: profile.label || 'Local API key',
186
+ detect: async () => ({
187
+ ok: true,
188
+ installed: true,
189
+ signedIn: Boolean(credentials?.apiKey),
190
+ agent: 'direct-byok',
191
+ }),
192
+ models: async () => profile.models || [],
193
+ runModelTurn,
194
+ cancel,
195
+ close,
196
+ };
197
+ }
@@ -1,4 +1,7 @@
1
1
  import { createCodexAppServerAdapter } from './codexAppServer.js';
2
+ import { createDirectByokAdapter } from './directByok.js';
3
+ import { createOpenCodeAdapter } from './openCode.js';
4
+ import { createAcpAdapter } from './acp.js';
2
5
 
3
6
  /**
4
7
  * Local agent adapters.
@@ -22,23 +25,46 @@ import { createCodexAppServerAdapter } from './codexAppServer.js';
22
25
  * @property {Promise<{ok:boolean}>} completed Resolves when sign-in finishes.
23
26
  *
24
27
  * @typedef {Object} LocalAgentAdapter
25
- * @property {'codex'|'cursor'|'claude-api'} id
28
+ * @property {string} id
26
29
  * @property {() => Promise<ProviderStatus>} detect
27
30
  * @property {(() => Promise<AuthChallenge>)=} authenticate
28
31
  * @property {() => Promise<Array<object>>} models
29
- * @property {(input: object) => Promise<{text: string, tokenUsage?: object}>} runModelTurn
32
+ * @property {(input: object & {responseContract?: 'model-turn'}) => Promise<{text: string, tokenUsage?: object}>} runModelTurn
33
+ * @property {((input: object & {responseContract: 'outcome', outputSchema: object}) => Promise<{text: string, tokenUsage?: object}>)=} runOutcome
30
34
  * @property {(sessionId: string) => Promise<void>} cancel
31
35
  * @property {(() => Promise<void>)=} logout
32
36
  */
33
37
 
34
- export const ADAPTER_FACTORIES = {
35
- codex: createCodexAppServerAdapter,
38
+ export const DRIVER_FACTORIES = {
39
+ 'codex-app-server': (profile, options) =>
40
+ createCodexAppServerAdapter(options),
41
+ 'direct-byok': (profile, options) =>
42
+ createDirectByokAdapter({
43
+ ...options,
44
+ profile,
45
+ credentials: options.credentials || profile.credentials,
46
+ }),
47
+ opencode: (profile, options) =>
48
+ createOpenCodeAdapter({ ...options, profile }),
49
+ acp: (profile, options) =>
50
+ createAcpAdapter({ ...options, profile }),
36
51
  };
37
52
 
38
53
  /**
39
54
  * Returns an adapter for the agent, or null when the agent has no adapter.
40
55
  */
41
- export function createLocalAgentAdapter(agent, options = {}) {
42
- if (agent === 'codex') return createCodexAppServerAdapter(options);
43
- return null;
56
+ export function createLocalAgentAdapter(profileOrAgent, options = {}) {
57
+ const profile =
58
+ typeof profileOrAgent === 'string'
59
+ ? profileOrAgent === 'codex'
60
+ ? { id: 'codex:default', driver: 'codex-app-server' }
61
+ : null
62
+ : profileOrAgent;
63
+ if (!profile?.driver) return null;
64
+ const factory = DRIVER_FACTORIES[profile.driver];
65
+ if (!factory) return null;
66
+ const adapter = factory(profile, options);
67
+ if (adapter && !adapter.driverKind) adapter.driverKind = profile.driver;
68
+ if (adapter && profile.id) adapter.profileId = profile.id;
69
+ return adapter;
44
70
  }