troxy-cli 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/mcp-server.js +14 -4
- package/src/tests/model-checkpoint.test.js +79 -0
package/package.json
CHANGED
package/src/mcp-server.js
CHANGED
|
@@ -62,7 +62,16 @@ export async function runMcp() {
|
|
|
62
62
|
// Token Optimization is supposed to be a precise record, not a mood.
|
|
63
63
|
// The trigger below is mechanical instead: did this turn use a tool,
|
|
64
64
|
// yes or no. That is a fact you can check, not a judgment you make.
|
|
65
|
-
'You MUST also call the evaluate_model tool at the start of ANY turn where you will use one or more tools of any kind — reading or writing a file, running a command, browsing, calling an external API, anything beyond a plain text reply — and whenever you switch model or effort level partway through the turn. This is not a judgment call about how the turn feels: if you are about to use a tool, call evaluate_model first, every time, regardless of how small the turn seems
|
|
65
|
+
'You MUST also call the evaluate_model tool at the start of ANY turn where you will use one or more tools of any kind — reading or writing a file, running a command, browsing, calling an external API, anything beyond a plain text reply — and whenever you switch model or effort level partway through the turn. This is not a judgment call about how the turn feels: if you are about to use a tool, call evaluate_model first, every time, regardless of how small the turn seems, and WAIT for the decision before proceeding. Give the exact model id and how many tokens you expect to use. Do not use any tool without first receiving an ALLOW or NOTIFY decision; if BLOCK, pick a cheaper or smaller model and call evaluate_model again with the new id; if ESCALATE, call wait_for_approval and do not proceed until it resolves. Then, at the end of that same turn, you MUST call report_model_usage with the audit_id from that decision and the real token total from your model API. Both halves are required: without the second call Troxy has only your prediction, and the account owner is shown that this agent\'s costs are unverified. Report the true number even when it is far above your estimate. ' +
|
|
66
|
+
// A turn that is PURE conversation, no tool use at all, still gets
|
|
67
|
+
// logged, just not gated. Blocking a chat reply on a network round
|
|
68
|
+
// trip protects against a risk that is vanishingly rare in practice
|
|
69
|
+
// (almost nobody writes a policy to block message content), and the
|
|
70
|
+
// cost of blocking anyway is a slower product on every single message.
|
|
71
|
+
// So chat calls the same two tools, in the same order, but treats the
|
|
72
|
+
// decision as a record rather than a gate: reply to the user first,
|
|
73
|
+
// report afterward, never hold the reply for Troxy's answer.
|
|
74
|
+
'A turn that is pure conversation, with no tool use at all, is different: you still call evaluate_model and report_model_usage for it, but you do NOT wait for or act on the decision before replying. Reply to the user first. Then, as the last step of that same turn (or the first step of your next turn if your environment does not let you act again after producing a reply), call evaluate_model with the model and your best token estimate, and call report_model_usage with the real total right after — same two tools as above, just not gating. If the decision comes back BLOCK or ESCALATE for a chat turn, do not retry with a different model and do not poll wait_for_approval: the reply has already been sent, there is nothing left to gate, and the decision is recorded for the account owner\'s visibility rather than enforced in the moment. Chat usage still counts toward every rolling spend and token limit, so it still shapes what the next tool-use call is allowed to do.',
|
|
66
75
|
},
|
|
67
76
|
{ capabilities: { tools: {} } },
|
|
68
77
|
);
|
|
@@ -279,9 +288,10 @@ export async function runMcp() {
|
|
|
279
288
|
description:
|
|
280
289
|
'Evaluate whether running a task on a given AI model should be allowed, blocked, or escalated ' +
|
|
281
290
|
'based on your Troxy policies, and record what it is expected to cost. ' +
|
|
282
|
-
'Call this at the start
|
|
283
|
-
'
|
|
284
|
-
'
|
|
291
|
+
'Call this on every turn: at the start, before proceeding, if you will use a tool of any kind, or whenever you switch model or effort level mid-turn; ' +
|
|
292
|
+
'or after replying, without waiting for the decision, if the turn was pure conversation with no tool use. ' +
|
|
293
|
+
'Tool use gates on the decision; chat only records it. ' +
|
|
294
|
+
'Returns an audit_id. Keep it: you must pass it to report_model_usage when the turn finishes.',
|
|
285
295
|
inputSchema: {
|
|
286
296
|
type: 'object',
|
|
287
297
|
required: ['model'],
|
|
@@ -166,3 +166,82 @@ test('evaluate_model\'s own tool description matches the mechanical trigger', ()
|
|
|
166
166
|
assert.ok(!/substantial/i.test(desc), 'the tool description still says "substantial"');
|
|
167
167
|
assert.ok(/tool/i.test(desc), 'the tool description does not mention the tool-use trigger');
|
|
168
168
|
});
|
|
169
|
+
|
|
170
|
+
test('pure chat still reports usage, it just does not gate on the decision', () => {
|
|
171
|
+
// The first version of this fix (tool-use = mechanical trigger) left chat
|
|
172
|
+
// unreported entirely, on the reasoning that chat isn't "work." That
|
|
173
|
+
// reasoning doesn't survive contact with the goal: chat still costs real
|
|
174
|
+
// tokens, and a page that silently misses all chat-only usage is not a
|
|
175
|
+
// precise record, it just moved the same gap somewhere less visible.
|
|
176
|
+
const instructions = src.slice(src.indexOf("'You are connected to Troxy"), src.indexOf('capabilities'));
|
|
177
|
+
const chatSeg = instructions.slice(instructions.indexOf('A turn that is pure conversation'));
|
|
178
|
+
assert.ok(chatSeg, 'no instruction exists for the pure-conversation case at all');
|
|
179
|
+
assert.ok(
|
|
180
|
+
/still call evaluate_model and report_model_usage/.test(chatSeg),
|
|
181
|
+
'chat turns are not instructed to call the checkpoint tools at all, so '
|
|
182
|
+
+ 'chat-only usage is invisible on Token Optimization',
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('chat is told explicitly not to block the reply on the decision', () => {
|
|
187
|
+
// The whole point of treating chat differently from tool use is that a
|
|
188
|
+
// human is waiting on the reply. If the instructions still say "wait for
|
|
189
|
+
// the decision" for chat, this fix adds latency to every message for no
|
|
190
|
+
// behavioural gain over the tool-use path it was meant to avoid copying.
|
|
191
|
+
const instructions = src.slice(src.indexOf("'You are connected to Troxy"), src.indexOf('capabilities'));
|
|
192
|
+
const chatSeg = instructions.slice(instructions.indexOf('A turn that is pure conversation'));
|
|
193
|
+
assert.ok(
|
|
194
|
+
/do NOT wait for or act on the decision before replying/.test(chatSeg),
|
|
195
|
+
'chat instructions do not say to reply before waiting on the checkpoint',
|
|
196
|
+
);
|
|
197
|
+
assert.ok(
|
|
198
|
+
/Reply to the user first/.test(chatSeg),
|
|
199
|
+
'chat instructions do not establish reply-then-report ordering',
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('a BLOCK or ESCALATE on a chat turn does not trigger retry or polling', () => {
|
|
204
|
+
// Retrying with a cheaper model, or polling wait_for_approval, are the
|
|
205
|
+
// tool-use responses to BLOCK/ESCALATE. For chat the reply is already sent
|
|
206
|
+
// by the time the decision comes back, so both of those actions are
|
|
207
|
+
// meaningless and would just make the agent stall on a message that
|
|
208
|
+
// already went out.
|
|
209
|
+
const instructions = src.slice(src.indexOf("'You are connected to Troxy"), src.indexOf('capabilities'));
|
|
210
|
+
const chatSeg = instructions.slice(instructions.indexOf('A turn that is pure conversation'));
|
|
211
|
+
assert.ok(
|
|
212
|
+
/do not retry with a different model and do not poll wait_for_approval/.test(chatSeg),
|
|
213
|
+
'chat instructions do not rule out the tool-use recovery actions, which '
|
|
214
|
+
+ 'would stall a reply that has already been sent',
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('chat usage still feeds the rolling limits that gate tool use', () => {
|
|
219
|
+
// Not gating chat in real time is only an acceptable trade if chat spend
|
|
220
|
+
// still counts toward spend_per_hour / tokens_per_day, so a chat-heavy
|
|
221
|
+
// agent still gets caught the moment it tries to use a tool. Otherwise
|
|
222
|
+
// "we don't block chat" quietly becomes "chat spend is uncapped."
|
|
223
|
+
const instructions = src.slice(src.indexOf("'You are connected to Troxy"), src.indexOf('capabilities'));
|
|
224
|
+
const chatSeg = instructions.slice(instructions.indexOf('A turn that is pure conversation'));
|
|
225
|
+
assert.ok(
|
|
226
|
+
/still counts toward every rolling spend and token limit/.test(chatSeg),
|
|
227
|
+
'nothing tells the agent that unblocked chat spend still accumulates '
|
|
228
|
+
+ 'toward the limits a later tool-use call is checked against',
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('no new tool was introduced for the chat path', () => {
|
|
233
|
+
// The backend already supports this: report_model_usage looks up its row
|
|
234
|
+
// by id/user/agent, never by decision, so it works identically whether the
|
|
235
|
+
// row says ALLOW, BLOCK, or ESCALATE. Reusing evaluate_model +
|
|
236
|
+
// report_model_usage for chat means zero backend changes and zero new
|
|
237
|
+
// surface area to get wrong; a bespoke chat-logging tool would duplicate a
|
|
238
|
+
// path that is already tested end to end.
|
|
239
|
+
// Scoped to the tools array, not the whole file: the Server constructor
|
|
240
|
+
// also has a top-level `name: 'troxy'` for the server itself, which the
|
|
241
|
+
// unscoped regex was catching as a ninth "tool".
|
|
242
|
+
const toolsSeg = src.slice(src.indexOf('tools: ['), src.indexOf('server.setRequestHandler(CallToolRequestSchema'));
|
|
243
|
+
const toolNames = [...toolsSeg.matchAll(/name: '([a-z_]+)'/g)].map(m => m[1]);
|
|
244
|
+
const unique = new Set(toolNames);
|
|
245
|
+
assert.strictEqual(unique.size, 8, 'a tool was added or removed for this change');
|
|
246
|
+
assert.ok(unique.has('evaluate_model') && unique.has('report_model_usage'));
|
|
247
|
+
});
|