@gakim-digital/dexter-bridge 0.5.0 → 0.5.3
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/README.md +19 -3
- package/package.json +2 -2
- package/src/agent.js +387 -26
- package/src/agentOutput.js +136 -18
- package/src/config.js +3 -1
- package/src/protocol.js +44 -6
- package/src/providers/codexAppServer.js +19 -3
package/README.md
CHANGED
|
@@ -75,14 +75,30 @@ The CLI stores the device token in `~/.dexter-bridge/config.json` with mode
|
|
|
75
75
|
credits are not charged for companion runs; the backend records reported usage
|
|
76
76
|
for visibility.
|
|
77
77
|
|
|
78
|
-
Claude Code
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
Claude Code is launched as a model-only engine: local tools, slash commands,
|
|
79
|
+
MCP integrations, browser access, and project-agent context are disabled. The
|
|
80
|
+
bridge supplies a strict JSON schema and a system prompt that requires Dexter
|
|
81
|
+
tool requests to be returned as structured output instead of being executed
|
|
82
|
+
inside Claude Code.
|
|
83
|
+
|
|
84
|
+
Claude Code runs use streaming JSON and Codex runs use JSONL so the bridge can
|
|
85
|
+
report input, output, cache, and reasoning tokens even when a turn is
|
|
86
|
+
interrupted. Claude's reported `total_cost_usd` is displayed as an estimated
|
|
81
87
|
API-equivalent cost, not an amount Dexter charged. If an older or customized
|
|
82
88
|
CLI does not support structured output, set `DEXTER_BRIDGE_STRUCTURED_USAGE=false`;
|
|
83
89
|
the run will continue, but its token usage will be marked unavailable rather
|
|
84
90
|
than estimated from text.
|
|
85
91
|
|
|
92
|
+
`DEXTER_BRIDGE_AGENT_TIMEOUT_MS` is the inactivity limit and resets whenever
|
|
93
|
+
the local model produces output. `DEXTER_BRIDGE_AGENT_MAX_DURATION_MS` is the
|
|
94
|
+
hard wall-clock ceiling; the API may supply a smaller per-turn ceiling from the
|
|
95
|
+
remaining run budget. Claude defaults to low effort for the intent-planner lane
|
|
96
|
+
and follow-up/recovery turns, and medium effort for the opening design turn.
|
|
97
|
+
Override these with `DEXTER_BRIDGE_CLAUDE_PLANNER_EFFORT`,
|
|
98
|
+
`DEXTER_BRIDGE_CLAUDE_MAIN_EFFORT`,
|
|
99
|
+
`DEXTER_BRIDGE_CLAUDE_FOLLOWUP_EFFORT`, or
|
|
100
|
+
`DEXTER_BRIDGE_CLAUDE_EFFORT`.
|
|
101
|
+
|
|
86
102
|
## Debugging
|
|
87
103
|
|
|
88
104
|
The Dexter Bridge terminal is the shell where `dexter-bridge start` is running.
|
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.3",
|
|
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',
|
|
@@ -133,6 +178,16 @@ const CLAUDE_CLI_MODEL_IDS = {
|
|
|
133
178
|
haiku: 'claude-haiku-4-5-20251001',
|
|
134
179
|
};
|
|
135
180
|
|
|
181
|
+
const CLAUDE_MODEL_ENGINE_SYSTEM_PROMPT = [
|
|
182
|
+
'You are a model-only completion engine embedded inside Dexter.',
|
|
183
|
+
'The user prompt contains Dexter messages and a catalog of remote tools as data.',
|
|
184
|
+
'Never execute, simulate, or emit native Claude Code tool calls for those tool names.',
|
|
185
|
+
'The only allowed tool is StructuredOutput, supplied by the JSON schema.',
|
|
186
|
+
'Encode requested Dexter actions only inside StructuredOutput.toolCalls.',
|
|
187
|
+
'Do not inspect the filesystem, project, shell, plugins, skills, MCP servers, or browser.',
|
|
188
|
+
'Be concise: reason only as much as needed to choose the next remote Dexter action.',
|
|
189
|
+
].join(' ');
|
|
190
|
+
|
|
136
191
|
export function mapClaudeCliModelId(value) {
|
|
137
192
|
const raw = String(value || '').trim();
|
|
138
193
|
return CLAUDE_CLI_MODEL_IDS[raw.toLowerCase()] || raw;
|
|
@@ -150,7 +205,16 @@ function argsWithSelectedModel(args, modelDefinition, definition) {
|
|
|
150
205
|
function argsWithStructuredOutput(args, definition, env = process.env) {
|
|
151
206
|
if (!structuredUsageEnabled(env)) return args;
|
|
152
207
|
if (definition.id === 'claude-code') {
|
|
153
|
-
|
|
208
|
+
const streamed = [
|
|
209
|
+
...argsWithoutFlagValue(args, '--output-format'),
|
|
210
|
+
'--output-format',
|
|
211
|
+
'stream-json',
|
|
212
|
+
];
|
|
213
|
+
if (!argsIncludeFlag(streamed, '--include-partial-messages')) {
|
|
214
|
+
streamed.push('--include-partial-messages');
|
|
215
|
+
}
|
|
216
|
+
if (!argsIncludeFlag(streamed, '--verbose')) streamed.push('--verbose');
|
|
217
|
+
return streamed;
|
|
154
218
|
}
|
|
155
219
|
if (definition.id === 'codex') {
|
|
156
220
|
return argsIncludeFlag(args, '--json') ? args : [...args, '--json'];
|
|
@@ -158,6 +222,89 @@ function argsWithStructuredOutput(args, definition, env = process.env) {
|
|
|
158
222
|
return args;
|
|
159
223
|
}
|
|
160
224
|
|
|
225
|
+
function argsWithClaudeIsolation(args, definition) {
|
|
226
|
+
if (definition.id !== 'claude-code') return args;
|
|
227
|
+
const isolated = argsWithoutVariadicFlag(args, '--tools');
|
|
228
|
+
isolated.push('--tools', '');
|
|
229
|
+
if (!argsIncludeFlag(isolated, '--disable-slash-commands')) isolated.push('--disable-slash-commands');
|
|
230
|
+
if (!argsIncludeFlag(isolated, '--safe-mode')) isolated.push('--safe-mode');
|
|
231
|
+
if (!argsIncludeFlag(isolated, '--strict-mcp-config')) isolated.push('--strict-mcp-config');
|
|
232
|
+
if (!argsIncludeFlag(isolated, '--no-chrome')) isolated.push('--no-chrome');
|
|
233
|
+
return isolated;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeClaudeEffort(value, fallback = 'medium') {
|
|
237
|
+
const effort = String(value || '').trim().toLowerCase();
|
|
238
|
+
return ['low', 'medium', 'high', 'xhigh', 'max'].includes(effort)
|
|
239
|
+
? effort
|
|
240
|
+
: fallback;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function claudeEffortForStep(step, env = process.env) {
|
|
244
|
+
if (env.DEXTER_BRIDGE_CLAUDE_EFFORT) {
|
|
245
|
+
return normalizeClaudeEffort(env.DEXTER_BRIDGE_CLAUDE_EFFORT);
|
|
246
|
+
}
|
|
247
|
+
if (Number(step) < 0) {
|
|
248
|
+
return normalizeClaudeEffort(env.DEXTER_BRIDGE_CLAUDE_PLANNER_EFFORT, 'low');
|
|
249
|
+
}
|
|
250
|
+
if (Number(step) > 0) {
|
|
251
|
+
return normalizeClaudeEffort(env.DEXTER_BRIDGE_CLAUDE_FOLLOWUP_EFFORT, 'low');
|
|
252
|
+
}
|
|
253
|
+
return normalizeClaudeEffort(env.DEXTER_BRIDGE_CLAUDE_MAIN_EFFORT, 'medium');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function argsWithClaudeModelEngine(args, definition, options = {}, env = process.env) {
|
|
257
|
+
if (definition.id !== 'claude-code') return args;
|
|
258
|
+
let isolated = argsWithoutFlagValue(args, '--system-prompt');
|
|
259
|
+
isolated = argsWithoutFlagValue(isolated, '--append-system-prompt');
|
|
260
|
+
isolated = argsWithoutFlagValue(isolated, '--agent');
|
|
261
|
+
isolated = argsWithoutFlagValue(isolated, '--effort');
|
|
262
|
+
return [
|
|
263
|
+
...isolated,
|
|
264
|
+
'--system-prompt',
|
|
265
|
+
CLAUDE_MODEL_ENGINE_SYSTEM_PROMPT,
|
|
266
|
+
'--effort',
|
|
267
|
+
claudeEffortForStep(options.step, env),
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function argsWithoutFlagValue(args, flag) {
|
|
272
|
+
const filtered = [];
|
|
273
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
274
|
+
const arg = args[index];
|
|
275
|
+
if (arg === flag) {
|
|
276
|
+
index += 1;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (arg.startsWith(`${flag}=`)) continue;
|
|
280
|
+
filtered.push(arg);
|
|
281
|
+
}
|
|
282
|
+
return filtered;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function argsWithoutVariadicFlag(args, flag) {
|
|
286
|
+
const filtered = [];
|
|
287
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
288
|
+
const arg = args[index];
|
|
289
|
+
if (arg === flag) {
|
|
290
|
+
while (index + 1 < args.length && !String(args[index + 1]).startsWith('-')) index += 1;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (arg.startsWith(`${flag}=`)) continue;
|
|
294
|
+
filtered.push(arg);
|
|
295
|
+
}
|
|
296
|
+
return filtered;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function argsWithOutputSchema(args, definition, outputSchema) {
|
|
300
|
+
if (definition.id !== 'claude-code' || !outputSchema) return args;
|
|
301
|
+
return [
|
|
302
|
+
...argsWithoutFlagValue(args, '--json-schema'),
|
|
303
|
+
'--json-schema',
|
|
304
|
+
JSON.stringify(outputSchema),
|
|
305
|
+
];
|
|
306
|
+
}
|
|
307
|
+
|
|
161
308
|
function argsWithPromptInput(args, definition) {
|
|
162
309
|
if (definition.id !== 'codex' || args.includes('-')) return args;
|
|
163
310
|
return [...args, '-'];
|
|
@@ -222,7 +369,10 @@ export function buildAgentArgs(definition, modelDefinition, env = process.env, o
|
|
|
222
369
|
const requiredArgs = argsWithRequiredAgentFlags(baseArgs, definition);
|
|
223
370
|
const modelArgs = argsWithSelectedModel(requiredArgs, modelDefinition, definition);
|
|
224
371
|
const structuredArgs = argsWithStructuredOutput(modelArgs, definition, env);
|
|
225
|
-
const
|
|
372
|
+
const isolatedArgs = argsWithClaudeIsolation(structuredArgs, definition);
|
|
373
|
+
const modelEngineArgs = argsWithClaudeModelEngine(isolatedArgs, definition, options, env);
|
|
374
|
+
const schemaArgs = argsWithOutputSchema(modelEngineArgs, definition, options.outputSchema);
|
|
375
|
+
const resumedArgs = argsWithResumedSession(schemaArgs, definition, options.resumeSessionId);
|
|
226
376
|
return argsWithPromptInput(resumedArgs, definition);
|
|
227
377
|
}
|
|
228
378
|
|
|
@@ -498,12 +648,17 @@ export function agentFailureMessage(command, code, stdout = '', stderr = '') {
|
|
|
498
648
|
|
|
499
649
|
function runProcess(command, args, stdin, {
|
|
500
650
|
timeoutMs = 120000,
|
|
651
|
+
maxDurationMs,
|
|
501
652
|
trace,
|
|
502
653
|
childEnv: providedChildEnv,
|
|
503
654
|
platform = process.platform,
|
|
504
655
|
} = {}) {
|
|
505
656
|
return new Promise((resolve, reject) => {
|
|
506
657
|
const started = Date.now();
|
|
658
|
+
// timeoutMs is an inactivity timeout (reset whenever the child produces
|
|
659
|
+
// output) so slow-but-streaming model turns are not killed mid-generation;
|
|
660
|
+
// hardDeadlineMs bounds total wall-clock time regardless of activity.
|
|
661
|
+
const hardDeadlineMs = Math.max(maxDurationMs || timeoutMs * 5, 10);
|
|
507
662
|
const childEnv = providedChildEnv || processEnvWithCliPath(platform);
|
|
508
663
|
const cwd = resolveAgentCwd(childEnv, process.cwd(), platform);
|
|
509
664
|
const invocation = processInvocation(command, args, childEnv, platform);
|
|
@@ -515,6 +670,7 @@ function runProcess(command, args, stdin, {
|
|
|
515
670
|
env: processEnvSummary(childEnv),
|
|
516
671
|
stdinChars: stdin.length,
|
|
517
672
|
timeoutMs,
|
|
673
|
+
maxDurationMs: hardDeadlineMs,
|
|
518
674
|
});
|
|
519
675
|
const child = spawn(invocation.command, invocation.args, {
|
|
520
676
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -525,33 +681,54 @@ function runProcess(command, args, stdin, {
|
|
|
525
681
|
let stdout = '';
|
|
526
682
|
let stderr = '';
|
|
527
683
|
let settled = false;
|
|
528
|
-
|
|
684
|
+
let inactivityTimer = null;
|
|
685
|
+
const timeOut = (message) => {
|
|
529
686
|
if (settled) return;
|
|
530
687
|
settled = true;
|
|
688
|
+
clearTimeout(inactivityTimer);
|
|
689
|
+
clearTimeout(deadlineTimer);
|
|
531
690
|
child.kill('SIGTERM');
|
|
532
|
-
const error = new Error(
|
|
691
|
+
const error = new Error(message);
|
|
692
|
+
error.code = 'AGENT_TIMEOUT';
|
|
693
|
+
error.stdout = stdout;
|
|
694
|
+
error.stderr = stderr;
|
|
695
|
+
error.timedOut = true;
|
|
533
696
|
trace?.error('agent_process_timeout', {
|
|
534
697
|
command,
|
|
535
698
|
durationMs: Date.now() - started,
|
|
536
699
|
timeoutMs,
|
|
700
|
+
maxDurationMs: hardDeadlineMs,
|
|
537
701
|
stdoutChars: stdout.length,
|
|
538
702
|
stderrChars: stderr.length,
|
|
539
703
|
stdoutExcerpt: clip(stdout, 1000),
|
|
540
704
|
stderrExcerpt: clip(stderr, 1000),
|
|
541
705
|
});
|
|
542
706
|
reject(error);
|
|
543
|
-
}
|
|
707
|
+
};
|
|
708
|
+
const armInactivityTimer = () => {
|
|
709
|
+
clearTimeout(inactivityTimer);
|
|
710
|
+
inactivityTimer = setTimeout(() => {
|
|
711
|
+
timeOut(`${command} produced no output for ${timeoutMs}ms.`);
|
|
712
|
+
}, timeoutMs);
|
|
713
|
+
};
|
|
714
|
+
const deadlineTimer = setTimeout(() => {
|
|
715
|
+
timeOut(`${command} timed out after ${hardDeadlineMs}ms.`);
|
|
716
|
+
}, hardDeadlineMs);
|
|
717
|
+
armInactivityTimer();
|
|
544
718
|
|
|
545
719
|
child.stdout.on('data', (chunk) => {
|
|
546
720
|
stdout += chunk.toString('utf8');
|
|
721
|
+
armInactivityTimer();
|
|
547
722
|
});
|
|
548
723
|
child.stderr.on('data', (chunk) => {
|
|
549
724
|
stderr += chunk.toString('utf8');
|
|
725
|
+
armInactivityTimer();
|
|
550
726
|
});
|
|
551
727
|
child.on('error', (error) => {
|
|
552
728
|
if (settled) return;
|
|
553
729
|
settled = true;
|
|
554
|
-
clearTimeout(
|
|
730
|
+
clearTimeout(inactivityTimer);
|
|
731
|
+
clearTimeout(deadlineTimer);
|
|
555
732
|
trace?.error('agent_process_error', {
|
|
556
733
|
command,
|
|
557
734
|
durationMs: Date.now() - started,
|
|
@@ -562,7 +739,8 @@ function runProcess(command, args, stdin, {
|
|
|
562
739
|
child.on('close', (code) => {
|
|
563
740
|
if (settled) return;
|
|
564
741
|
settled = true;
|
|
565
|
-
clearTimeout(
|
|
742
|
+
clearTimeout(inactivityTimer);
|
|
743
|
+
clearTimeout(deadlineTimer);
|
|
566
744
|
const meta = {
|
|
567
745
|
command,
|
|
568
746
|
code,
|
|
@@ -738,8 +916,15 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
738
916
|
const modelDefinition = companionModelDefinition(options.model, definition.id);
|
|
739
917
|
const args = buildAgentArgs(definition, modelDefinition, process.env, {
|
|
740
918
|
resumeSessionId: options.resumeSessionId,
|
|
919
|
+
outputSchema: options.outputSchema,
|
|
920
|
+
step: options.step,
|
|
741
921
|
});
|
|
742
922
|
const timeoutMs = options.timeoutMs || Number(process.env.DEXTER_BRIDGE_AGENT_TIMEOUT_MS || 120000);
|
|
923
|
+
const maxDurationMs = boundedDurationMs(
|
|
924
|
+
options.maxDurationMs ?? process.env.DEXTER_BRIDGE_AGENT_MAX_DURATION_MS,
|
|
925
|
+
Math.max(timeoutMs * 5, 600000),
|
|
926
|
+
10,
|
|
927
|
+
);
|
|
743
928
|
options.trace?.info('agent_step_invoke', {
|
|
744
929
|
agent: definition.id,
|
|
745
930
|
command,
|
|
@@ -751,13 +936,28 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
751
936
|
promptChars: prompt.length,
|
|
752
937
|
resumeSessionId: options.resumeSessionId,
|
|
753
938
|
timeoutMs,
|
|
939
|
+
maxDurationMs,
|
|
754
940
|
});
|
|
755
941
|
return runProcess(command, args, prompt, {
|
|
756
942
|
timeoutMs,
|
|
943
|
+
maxDurationMs,
|
|
757
944
|
trace: options.trace,
|
|
758
945
|
});
|
|
759
946
|
}
|
|
760
947
|
|
|
948
|
+
function agentErrorWithOutputUsage(agent, error) {
|
|
949
|
+
const failure = error instanceof Error
|
|
950
|
+
? error
|
|
951
|
+
: new Error(String(error || 'The companion model call failed.'));
|
|
952
|
+
const parsed = parseAgentOutput(agent, failure.stdout || '');
|
|
953
|
+
if (!parsed.usageAvailable) return failure;
|
|
954
|
+
const accumulator = createCompanionUsageAccumulator(agent);
|
|
955
|
+
accumulator.add(failure.companionUsage || {});
|
|
956
|
+
accumulator.add(parsed);
|
|
957
|
+
failure.companionUsage = accumulator.snapshot();
|
|
958
|
+
return failure;
|
|
959
|
+
}
|
|
960
|
+
|
|
761
961
|
async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
762
962
|
if (normalizeAgentName(agent) === 'dry-run') {
|
|
763
963
|
await send('done', {
|
|
@@ -781,7 +981,20 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
781
981
|
? requestedModel.trim()
|
|
782
982
|
: selectedModel?.id || definition.model;
|
|
783
983
|
const adapter = options.providerAdapter || null;
|
|
784
|
-
const
|
|
984
|
+
const sessionKey = companionSessionKey(run);
|
|
985
|
+
const rememberedSession = rememberedCompanionSession(sessionKey);
|
|
986
|
+
const deltaRequested =
|
|
987
|
+
deltaModelSessionsEnabled(options.env)
|
|
988
|
+
&& run?.modelTurn?.session?.contextMode === 'delta';
|
|
989
|
+
let callContextMode = deltaRequested && rememberedSession ? 'delta' : 'full';
|
|
990
|
+
let prompt = callContextMode === 'delta'
|
|
991
|
+
? buildModelTurnDeltaPrompt(run.modelTurn)
|
|
992
|
+
: deltaRequested
|
|
993
|
+
? buildModelTurnFallbackPrompt(run.modelTurn)
|
|
994
|
+
: buildModelTurnPrompt(run.modelTurn);
|
|
995
|
+
const callStartedAt = Date.now();
|
|
996
|
+
let providerSessionId;
|
|
997
|
+
let fallbackAfterResumeFailure = false;
|
|
785
998
|
const statusResponse = await send('status', {
|
|
786
999
|
stage: 'model_turn',
|
|
787
1000
|
message: `${definition.label} is generating the next Dexter action.`,
|
|
@@ -791,10 +1004,39 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
791
1004
|
throw new CompanionRunCancelledError();
|
|
792
1005
|
}
|
|
793
1006
|
const usageAccumulator = createCompanionUsageAccumulator(definition.id);
|
|
1007
|
+
const failedModelCall = (error) => {
|
|
1008
|
+
const failure = error instanceof Error
|
|
1009
|
+
? error
|
|
1010
|
+
: new Error(String(error || 'The companion model call failed.'));
|
|
1011
|
+
usageAccumulator.add(failure.companionUsage || {});
|
|
1012
|
+
const snapshot = usageAccumulator.snapshot();
|
|
1013
|
+
failure.companionUsage = {
|
|
1014
|
+
...snapshot,
|
|
1015
|
+
modelCalls: [{
|
|
1016
|
+
callId: run.runId,
|
|
1017
|
+
step: Number(run?.modelTurn?.step ?? 0),
|
|
1018
|
+
model: selectedModelId,
|
|
1019
|
+
status: 'failed',
|
|
1020
|
+
durationMs: Date.now() - callStartedAt,
|
|
1021
|
+
promptChars: prompt.length,
|
|
1022
|
+
requestedContextMode: deltaRequested ? 'delta' : 'full',
|
|
1023
|
+
contextMode: callContextMode,
|
|
1024
|
+
resumed: callContextMode === 'delta',
|
|
1025
|
+
resumeSessionAvailable: Boolean(rememberedSession),
|
|
1026
|
+
fallbackAfterResumeFailure,
|
|
1027
|
+
...snapshot.tokenUsage,
|
|
1028
|
+
estimatedCostUSD: snapshot.estimatedCostUSD,
|
|
1029
|
+
}],
|
|
1030
|
+
};
|
|
1031
|
+
return failure;
|
|
1032
|
+
};
|
|
794
1033
|
let resultText;
|
|
795
1034
|
if (adapter) {
|
|
796
|
-
const sessionId = adapterSessionId(run);
|
|
1035
|
+
const sessionId = sessionKey || adapterSessionId(run);
|
|
797
1036
|
const invocationModel = adapterInvocationModel(run, selectedModel, definition.id);
|
|
1037
|
+
if (callContextMode === 'full' && rememberedSession) {
|
|
1038
|
+
await adapter.resetSession?.(sessionId);
|
|
1039
|
+
}
|
|
798
1040
|
options.trace?.info('agent_adapter_invoke', {
|
|
799
1041
|
adapter: adapter.id,
|
|
800
1042
|
agent: definition.id,
|
|
@@ -803,17 +1045,26 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
803
1045
|
invocationModel,
|
|
804
1046
|
promptChars: prompt.length,
|
|
805
1047
|
});
|
|
806
|
-
const
|
|
1048
|
+
const adapterInput = {
|
|
807
1049
|
runId: run.runId,
|
|
808
1050
|
sessionId,
|
|
809
1051
|
prompt,
|
|
810
1052
|
model: invocationModel,
|
|
1053
|
+
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
811
1054
|
timeoutMs: boundedDurationMs(
|
|
812
1055
|
options.timeoutMs ?? options.env?.DEXTER_BRIDGE_AGENT_TIMEOUT_MS,
|
|
813
1056
|
120000,
|
|
814
1057
|
1000,
|
|
815
1058
|
),
|
|
816
|
-
|
|
1059
|
+
maxDurationMs: boundedDurationMs(
|
|
1060
|
+
run?.modelTurn?.maxDurationMs
|
|
1061
|
+
?? options.maxDurationMs
|
|
1062
|
+
?? options.env?.DEXTER_BRIDGE_AGENT_MAX_DURATION_MS,
|
|
1063
|
+
600000,
|
|
1064
|
+
1000,
|
|
1065
|
+
),
|
|
1066
|
+
};
|
|
1067
|
+
const adapterOptions = {
|
|
817
1068
|
send,
|
|
818
1069
|
trace: options.trace,
|
|
819
1070
|
controlPollMs: boundedDurationMs(
|
|
@@ -822,9 +1073,30 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
822
1073
|
10,
|
|
823
1074
|
30000,
|
|
824
1075
|
),
|
|
825
|
-
}
|
|
1076
|
+
};
|
|
1077
|
+
let result;
|
|
1078
|
+
try {
|
|
1079
|
+
result = await callProviderAdapter(adapter, adapterInput, adapterOptions);
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
if (callContextMode !== 'delta' || !resumeFailure(error)) throw failedModelCall(error);
|
|
1082
|
+
usageAccumulator.add(error?.companionUsage || {});
|
|
1083
|
+
fallbackAfterResumeFailure = true;
|
|
1084
|
+
callContextMode = 'full';
|
|
1085
|
+
prompt = buildModelTurnFallbackPrompt(run.modelTurn);
|
|
1086
|
+
await adapter.resetSession?.(sessionId);
|
|
1087
|
+
options.trace?.warn('agent_adapter_resume_fallback', {
|
|
1088
|
+
sessionId,
|
|
1089
|
+
error: errorMeta(error),
|
|
1090
|
+
fallbackPromptChars: prompt.length,
|
|
1091
|
+
});
|
|
1092
|
+
result = await callProviderAdapter(adapter, { ...adapterInput, prompt }, adapterOptions)
|
|
1093
|
+
.catch((error) => {
|
|
1094
|
+
throw failedModelCall(error);
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
826
1097
|
usageAccumulator.add(result);
|
|
827
1098
|
resultText = result?.text;
|
|
1099
|
+
providerSessionId = result?.threadId || result?.sessionId;
|
|
828
1100
|
} else {
|
|
829
1101
|
const runtime = await resolveAgentRuntime(definition, selectedModel, {
|
|
830
1102
|
env: options.env,
|
|
@@ -832,25 +1104,96 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
832
1104
|
if (!runtime.ok) {
|
|
833
1105
|
throw new Error(runtime.error || `${definition.label} is not available.`);
|
|
834
1106
|
}
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1107
|
+
const resumeSessionId =
|
|
1108
|
+
callContextMode === 'delta'
|
|
1109
|
+
&& agentSessionResumeEnabled(definition, options.env)
|
|
1110
|
+
? rememberedSession?.providerSessionId
|
|
1111
|
+
: undefined;
|
|
1112
|
+
let result;
|
|
1113
|
+
try {
|
|
1114
|
+
result = await callLocalJsonAgent(definition.id, prompt, {
|
|
1115
|
+
...options,
|
|
1116
|
+
model: selectedModelId,
|
|
1117
|
+
runtime,
|
|
1118
|
+
resumeSessionId,
|
|
1119
|
+
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
1120
|
+
step: run?.modelTurn?.step,
|
|
1121
|
+
maxDurationMs: run?.modelTurn?.maxDurationMs ?? options.maxDurationMs,
|
|
1122
|
+
});
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
const failure = agentErrorWithOutputUsage(definition.id, error);
|
|
1125
|
+
if (callContextMode !== 'delta' || !resumeFailure(failure)) throw failedModelCall(failure);
|
|
1126
|
+
usageAccumulator.add(failure.companionUsage || {});
|
|
1127
|
+
fallbackAfterResumeFailure = true;
|
|
1128
|
+
callContextMode = 'full';
|
|
1129
|
+
prompt = buildModelTurnFallbackPrompt(run.modelTurn);
|
|
1130
|
+
options.trace?.warn('agent_cli_resume_fallback', {
|
|
1131
|
+
resumeSessionId,
|
|
1132
|
+
error: errorMeta(failure),
|
|
1133
|
+
fallbackPromptChars: prompt.length,
|
|
1134
|
+
});
|
|
1135
|
+
result = await callLocalJsonAgent(definition.id, prompt, {
|
|
1136
|
+
...options,
|
|
1137
|
+
model: selectedModelId,
|
|
1138
|
+
runtime,
|
|
1139
|
+
resumeSessionId: undefined,
|
|
1140
|
+
outputSchema: modelTurnOutputSchema(run.modelTurn),
|
|
1141
|
+
step: run?.modelTurn?.step,
|
|
1142
|
+
maxDurationMs: run?.modelTurn?.maxDurationMs ?? options.maxDurationMs,
|
|
1143
|
+
}).catch((error) => {
|
|
1144
|
+
throw failedModelCall(agentErrorWithOutputUsage(definition.id, error));
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
840
1147
|
const parsed = parseAgentOutput(definition.id, result.stdout);
|
|
841
1148
|
usageAccumulator.add(parsed);
|
|
842
1149
|
resultText = parsed.resultText;
|
|
1150
|
+
providerSessionId = parsed.sessionId;
|
|
1151
|
+
}
|
|
1152
|
+
if (providerSessionId || adapter) {
|
|
1153
|
+
rememberCompanionSession(sessionKey, {
|
|
1154
|
+
providerSessionId: providerSessionId || rememberedSession?.providerSessionId || sessionKey,
|
|
1155
|
+
agent: definition.id,
|
|
1156
|
+
updatedAt: Date.now(),
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
const usageSnapshot = usageAccumulator.snapshot();
|
|
1160
|
+
const callTelemetry = {
|
|
1161
|
+
callId: run.runId,
|
|
1162
|
+
step: Number(run?.modelTurn?.step ?? 0),
|
|
1163
|
+
model: selectedModelId,
|
|
1164
|
+
durationMs: Date.now() - callStartedAt,
|
|
1165
|
+
promptChars: prompt.length,
|
|
1166
|
+
requestedContextMode: deltaRequested ? 'delta' : 'full',
|
|
1167
|
+
contextMode: callContextMode,
|
|
1168
|
+
resumed: callContextMode === 'delta',
|
|
1169
|
+
resumeSessionAvailable: Boolean(rememberedSession),
|
|
1170
|
+
fallbackAfterResumeFailure,
|
|
1171
|
+
...usageSnapshot.tokenUsage,
|
|
1172
|
+
estimatedCostUSD: usageSnapshot.estimatedCostUSD,
|
|
1173
|
+
};
|
|
1174
|
+
const usage = {
|
|
1175
|
+
...usageSnapshot,
|
|
1176
|
+
modelCalls: [{ ...callTelemetry, status: 'succeeded' }],
|
|
1177
|
+
};
|
|
1178
|
+
let completion;
|
|
1179
|
+
try {
|
|
1180
|
+
completion = normalizeModelTurnCompletion(
|
|
1181
|
+
extractJsonObject(resultText),
|
|
1182
|
+
selectedModelId,
|
|
1183
|
+
);
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
error.companionUsage = {
|
|
1186
|
+
...usage,
|
|
1187
|
+
modelCalls: [{ ...callTelemetry, status: 'rejected' }],
|
|
1188
|
+
};
|
|
1189
|
+
throw error;
|
|
843
1190
|
}
|
|
844
|
-
const completion = normalizeModelTurnCompletion(
|
|
845
|
-
extractJsonObject(resultText),
|
|
846
|
-
selectedModelId,
|
|
847
|
-
);
|
|
848
1191
|
await send('done', {
|
|
849
1192
|
operationType: 'chat',
|
|
850
1193
|
outcome: 'answer',
|
|
851
1194
|
completion,
|
|
852
1195
|
model: selectedModelId,
|
|
853
|
-
...
|
|
1196
|
+
...usage,
|
|
854
1197
|
});
|
|
855
1198
|
}
|
|
856
1199
|
|
|
@@ -885,14 +1228,26 @@ export async function executeRun(run, {
|
|
|
885
1228
|
const runModel = run?.companion?.model?.id || run?.model || selectedModel;
|
|
886
1229
|
const usageAccumulator = createCompanionUsageAccumulator(normalizedAgent);
|
|
887
1230
|
const adapterProvided = providerAdapter !== undefined;
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
1231
|
+
const shareAdapter = !adapterProvided && deltaModelSessionsEnabled(env);
|
|
1232
|
+
let activeAdapter = adapterProvided ? providerAdapter : null;
|
|
1233
|
+
if (!adapterProvided && shareAdapter) {
|
|
1234
|
+
activeAdapter = sharedProviderAdapters.get(normalizedAgent) || null;
|
|
1235
|
+
if (!activeAdapter) {
|
|
1236
|
+
activeAdapter = createLocalAgentAdapter(normalizedAgent, {
|
|
1237
|
+
...adapterOptions,
|
|
1238
|
+
env: adapterOptions?.env || env,
|
|
1239
|
+
trace,
|
|
1240
|
+
});
|
|
1241
|
+
if (activeAdapter) sharedProviderAdapters.set(normalizedAgent, activeAdapter);
|
|
1242
|
+
}
|
|
1243
|
+
} else if (!adapterProvided) {
|
|
1244
|
+
activeAdapter = createLocalAgentAdapter(normalizedAgent, {
|
|
891
1245
|
...adapterOptions,
|
|
892
1246
|
env: adapterOptions?.env || env,
|
|
893
1247
|
trace,
|
|
894
1248
|
});
|
|
895
|
-
|
|
1249
|
+
}
|
|
1250
|
+
const ownsAdapter = !adapterProvided && !shareAdapter && Boolean(activeAdapter);
|
|
896
1251
|
if (
|
|
897
1252
|
activeAdapter?.id
|
|
898
1253
|
&& activeAdapter.id !== normalizedAgent
|
|
@@ -945,6 +1300,12 @@ export async function executeRun(run, {
|
|
|
945
1300
|
}
|
|
946
1301
|
}
|
|
947
1302
|
|
|
1303
|
+
export function __resetAgentSessionsForTests() {
|
|
1304
|
+
companionModelSessions.clear();
|
|
1305
|
+
for (const adapter of sharedProviderAdapters.values()) adapter?.close?.();
|
|
1306
|
+
sharedProviderAdapters.clear();
|
|
1307
|
+
}
|
|
1308
|
+
|
|
948
1309
|
export function checkCommand(command, args = ['--version'], timeoutMs = 10000) {
|
|
949
1310
|
return runProcess(command, args, '', { timeoutMs })
|
|
950
1311
|
.then((result) => ({ ok: true, command, output: (result.stdout || result.stderr || '').trim() }))
|
package/src/agentOutput.js
CHANGED
|
@@ -79,11 +79,25 @@ function parseWholeJson(stdout) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
function parseJsonLines(stdout) {
|
|
83
|
+
return String(stdout || '')
|
|
84
|
+
.split(/\r?\n/)
|
|
85
|
+
.map((line) => line.trim())
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.flatMap((line) => {
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(line);
|
|
90
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? [parsed] : [];
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseClaudeEnvelope(parsed, fallbackText = '') {
|
|
84
98
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
85
99
|
return {
|
|
86
|
-
resultText: String(
|
|
100
|
+
resultText: String(fallbackText || '').trim(),
|
|
87
101
|
tokenUsage: normalizeCompanionTokenUsage(),
|
|
88
102
|
modelUsage: [],
|
|
89
103
|
usageAvailable: false,
|
|
@@ -92,10 +106,16 @@ export function parseClaudeOutput(stdout) {
|
|
|
92
106
|
};
|
|
93
107
|
}
|
|
94
108
|
|
|
95
|
-
const
|
|
109
|
+
const structuredOutput = parsed.structured_output ?? parsed.structuredOutput;
|
|
110
|
+
const hasEnvelope =
|
|
111
|
+
typeof parsed.result === 'string'
|
|
112
|
+
|| structuredOutput !== undefined
|
|
113
|
+
|| parsed.usage
|
|
114
|
+
|| parsed.total_cost_usd !== undefined
|
|
115
|
+
|| parsed.modelUsage;
|
|
96
116
|
if (!hasEnvelope) {
|
|
97
117
|
return {
|
|
98
|
-
resultText: String(
|
|
118
|
+
resultText: String(fallbackText || '').trim(),
|
|
99
119
|
tokenUsage: normalizeCompanionTokenUsage(),
|
|
100
120
|
modelUsage: [],
|
|
101
121
|
usageAvailable: false,
|
|
@@ -108,7 +128,13 @@ export function parseClaudeOutput(stdout) {
|
|
|
108
128
|
const modelUsage = normalizeModelUsage(parsed.modelUsage || parsed.model_usage);
|
|
109
129
|
const estimatedCostUSD = optionalNonNegativeNumber(parsed.total_cost_usd ?? parsed.totalCostUsd);
|
|
110
130
|
return {
|
|
111
|
-
resultText: typeof
|
|
131
|
+
resultText: structuredOutput && typeof structuredOutput === 'object'
|
|
132
|
+
? JSON.stringify(structuredOutput)
|
|
133
|
+
: typeof structuredOutput === 'string'
|
|
134
|
+
? structuredOutput.trim()
|
|
135
|
+
: typeof parsed.result === 'string'
|
|
136
|
+
? parsed.result.trim()
|
|
137
|
+
: '',
|
|
112
138
|
tokenUsage,
|
|
113
139
|
model: typeof parsed.model === 'string' ? parsed.model : undefined,
|
|
114
140
|
sessionId: typeof parsed.session_id === 'string'
|
|
@@ -124,19 +150,111 @@ export function parseClaudeOutput(stdout) {
|
|
|
124
150
|
};
|
|
125
151
|
}
|
|
126
152
|
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
function mergeMaximumUsage(target, rawUsage) {
|
|
154
|
+
const usage = normalizeCompanionTokenUsage(rawUsage || {});
|
|
155
|
+
for (const field of TOKEN_FIELDS) target[field] = Math.max(target[field], usage[field]);
|
|
156
|
+
target.totalTokens = target.inputTokens + target.outputTokens + target.reasoningOutputTokens;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseClaudeStreamUsage(events) {
|
|
160
|
+
const turns = new Map();
|
|
161
|
+
let activeTurnKey;
|
|
162
|
+
let anonymousSequence = 0;
|
|
163
|
+
let sessionId;
|
|
164
|
+
let partialText = '';
|
|
165
|
+
|
|
166
|
+
const ensureTurn = (key, model) => {
|
|
167
|
+
const resolvedKey = key || activeTurnKey || `turn-${++anonymousSequence}`;
|
|
168
|
+
if (!turns.has(resolvedKey)) {
|
|
169
|
+
turns.set(resolvedKey, {
|
|
170
|
+
model: typeof model === 'string' && model.trim() ? model.trim() : undefined,
|
|
171
|
+
usage: normalizeCompanionTokenUsage(),
|
|
172
|
+
});
|
|
173
|
+
} else if (model && !turns.get(resolvedKey).model) {
|
|
174
|
+
turns.get(resolvedKey).model = model;
|
|
175
|
+
}
|
|
176
|
+
activeTurnKey = resolvedKey;
|
|
177
|
+
return turns.get(resolvedKey);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
for (const row of events) {
|
|
181
|
+
if (
|
|
182
|
+
(row.type === 'system' || row.type === 'init')
|
|
183
|
+
&& typeof (row.session_id || row.sessionId) === 'string'
|
|
184
|
+
) {
|
|
185
|
+
sessionId = row.session_id || row.sessionId;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (row.type === 'assistant' && row.message && typeof row.message === 'object') {
|
|
189
|
+
const message = row.message;
|
|
190
|
+
const turn = ensureTurn(message.id, message.model);
|
|
191
|
+
mergeMaximumUsage(turn.usage, message.usage);
|
|
192
|
+
for (const part of Array.isArray(message.content) ? message.content : []) {
|
|
193
|
+
if (part?.type === 'text' && typeof part.text === 'string') partialText += part.text;
|
|
138
194
|
}
|
|
139
|
-
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (row.type !== 'stream_event' || !row.event || typeof row.event !== 'object') continue;
|
|
198
|
+
const event = row.event;
|
|
199
|
+
if (event.type === 'message_start' && event.message && typeof event.message === 'object') {
|
|
200
|
+
const turn = ensureTurn(event.message.id, event.message.model);
|
|
201
|
+
mergeMaximumUsage(turn.usage, event.message.usage);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (event.type === 'message_delta') {
|
|
205
|
+
const turn = ensureTurn(activeTurnKey);
|
|
206
|
+
mergeMaximumUsage(turn.usage, event.usage);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (
|
|
210
|
+
event.type === 'content_block_delta'
|
|
211
|
+
&& event.delta?.type === 'text_delta'
|
|
212
|
+
&& typeof event.delta.text === 'string'
|
|
213
|
+
) {
|
|
214
|
+
partialText += event.delta.text;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const tokenUsage = normalizeCompanionTokenUsage();
|
|
219
|
+
const modelTotals = new Map();
|
|
220
|
+
for (const turn of turns.values()) {
|
|
221
|
+
for (const field of TOKEN_FIELDS) tokenUsage[field] += turn.usage[field];
|
|
222
|
+
tokenUsage.totalTokens += turn.usage.totalTokens;
|
|
223
|
+
if (!turn.model) continue;
|
|
224
|
+
const current = modelTotals.get(turn.model) || {
|
|
225
|
+
model: turn.model,
|
|
226
|
+
...normalizeCompanionTokenUsage(),
|
|
227
|
+
};
|
|
228
|
+
for (const field of TOKEN_FIELDS) current[field] += turn.usage[field];
|
|
229
|
+
current.totalTokens += turn.usage.totalTokens;
|
|
230
|
+
modelTotals.set(turn.model, current);
|
|
231
|
+
}
|
|
232
|
+
const usageAvailable = usageHasReportedTokens(tokenUsage);
|
|
233
|
+
return {
|
|
234
|
+
resultText: partialText.trim(),
|
|
235
|
+
tokenUsage,
|
|
236
|
+
modelUsage: Array.from(modelTotals.values()),
|
|
237
|
+
sessionId,
|
|
238
|
+
usageAvailable,
|
|
239
|
+
usageSource: 'claude-code',
|
|
240
|
+
usageAccuracy: usageAvailable ? 'reported' : 'unavailable',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function parseClaudeOutput(stdout) {
|
|
245
|
+
const whole = parseWholeJson(stdout);
|
|
246
|
+
if (whole && typeof whole === 'object' && !Array.isArray(whole)) {
|
|
247
|
+
return parseClaudeEnvelope(whole, stdout);
|
|
248
|
+
}
|
|
249
|
+
const events = parseJsonLines(stdout);
|
|
250
|
+
const resultEnvelope = [...events].reverse().find((event) =>
|
|
251
|
+
event.type === 'result'
|
|
252
|
+
|| event.structured_output !== undefined
|
|
253
|
+
|| event.structuredOutput !== undefined
|
|
254
|
+
|| typeof event.result === 'string');
|
|
255
|
+
if (resultEnvelope) return parseClaudeEnvelope(resultEnvelope, stdout);
|
|
256
|
+
if (events.length) return parseClaudeStreamUsage(events);
|
|
257
|
+
return parseClaudeEnvelope(null, stdout);
|
|
140
258
|
}
|
|
141
259
|
|
|
142
260
|
export function parseCodexOutput(stdout) {
|
package/src/config.js
CHANGED
|
@@ -3,7 +3,9 @@ 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 =
|
|
6
|
+
export const BRIDGE_VERSION = JSON.parse(
|
|
7
|
+
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
8
|
+
).version;
|
|
7
9
|
export const BRIDGE_CAPABILITIES = [
|
|
8
10
|
'model-turn-v1',
|
|
9
11
|
];
|
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
|
};
|