@gakim-digital/dexter-bridge 0.5.6 → 0.5.7
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 +1 -1
- package/src/config.js +3 -0
- package/src/protocol.js +113 -51
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -13,8 +13,10 @@ export const BRIDGE_BUILD_FINGERPRINT = crypto
|
|
|
13
13
|
'../package.json',
|
|
14
14
|
'./agent.js',
|
|
15
15
|
'./agentOutput.js',
|
|
16
|
+
'./api.js',
|
|
16
17
|
'./cli.js',
|
|
17
18
|
'./config.js',
|
|
19
|
+
'./protocol.js',
|
|
18
20
|
].map((relativePath) => {
|
|
19
21
|
const url = new URL(relativePath, import.meta.url);
|
|
20
22
|
return `${relativePath}\u0000${fs.readFileSync(url, 'utf8')}`;
|
|
@@ -23,6 +25,7 @@ export const BRIDGE_BUILD_FINGERPRINT = crypto
|
|
|
23
25
|
export const BRIDGE_CAPABILITIES = [
|
|
24
26
|
'model-turn-v1',
|
|
25
27
|
'build-fingerprint-v1',
|
|
28
|
+
'tool-schema-parity-v1',
|
|
26
29
|
];
|
|
27
30
|
// Codex is the default local agent: driving Claude Code from a user's Claude.ai
|
|
28
31
|
// subscription needs prior written approval from Anthropic for commercial use, so
|
package/src/protocol.js
CHANGED
|
@@ -1,27 +1,73 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
1
3
|
function isRecord(value) {
|
|
2
4
|
return value && typeof value === 'object' && !Array.isArray(value);
|
|
3
5
|
}
|
|
4
6
|
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
function toolCatalogDigest(tools) {
|
|
8
|
+
return createHash('sha256')
|
|
9
|
+
.update(JSON.stringify(tools))
|
|
10
|
+
.digest('hex')
|
|
11
|
+
.slice(0, 24);
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
export function assertToolCatalogParity(modelTurn = {}) {
|
|
15
|
+
const tools = Array.isArray(modelTurn.tools) ? modelTurn.tools : [];
|
|
16
|
+
const expected = modelTurn?.session?.toolCatalogHash;
|
|
17
|
+
if (!expected) return toolCatalogDigest(tools);
|
|
18
|
+
const actual = toolCatalogDigest(tools);
|
|
19
|
+
if (actual !== expected) {
|
|
20
|
+
throw Object.assign(
|
|
21
|
+
new Error(
|
|
22
|
+
`Dexter tool schema parity failed: expected ${expected}, received ${actual}.`,
|
|
23
|
+
),
|
|
24
|
+
{
|
|
25
|
+
code: 'TOOL_SCHEMA_PARITY_ERROR',
|
|
26
|
+
expectedToolCatalogHash: expected,
|
|
27
|
+
actualToolCatalogHash: actual,
|
|
28
|
+
},
|
|
29
|
+
);
|
|
15
30
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
31
|
+
return actual;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toolParameters(tool) {
|
|
35
|
+
return isRecord(tool?.parameters)
|
|
36
|
+
? tool.parameters
|
|
37
|
+
: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {},
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function nestedToolParameters(schema, namespace, outputDefinitions) {
|
|
45
|
+
const source = JSON.parse(JSON.stringify(schema));
|
|
46
|
+
delete source.$schema;
|
|
47
|
+
const localDefinitions = {
|
|
48
|
+
...(isRecord(source.definitions) ? source.definitions : {}),
|
|
49
|
+
...(isRecord(source.$defs) ? source.$defs : {}),
|
|
50
|
+
};
|
|
51
|
+
delete source.definitions;
|
|
52
|
+
delete source.$defs;
|
|
53
|
+
const prefix = `${namespace}__`;
|
|
54
|
+
const rewrite = (value) => {
|
|
55
|
+
if (Array.isArray(value)) return value.map(rewrite);
|
|
56
|
+
if (!isRecord(value)) return value;
|
|
57
|
+
return Object.fromEntries(
|
|
58
|
+
Object.entries(value).map(([key, nested]) => {
|
|
59
|
+
if (key === '$ref' && typeof nested === 'string') {
|
|
60
|
+
const match = nested.match(/^#\/(?:definitions|\$defs)\/(.+)$/);
|
|
61
|
+
if (match) return [key, `#/$defs/${prefix}${match[1]}`];
|
|
62
|
+
}
|
|
63
|
+
return [key, rewrite(nested)];
|
|
64
|
+
}),
|
|
21
65
|
);
|
|
66
|
+
};
|
|
67
|
+
for (const [name, definition] of Object.entries(localDefinitions)) {
|
|
68
|
+
outputDefinitions[`${prefix}${name}`] = rewrite(definition);
|
|
22
69
|
}
|
|
23
|
-
|
|
24
|
-
return result;
|
|
70
|
+
return rewrite(source);
|
|
25
71
|
}
|
|
26
72
|
|
|
27
73
|
function compactModelTurnContent(content) {
|
|
@@ -65,8 +111,8 @@ export function compactToolCatalog(tools = []) {
|
|
|
65
111
|
.slice(0, 40)
|
|
66
112
|
.map((tool) => ({
|
|
67
113
|
name: typeof tool.name === 'string' ? tool.name : '',
|
|
68
|
-
description:
|
|
69
|
-
parameters:
|
|
114
|
+
description: typeof tool.description === 'string' ? tool.description : '',
|
|
115
|
+
parameters: toolParameters(tool),
|
|
70
116
|
}))
|
|
71
117
|
.filter((tool) => tool.name);
|
|
72
118
|
}
|
|
@@ -107,8 +153,8 @@ function modelTurnInstructions(modelTurn = {}) {
|
|
|
107
153
|
'You are the model engine for Dexter. The server owns the agent loop and executes all tools.',
|
|
108
154
|
'Return exactly one JSON object and no markdown.',
|
|
109
155
|
'Allowed response:',
|
|
110
|
-
'{"text":"optional assistant text","toolCalls":[{"id":"stable-id","name":"toolName","arguments":
|
|
111
|
-
'Each toolCalls[].arguments value must be
|
|
156
|
+
'{"text":"optional assistant text","toolCalls":[{"id":"stable-id","name":"toolName","arguments":{"key":"value"}}],"finishReason":"tool_calls|stop|length"}',
|
|
157
|
+
'Each toolCalls[].arguments value must be an object matching the exact selected tool schema.',
|
|
112
158
|
'Use only tools listed below. Do not claim a tool executed; only request it.',
|
|
113
159
|
toolChoice.required
|
|
114
160
|
? toolChoice.toolName
|
|
@@ -119,6 +165,7 @@ function modelTurnInstructions(modelTurn = {}) {
|
|
|
119
165
|
}
|
|
120
166
|
|
|
121
167
|
export function buildModelTurnPrompt(modelTurn = {}) {
|
|
168
|
+
assertToolCatalogParity(modelTurn);
|
|
122
169
|
const messages = compactMessages(modelTurn.messages, 64);
|
|
123
170
|
const tools = compactToolCatalog(Array.isArray(modelTurn.tools) ? modelTurn.tools : []);
|
|
124
171
|
return [
|
|
@@ -131,6 +178,7 @@ export function buildModelTurnPrompt(modelTurn = {}) {
|
|
|
131
178
|
}
|
|
132
179
|
|
|
133
180
|
export function buildModelTurnDeltaPrompt(modelTurn = {}) {
|
|
181
|
+
assertToolCatalogParity(modelTurn);
|
|
134
182
|
const messages = compactMessages(modelTurn.messages, 24);
|
|
135
183
|
const includeTools = modelTurn?.session?.toolCatalogChanged === true;
|
|
136
184
|
const tools = includeTools
|
|
@@ -167,37 +215,56 @@ export function buildModelTurnFallbackPrompt(modelTurn = {}) {
|
|
|
167
215
|
}
|
|
168
216
|
|
|
169
217
|
export function modelTurnOutputSchema(modelTurn = {}) {
|
|
218
|
+
assertToolCatalogParity(modelTurn);
|
|
170
219
|
const tools = compactToolCatalog(Array.isArray(modelTurn.tools) ? modelTurn.tools : []);
|
|
171
220
|
const toolChoice = normalizedToolChoice(modelTurn);
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
221
|
+
const selectedTools =
|
|
222
|
+
toolChoice.toolName
|
|
223
|
+
? tools.filter((tool) => tool.name === toolChoice.toolName)
|
|
224
|
+
: tools;
|
|
225
|
+
const definitions = {};
|
|
226
|
+
const toolCallVariants = selectedTools.map((tool, index) => ({
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
id: { type: 'string' },
|
|
230
|
+
name: { type: 'string', enum: [tool.name] },
|
|
231
|
+
arguments: nestedToolParameters(
|
|
232
|
+
tool.parameters,
|
|
233
|
+
`tool_${index}_${tool.name.replace(/[^A-Za-z0-9_]/g, '_')}`,
|
|
234
|
+
definitions,
|
|
235
|
+
),
|
|
236
|
+
},
|
|
237
|
+
required: ['id', 'name', 'arguments'],
|
|
238
|
+
additionalProperties: false,
|
|
239
|
+
}));
|
|
240
|
+
const emptyToolCall = {
|
|
241
|
+
type: 'object',
|
|
242
|
+
properties: {
|
|
243
|
+
id: { type: 'string' },
|
|
244
|
+
name: { type: 'string' },
|
|
245
|
+
arguments: {
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: {},
|
|
248
|
+
additionalProperties: false,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
required: ['id', 'name', 'arguments'],
|
|
252
|
+
additionalProperties: false,
|
|
253
|
+
};
|
|
254
|
+
const output = {
|
|
178
255
|
type: 'object',
|
|
179
256
|
properties: {
|
|
180
257
|
text: { type: 'string' },
|
|
181
258
|
toolCalls: {
|
|
182
259
|
type: 'array',
|
|
183
260
|
...(toolChoice.required ? { minItems: 1 } : {}),
|
|
184
|
-
maxItems:
|
|
185
|
-
items:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
: { type: 'string' },
|
|
192
|
-
// Structured Outputs requires every object schema to declare
|
|
193
|
-
// additionalProperties:false. Tool argument shapes differ per
|
|
194
|
-
// selected tool, so encode them at this boundary and validate the
|
|
195
|
-
// decoded object before returning the completion to Dexter.
|
|
196
|
-
arguments: { type: 'string' },
|
|
197
|
-
},
|
|
198
|
-
required: ['id', 'name', 'arguments'],
|
|
199
|
-
additionalProperties: false,
|
|
200
|
-
},
|
|
261
|
+
maxItems: selectedTools.length ? 12 : 0,
|
|
262
|
+
items:
|
|
263
|
+
toolCallVariants.length === 1
|
|
264
|
+
? toolCallVariants[0]
|
|
265
|
+
: toolCallVariants.length > 1
|
|
266
|
+
? { anyOf: toolCallVariants }
|
|
267
|
+
: emptyToolCall,
|
|
201
268
|
},
|
|
202
269
|
finishReason: {
|
|
203
270
|
type: 'string',
|
|
@@ -209,6 +276,8 @@ export function modelTurnOutputSchema(modelTurn = {}) {
|
|
|
209
276
|
required: ['text', 'toolCalls', 'finishReason'],
|
|
210
277
|
additionalProperties: false,
|
|
211
278
|
};
|
|
279
|
+
if (Object.keys(definitions).length) output.$defs = definitions;
|
|
280
|
+
return output;
|
|
212
281
|
}
|
|
213
282
|
|
|
214
283
|
export function normalizeModelTurnCompletion(raw, fallbackModel = 'local-companion') {
|
|
@@ -221,16 +290,9 @@ export function normalizeModelTurnCompletion(raw, fallbackModel = 'local-compani
|
|
|
221
290
|
if (!isRecord(call) || typeof call.name !== 'string' || !call.name.trim()) {
|
|
222
291
|
throw new Error(`Model turn tool call ${index + 1} is missing a name.`);
|
|
223
292
|
}
|
|
224
|
-
|
|
225
|
-
if (typeof toolArguments === 'string') {
|
|
226
|
-
try {
|
|
227
|
-
toolArguments = JSON.parse(toolArguments);
|
|
228
|
-
} catch {
|
|
229
|
-
throw new Error(`Model turn tool call ${index + 1} arguments must contain valid JSON.`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
293
|
+
const toolArguments = call.arguments;
|
|
232
294
|
if (toolArguments !== undefined && !isRecord(toolArguments)) {
|
|
233
|
-
throw new Error(`Model turn tool call ${index + 1} arguments must
|
|
295
|
+
throw new Error(`Model turn tool call ${index + 1} arguments must be an object.`);
|
|
234
296
|
}
|
|
235
297
|
return {
|
|
236
298
|
id: typeof call.id === 'string' && call.id.trim() ? call.id.slice(0, 160) : `local_tool_${index + 1}`,
|