@gakim-digital/dexter-bridge 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/agent.js +265 -21
- package/src/agentOutput.js +14 -2
- package/src/config.js +1 -1
- package/src/protocol.js +44 -6
- package/src/providers/codexAppServer.js +19 -3
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gakim-digital/dexter-bridge",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Local Companion bridge for the Dexter Framer plugin — runs Codex or Claude Code on your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"dexter-bridge": "
|
|
7
|
+
"dexter-bridge": "bin/dexter-bridge.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
package/src/agent.js
CHANGED
|
@@ -12,8 +12,11 @@ import {
|
|
|
12
12
|
normalizeCompanionModelName,
|
|
13
13
|
} from './config.js';
|
|
14
14
|
import {
|
|
15
|
+
buildModelTurnDeltaPrompt,
|
|
16
|
+
buildModelTurnFallbackPrompt,
|
|
15
17
|
buildModelTurnPrompt,
|
|
16
18
|
extractJsonObject,
|
|
19
|
+
modelTurnOutputSchema,
|
|
17
20
|
normalizeModelTurnCompletion,
|
|
18
21
|
runSummary,
|
|
19
22
|
} from './protocol.js';
|
|
@@ -27,6 +30,48 @@ import {
|
|
|
27
30
|
} from './logger.js';
|
|
28
31
|
import { createLocalAgentAdapter } from './providers/index.js';
|
|
29
32
|
|
|
33
|
+
const companionModelSessions = new Map();
|
|
34
|
+
const sharedProviderAdapters = new Map();
|
|
35
|
+
const MAX_COMPANION_MODEL_SESSIONS = 64;
|
|
36
|
+
|
|
37
|
+
function enabledFlag(value, fallback = false) {
|
|
38
|
+
const raw = String(value ?? '').trim().toLowerCase();
|
|
39
|
+
if (!raw) return fallback;
|
|
40
|
+
return !['0', 'false', 'no', 'off', 'disabled'].includes(raw);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function deltaModelSessionsEnabled(env = process.env) {
|
|
44
|
+
return enabledFlag(env.DEXTER_DELTA_MODEL_SESSIONS, true);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function companionSessionKey(run) {
|
|
48
|
+
return run?.modelTurn?.session?.sessionId || run?.turnId || run?.runId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function rememberedCompanionSession(key) {
|
|
52
|
+
const session = key ? companionModelSessions.get(key) : null;
|
|
53
|
+
if (!session) return null;
|
|
54
|
+
companionModelSessions.delete(key);
|
|
55
|
+
companionModelSessions.set(key, session);
|
|
56
|
+
return session;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rememberCompanionSession(key, session) {
|
|
60
|
+
if (!key) return;
|
|
61
|
+
companionModelSessions.delete(key);
|
|
62
|
+
companionModelSessions.set(key, session);
|
|
63
|
+
while (companionModelSessions.size > MAX_COMPANION_MODEL_SESSIONS) {
|
|
64
|
+
const oldest = companionModelSessions.keys().next().value;
|
|
65
|
+
if (!oldest) break;
|
|
66
|
+
companionModelSessions.delete(oldest);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resumeFailure(error) {
|
|
71
|
+
const message = error?.message || String(error || '');
|
|
72
|
+
return /resume|session|thread|conversation|not found|unknown id|expired/i.test(message);
|
|
73
|
+
}
|
|
74
|
+
|
|
30
75
|
export const AGENT_DEFINITIONS = {
|
|
31
76
|
'claude-code': {
|
|
32
77
|
id: 'claude-code',
|
|
@@ -150,7 +195,7 @@ function argsWithSelectedModel(args, modelDefinition, definition) {
|
|
|
150
195
|
function argsWithStructuredOutput(args, definition, env = process.env) {
|
|
151
196
|
if (!structuredUsageEnabled(env)) return args;
|
|
152
197
|
if (definition.id === 'claude-code') {
|
|
153
|
-
return
|
|
198
|
+
return [...argsWithoutFlagValue(args, '--output-format'), '--output-format', 'json'];
|
|
154
199
|
}
|
|
155
200
|
if (definition.id === 'codex') {
|
|
156
201
|
return argsIncludeFlag(args, '--json') ? args : [...args, '--json'];
|
|
@@ -158,6 +203,54 @@ function argsWithStructuredOutput(args, definition, env = process.env) {
|
|
|
158
203
|
return args;
|
|
159
204
|
}
|
|
160
205
|
|
|
206
|
+
function argsWithClaudeIsolation(args, definition) {
|
|
207
|
+
if (definition.id !== 'claude-code') return args;
|
|
208
|
+
const isolated = argsWithoutVariadicFlag(args, '--tools');
|
|
209
|
+
isolated.push('--tools', '');
|
|
210
|
+
if (!argsIncludeFlag(isolated, '--disable-slash-commands')) isolated.push('--disable-slash-commands');
|
|
211
|
+
if (!argsIncludeFlag(isolated, '--safe-mode')) isolated.push('--safe-mode');
|
|
212
|
+
if (!argsIncludeFlag(isolated, '--strict-mcp-config')) isolated.push('--strict-mcp-config');
|
|
213
|
+
if (!argsIncludeFlag(isolated, '--no-chrome')) isolated.push('--no-chrome');
|
|
214
|
+
return isolated;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function argsWithoutFlagValue(args, flag) {
|
|
218
|
+
const filtered = [];
|
|
219
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
220
|
+
const arg = args[index];
|
|
221
|
+
if (arg === flag) {
|
|
222
|
+
index += 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (arg.startsWith(`${flag}=`)) continue;
|
|
226
|
+
filtered.push(arg);
|
|
227
|
+
}
|
|
228
|
+
return filtered;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function argsWithoutVariadicFlag(args, flag) {
|
|
232
|
+
const filtered = [];
|
|
233
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
234
|
+
const arg = args[index];
|
|
235
|
+
if (arg === flag) {
|
|
236
|
+
while (index + 1 < args.length && !String(args[index + 1]).startsWith('-')) index += 1;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (arg.startsWith(`${flag}=`)) continue;
|
|
240
|
+
filtered.push(arg);
|
|
241
|
+
}
|
|
242
|
+
return filtered;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function argsWithOutputSchema(args, definition, outputSchema) {
|
|
246
|
+
if (definition.id !== 'claude-code' || !outputSchema) return args;
|
|
247
|
+
return [
|
|
248
|
+
...argsWithoutFlagValue(args, '--json-schema'),
|
|
249
|
+
'--json-schema',
|
|
250
|
+
JSON.stringify(outputSchema),
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
|
|
161
254
|
function argsWithPromptInput(args, definition) {
|
|
162
255
|
if (definition.id !== 'codex' || args.includes('-')) return args;
|
|
163
256
|
return [...args, '-'];
|
|
@@ -222,7 +315,9 @@ export function buildAgentArgs(definition, modelDefinition, env = process.env, o
|
|
|
222
315
|
const requiredArgs = argsWithRequiredAgentFlags(baseArgs, definition);
|
|
223
316
|
const modelArgs = argsWithSelectedModel(requiredArgs, modelDefinition, definition);
|
|
224
317
|
const structuredArgs = argsWithStructuredOutput(modelArgs, definition, env);
|
|
225
|
-
const
|
|
318
|
+
const isolatedArgs = argsWithClaudeIsolation(structuredArgs, definition);
|
|
319
|
+
const schemaArgs = argsWithOutputSchema(isolatedArgs, definition, options.outputSchema);
|
|
320
|
+
const resumedArgs = argsWithResumedSession(schemaArgs, definition, options.resumeSessionId);
|
|
226
321
|
return argsWithPromptInput(resumedArgs, definition);
|
|
227
322
|
}
|
|
228
323
|
|
|
@@ -738,6 +833,7 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
738
833
|
const modelDefinition = companionModelDefinition(options.model, definition.id);
|
|
739
834
|
const args = buildAgentArgs(definition, modelDefinition, process.env, {
|
|
740
835
|
resumeSessionId: options.resumeSessionId,
|
|
836
|
+
outputSchema: options.outputSchema,
|
|
741
837
|
});
|
|
742
838
|
const timeoutMs = options.timeoutMs || Number(process.env.DEXTER_BRIDGE_AGENT_TIMEOUT_MS || 120000);
|
|
743
839
|
options.trace?.info('agent_step_invoke', {
|
|
@@ -781,7 +877,20 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
781
877
|
? requestedModel.trim()
|
|
782
878
|
: selectedModel?.id || definition.model;
|
|
783
879
|
const adapter = options.providerAdapter || null;
|
|
784
|
-
const
|
|
880
|
+
const sessionKey = companionSessionKey(run);
|
|
881
|
+
const rememberedSession = rememberedCompanionSession(sessionKey);
|
|
882
|
+
const deltaRequested =
|
|
883
|
+
deltaModelSessionsEnabled(options.env)
|
|
884
|
+
&& run?.modelTurn?.session?.contextMode === 'delta';
|
|
885
|
+
let callContextMode = deltaRequested && rememberedSession ? 'delta' : 'full';
|
|
886
|
+
let prompt = callContextMode === 'delta'
|
|
887
|
+
? buildModelTurnDeltaPrompt(run.modelTurn)
|
|
888
|
+
: deltaRequested
|
|
889
|
+
? buildModelTurnFallbackPrompt(run.modelTurn)
|
|
890
|
+
: buildModelTurnPrompt(run.modelTurn);
|
|
891
|
+
const callStartedAt = Date.now();
|
|
892
|
+
let providerSessionId;
|
|
893
|
+
let fallbackAfterResumeFailure = false;
|
|
785
894
|
const statusResponse = await send('status', {
|
|
786
895
|
stage: 'model_turn',
|
|
787
896
|
message: `${definition.label} is generating the next Dexter action.`,
|
|
@@ -791,10 +900,39 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
791
900
|
throw new CompanionRunCancelledError();
|
|
792
901
|
}
|
|
793
902
|
const usageAccumulator = createCompanionUsageAccumulator(definition.id);
|
|
903
|
+
const failedModelCall = (error) => {
|
|
904
|
+
const failure = error instanceof Error
|
|
905
|
+
? error
|
|
906
|
+
: new Error(String(error || 'The companion model call failed.'));
|
|
907
|
+
usageAccumulator.add(failure.companionUsage || {});
|
|
908
|
+
const snapshot = usageAccumulator.snapshot();
|
|
909
|
+
failure.companionUsage = {
|
|
910
|
+
...snapshot,
|
|
911
|
+
modelCalls: [{
|
|
912
|
+
callId: run.runId,
|
|
913
|
+
step: Number(run?.modelTurn?.step ?? 0),
|
|
914
|
+
model: selectedModelId,
|
|
915
|
+
status: 'failed',
|
|
916
|
+
durationMs: Date.now() - callStartedAt,
|
|
917
|
+
promptChars: prompt.length,
|
|
918
|
+
requestedContextMode: deltaRequested ? 'delta' : 'full',
|
|
919
|
+
contextMode: callContextMode,
|
|
920
|
+
resumed: callContextMode === 'delta',
|
|
921
|
+
resumeSessionAvailable: Boolean(rememberedSession),
|
|
922
|
+
fallbackAfterResumeFailure,
|
|
923
|
+
...snapshot.tokenUsage,
|
|
924
|
+
estimatedCostUSD: snapshot.estimatedCostUSD,
|
|
925
|
+
}],
|
|
926
|
+
};
|
|
927
|
+
return failure;
|
|
928
|
+
};
|
|
794
929
|
let resultText;
|
|
795
930
|
if (adapter) {
|
|
796
|
-
const sessionId = adapterSessionId(run);
|
|
931
|
+
const sessionId = sessionKey || adapterSessionId(run);
|
|
797
932
|
const invocationModel = adapterInvocationModel(run, selectedModel, definition.id);
|
|
933
|
+
if (callContextMode === 'full' && rememberedSession) {
|
|
934
|
+
await adapter.resetSession?.(sessionId);
|
|
935
|
+
}
|
|
798
936
|
options.trace?.info('agent_adapter_invoke', {
|
|
799
937
|
adapter: adapter.id,
|
|
800
938
|
agent: definition.id,
|
|
@@ -803,17 +941,19 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
803
941
|
invocationModel,
|
|
804
942
|
promptChars: prompt.length,
|
|
805
943
|
});
|
|
806
|
-
const
|
|
944
|
+
const adapterInput = {
|
|
807
945
|
runId: run.runId,
|
|
808
946
|
sessionId,
|
|
809
947
|
prompt,
|
|
810
948
|
model: invocationModel,
|
|
949
|
+
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
811
950
|
timeoutMs: boundedDurationMs(
|
|
812
951
|
options.timeoutMs ?? options.env?.DEXTER_BRIDGE_AGENT_TIMEOUT_MS,
|
|
813
952
|
120000,
|
|
814
953
|
1000,
|
|
815
954
|
),
|
|
816
|
-
}
|
|
955
|
+
};
|
|
956
|
+
const adapterOptions = {
|
|
817
957
|
send,
|
|
818
958
|
trace: options.trace,
|
|
819
959
|
controlPollMs: boundedDurationMs(
|
|
@@ -822,9 +962,30 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
822
962
|
10,
|
|
823
963
|
30000,
|
|
824
964
|
),
|
|
825
|
-
}
|
|
965
|
+
};
|
|
966
|
+
let result;
|
|
967
|
+
try {
|
|
968
|
+
result = await callProviderAdapter(adapter, adapterInput, adapterOptions);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
if (callContextMode !== 'delta' || !resumeFailure(error)) throw failedModelCall(error);
|
|
971
|
+
usageAccumulator.add(error?.companionUsage || {});
|
|
972
|
+
fallbackAfterResumeFailure = true;
|
|
973
|
+
callContextMode = 'full';
|
|
974
|
+
prompt = buildModelTurnFallbackPrompt(run.modelTurn);
|
|
975
|
+
await adapter.resetSession?.(sessionId);
|
|
976
|
+
options.trace?.warn('agent_adapter_resume_fallback', {
|
|
977
|
+
sessionId,
|
|
978
|
+
error: errorMeta(error),
|
|
979
|
+
fallbackPromptChars: prompt.length,
|
|
980
|
+
});
|
|
981
|
+
result = await callProviderAdapter(adapter, { ...adapterInput, prompt }, adapterOptions)
|
|
982
|
+
.catch((error) => {
|
|
983
|
+
throw failedModelCall(error);
|
|
984
|
+
});
|
|
985
|
+
}
|
|
826
986
|
usageAccumulator.add(result);
|
|
827
987
|
resultText = result?.text;
|
|
988
|
+
providerSessionId = result?.threadId || result?.sessionId;
|
|
828
989
|
} else {
|
|
829
990
|
const runtime = await resolveAgentRuntime(definition, selectedModel, {
|
|
830
991
|
env: options.env,
|
|
@@ -832,25 +993,90 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
832
993
|
if (!runtime.ok) {
|
|
833
994
|
throw new Error(runtime.error || `${definition.label} is not available.`);
|
|
834
995
|
}
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
996
|
+
const resumeSessionId =
|
|
997
|
+
callContextMode === 'delta'
|
|
998
|
+
&& agentSessionResumeEnabled(definition, options.env)
|
|
999
|
+
? rememberedSession?.providerSessionId
|
|
1000
|
+
: undefined;
|
|
1001
|
+
let result;
|
|
1002
|
+
try {
|
|
1003
|
+
result = await callLocalJsonAgent(definition.id, prompt, {
|
|
1004
|
+
...options,
|
|
1005
|
+
model: selectedModelId,
|
|
1006
|
+
runtime,
|
|
1007
|
+
resumeSessionId,
|
|
1008
|
+
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
1009
|
+
});
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
if (callContextMode !== 'delta' || !resumeFailure(error)) throw failedModelCall(error);
|
|
1012
|
+
fallbackAfterResumeFailure = true;
|
|
1013
|
+
callContextMode = 'full';
|
|
1014
|
+
prompt = buildModelTurnFallbackPrompt(run.modelTurn);
|
|
1015
|
+
options.trace?.warn('agent_cli_resume_fallback', {
|
|
1016
|
+
resumeSessionId,
|
|
1017
|
+
error: errorMeta(error),
|
|
1018
|
+
fallbackPromptChars: prompt.length,
|
|
1019
|
+
});
|
|
1020
|
+
result = await callLocalJsonAgent(definition.id, prompt, {
|
|
1021
|
+
...options,
|
|
1022
|
+
model: selectedModelId,
|
|
1023
|
+
runtime,
|
|
1024
|
+
resumeSessionId: undefined,
|
|
1025
|
+
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
1026
|
+
}).catch((error) => {
|
|
1027
|
+
throw failedModelCall(error);
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
840
1030
|
const parsed = parseAgentOutput(definition.id, result.stdout);
|
|
841
1031
|
usageAccumulator.add(parsed);
|
|
842
1032
|
resultText = parsed.resultText;
|
|
1033
|
+
providerSessionId = parsed.sessionId;
|
|
1034
|
+
}
|
|
1035
|
+
if (providerSessionId || adapter) {
|
|
1036
|
+
rememberCompanionSession(sessionKey, {
|
|
1037
|
+
providerSessionId: providerSessionId || rememberedSession?.providerSessionId || sessionKey,
|
|
1038
|
+
agent: definition.id,
|
|
1039
|
+
updatedAt: Date.now(),
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
const usageSnapshot = usageAccumulator.snapshot();
|
|
1043
|
+
const callTelemetry = {
|
|
1044
|
+
callId: run.runId,
|
|
1045
|
+
step: Number(run?.modelTurn?.step ?? 0),
|
|
1046
|
+
model: selectedModelId,
|
|
1047
|
+
durationMs: Date.now() - callStartedAt,
|
|
1048
|
+
promptChars: prompt.length,
|
|
1049
|
+
requestedContextMode: deltaRequested ? 'delta' : 'full',
|
|
1050
|
+
contextMode: callContextMode,
|
|
1051
|
+
resumed: callContextMode === 'delta',
|
|
1052
|
+
resumeSessionAvailable: Boolean(rememberedSession),
|
|
1053
|
+
fallbackAfterResumeFailure,
|
|
1054
|
+
...usageSnapshot.tokenUsage,
|
|
1055
|
+
estimatedCostUSD: usageSnapshot.estimatedCostUSD,
|
|
1056
|
+
};
|
|
1057
|
+
const usage = {
|
|
1058
|
+
...usageSnapshot,
|
|
1059
|
+
modelCalls: [{ ...callTelemetry, status: 'succeeded' }],
|
|
1060
|
+
};
|
|
1061
|
+
let completion;
|
|
1062
|
+
try {
|
|
1063
|
+
completion = normalizeModelTurnCompletion(
|
|
1064
|
+
extractJsonObject(resultText),
|
|
1065
|
+
selectedModelId,
|
|
1066
|
+
);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
error.companionUsage = {
|
|
1069
|
+
...usage,
|
|
1070
|
+
modelCalls: [{ ...callTelemetry, status: 'rejected' }],
|
|
1071
|
+
};
|
|
1072
|
+
throw error;
|
|
843
1073
|
}
|
|
844
|
-
const completion = normalizeModelTurnCompletion(
|
|
845
|
-
extractJsonObject(resultText),
|
|
846
|
-
selectedModelId,
|
|
847
|
-
);
|
|
848
1074
|
await send('done', {
|
|
849
1075
|
operationType: 'chat',
|
|
850
1076
|
outcome: 'answer',
|
|
851
1077
|
completion,
|
|
852
1078
|
model: selectedModelId,
|
|
853
|
-
...
|
|
1079
|
+
...usage,
|
|
854
1080
|
});
|
|
855
1081
|
}
|
|
856
1082
|
|
|
@@ -885,14 +1111,26 @@ export async function executeRun(run, {
|
|
|
885
1111
|
const runModel = run?.companion?.model?.id || run?.model || selectedModel;
|
|
886
1112
|
const usageAccumulator = createCompanionUsageAccumulator(normalizedAgent);
|
|
887
1113
|
const adapterProvided = providerAdapter !== undefined;
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
1114
|
+
const shareAdapter = !adapterProvided && deltaModelSessionsEnabled(env);
|
|
1115
|
+
let activeAdapter = adapterProvided ? providerAdapter : null;
|
|
1116
|
+
if (!adapterProvided && shareAdapter) {
|
|
1117
|
+
activeAdapter = sharedProviderAdapters.get(normalizedAgent) || null;
|
|
1118
|
+
if (!activeAdapter) {
|
|
1119
|
+
activeAdapter = createLocalAgentAdapter(normalizedAgent, {
|
|
1120
|
+
...adapterOptions,
|
|
1121
|
+
env: adapterOptions?.env || env,
|
|
1122
|
+
trace,
|
|
1123
|
+
});
|
|
1124
|
+
if (activeAdapter) sharedProviderAdapters.set(normalizedAgent, activeAdapter);
|
|
1125
|
+
}
|
|
1126
|
+
} else if (!adapterProvided) {
|
|
1127
|
+
activeAdapter = createLocalAgentAdapter(normalizedAgent, {
|
|
891
1128
|
...adapterOptions,
|
|
892
1129
|
env: adapterOptions?.env || env,
|
|
893
1130
|
trace,
|
|
894
1131
|
});
|
|
895
|
-
|
|
1132
|
+
}
|
|
1133
|
+
const ownsAdapter = !adapterProvided && !shareAdapter && Boolean(activeAdapter);
|
|
896
1134
|
if (
|
|
897
1135
|
activeAdapter?.id
|
|
898
1136
|
&& activeAdapter.id !== normalizedAgent
|
|
@@ -945,6 +1183,12 @@ export async function executeRun(run, {
|
|
|
945
1183
|
}
|
|
946
1184
|
}
|
|
947
1185
|
|
|
1186
|
+
export function __resetAgentSessionsForTests() {
|
|
1187
|
+
companionModelSessions.clear();
|
|
1188
|
+
for (const adapter of sharedProviderAdapters.values()) adapter?.close?.();
|
|
1189
|
+
sharedProviderAdapters.clear();
|
|
1190
|
+
}
|
|
1191
|
+
|
|
948
1192
|
export function checkCommand(command, args = ['--version'], timeoutMs = 10000) {
|
|
949
1193
|
return runProcess(command, args, '', { timeoutMs })
|
|
950
1194
|
.then((result) => ({ ok: true, command, output: (result.stdout || result.stderr || '').trim() }))
|
package/src/agentOutput.js
CHANGED
|
@@ -92,7 +92,13 @@ export function parseClaudeOutput(stdout) {
|
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const
|
|
95
|
+
const structuredOutput = parsed.structured_output ?? parsed.structuredOutput;
|
|
96
|
+
const hasEnvelope =
|
|
97
|
+
typeof parsed.result === 'string'
|
|
98
|
+
|| structuredOutput !== undefined
|
|
99
|
+
|| parsed.usage
|
|
100
|
+
|| parsed.total_cost_usd !== undefined
|
|
101
|
+
|| parsed.modelUsage;
|
|
96
102
|
if (!hasEnvelope) {
|
|
97
103
|
return {
|
|
98
104
|
resultText: String(stdout || '').trim(),
|
|
@@ -108,7 +114,13 @@ export function parseClaudeOutput(stdout) {
|
|
|
108
114
|
const modelUsage = normalizeModelUsage(parsed.modelUsage || parsed.model_usage);
|
|
109
115
|
const estimatedCostUSD = optionalNonNegativeNumber(parsed.total_cost_usd ?? parsed.totalCostUsd);
|
|
110
116
|
return {
|
|
111
|
-
resultText: typeof
|
|
117
|
+
resultText: structuredOutput && typeof structuredOutput === 'object'
|
|
118
|
+
? JSON.stringify(structuredOutput)
|
|
119
|
+
: typeof structuredOutput === 'string'
|
|
120
|
+
? structuredOutput.trim()
|
|
121
|
+
: typeof parsed.result === 'string'
|
|
122
|
+
? parsed.result.trim()
|
|
123
|
+
: '',
|
|
112
124
|
tokenUsage,
|
|
113
125
|
model: typeof parsed.model === 'string' ? parsed.model : undefined,
|
|
114
126
|
sessionId: typeof parsed.session_id === 'string'
|
package/src/config.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
5
|
export const DEFAULT_API_BASE_URL = 'http://localhost:3800/iwm-api/0.0.1';
|
|
6
|
-
export const BRIDGE_VERSION = '0.5.
|
|
6
|
+
export const BRIDGE_VERSION = '0.5.1';
|
|
7
7
|
export const BRIDGE_CAPABILITIES = [
|
|
8
8
|
'model-turn-v1',
|
|
9
9
|
];
|
package/src/protocol.js
CHANGED
|
@@ -71,16 +71,18 @@ export function compactToolCatalog(tools = []) {
|
|
|
71
71
|
.filter((tool) => tool.name);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
?
|
|
74
|
+
function compactMessages(messages = [], limit = 64) {
|
|
75
|
+
return Array.isArray(messages)
|
|
76
|
+
? messages.slice(-limit).map((message) => ({
|
|
77
77
|
role: message?.role,
|
|
78
78
|
content: compactModelTurnContent(message?.content),
|
|
79
79
|
toolCalls: message?.toolCalls,
|
|
80
80
|
toolCallId: message?.toolCallId,
|
|
81
81
|
}))
|
|
82
82
|
: [];
|
|
83
|
-
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function modelTurnInstructions() {
|
|
84
86
|
return [
|
|
85
87
|
'You are the model engine for Dexter. The server owns the agent loop and executes all tools.',
|
|
86
88
|
'Return exactly one JSON object and no markdown.',
|
|
@@ -89,13 +91,49 @@ export function buildModelTurnPrompt(modelTurn = {}) {
|
|
|
89
91
|
'Each toolCalls[].arguments value must be a JSON-encoded string whose decoded value is an object.',
|
|
90
92
|
'Use only tools listed below. Do not claim a tool executed; only request it.',
|
|
91
93
|
'When the task is complete, return toolCalls:[] and finishReason:"stop".',
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildModelTurnPrompt(modelTurn = {}) {
|
|
98
|
+
const messages = compactMessages(modelTurn.messages, 64);
|
|
99
|
+
const tools = compactToolCatalog(Array.isArray(modelTurn.tools) ? modelTurn.tools : []);
|
|
100
|
+
return [
|
|
101
|
+
...modelTurnInstructions(),
|
|
94
102
|
'',
|
|
95
103
|
`Tools:\n${JSON.stringify(tools)}`,
|
|
104
|
+
'',
|
|
105
|
+
`Messages:\n${JSON.stringify(messages)}`,
|
|
96
106
|
].join('\n');
|
|
97
107
|
}
|
|
98
108
|
|
|
109
|
+
export function buildModelTurnDeltaPrompt(modelTurn = {}) {
|
|
110
|
+
const messages = compactMessages(modelTurn.messages, 24);
|
|
111
|
+
const includeTools = modelTurn?.session?.toolCatalogChanged === true;
|
|
112
|
+
const tools = includeTools
|
|
113
|
+
? compactToolCatalog(Array.isArray(modelTurn.tools) ? modelTurn.tools : [])
|
|
114
|
+
: [];
|
|
115
|
+
return [
|
|
116
|
+
'Continue the existing Dexter model session. The server has already supplied the doctrine, goal, prior messages, and tool catalog.',
|
|
117
|
+
'Apply only the new canonical messages/state changes below.',
|
|
118
|
+
'Return exactly one JSON object using the previously established response contract.',
|
|
119
|
+
...(includeTools ? ['', `Updated tools:\n${JSON.stringify(tools)}`] : []),
|
|
120
|
+
'',
|
|
121
|
+
`New messages:\n${JSON.stringify(messages)}`,
|
|
122
|
+
modelTurn?.session?.lastStateDigest
|
|
123
|
+
? `State digest: ${modelTurn.session.lastStateDigest}`
|
|
124
|
+
: '',
|
|
125
|
+
].filter(Boolean).join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function buildModelTurnFallbackPrompt(modelTurn = {}) {
|
|
129
|
+
return buildModelTurnPrompt({
|
|
130
|
+
...modelTurn,
|
|
131
|
+
messages: Array.isArray(modelTurn.fallbackMessages)
|
|
132
|
+
? modelTurn.fallbackMessages
|
|
133
|
+
: modelTurn.messages,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
99
137
|
export function modelTurnOutputSchema(modelTurn = {}) {
|
|
100
138
|
const tools = compactToolCatalog(Array.isArray(modelTurn.tools) ? modelTurn.tools : []);
|
|
101
139
|
const toolNames = tools.map((tool) => tool.name);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createJsonRpcClient } from './jsonRpcClient.js';
|
|
2
2
|
import { normalizeCompanionTokenUsage } from '../agentOutput.js';
|
|
3
|
+
import { BRIDGE_VERSION } from '../config.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Codex App Server adapter — the primary local-agent path.
|
|
@@ -20,7 +21,7 @@ import { normalizeCompanionTokenUsage } from '../agentOutput.js';
|
|
|
20
21
|
const CLIENT_INFO = {
|
|
21
22
|
name: 'dexter_bridge',
|
|
22
23
|
title: 'Dexter Bridge',
|
|
23
|
-
version:
|
|
24
|
+
version: BRIDGE_VERSION,
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export const CODEX_APP_SERVER_METHODS = {
|
|
@@ -382,10 +383,12 @@ export function createCodexAppServerAdapter({
|
|
|
382
383
|
if (event.method === CODEX_APP_SERVER_NOTIFICATIONS.turnCompleted) {
|
|
383
384
|
const turn = event.params?.turn || {};
|
|
384
385
|
if (turn.status === 'failed') {
|
|
385
|
-
|
|
386
|
+
const error = new Error(codexTurnErrorMessage(
|
|
386
387
|
turn,
|
|
387
388
|
'Codex turn failed without an error message.',
|
|
388
|
-
))
|
|
389
|
+
));
|
|
390
|
+
error.companionUsage = normalizeUsage(turn);
|
|
391
|
+
finish(reject, error);
|
|
389
392
|
return;
|
|
390
393
|
}
|
|
391
394
|
finish(resolve, {
|
|
@@ -424,6 +427,18 @@ export function createCodexAppServerAdapter({
|
|
|
424
427
|
}
|
|
425
428
|
}
|
|
426
429
|
|
|
430
|
+
async function resetSession(sessionId) {
|
|
431
|
+
const threadId = threadsByRun.get(sessionId);
|
|
432
|
+
if (!threadId) return;
|
|
433
|
+
threadsByRun.delete(sessionId);
|
|
434
|
+
if (!client || client.closed) return;
|
|
435
|
+
await client.request(
|
|
436
|
+
CODEX_APP_SERVER_METHODS.threadUnsubscribe,
|
|
437
|
+
{ threadId },
|
|
438
|
+
{ timeoutMs: 10000 },
|
|
439
|
+
).catch(() => undefined);
|
|
440
|
+
}
|
|
441
|
+
|
|
427
442
|
async function logout() {
|
|
428
443
|
if (!client || client.closed) return;
|
|
429
444
|
try {
|
|
@@ -451,6 +466,7 @@ export function createCodexAppServerAdapter({
|
|
|
451
466
|
usage,
|
|
452
467
|
runModelTurn,
|
|
453
468
|
cancel,
|
|
469
|
+
resetSession,
|
|
454
470
|
logout,
|
|
455
471
|
close,
|
|
456
472
|
};
|