premanmcp 1.1.0 → 1.1.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/bin/eval.js +24 -1
- package/bin/eval_target.js +64 -3
- package/bin/shared.js +14 -0
- package/package.json +1 -1
package/bin/eval.js
CHANGED
|
@@ -34,7 +34,7 @@ import os from "node:os";
|
|
|
34
34
|
import path from "node:path";
|
|
35
35
|
import { fileURLToPath } from "node:url";
|
|
36
36
|
|
|
37
|
-
import { discoverTarget, startAdapter, stepFeed } from "./eval_target.js";
|
|
37
|
+
import { adapterStats, discoverTarget, startAdapter, stepFeed } from "./eval_target.js";
|
|
38
38
|
import { resolveEvalSurface } from "./link.js";
|
|
39
39
|
import {
|
|
40
40
|
backendUrl,
|
|
@@ -799,6 +799,29 @@ export async function executeEvalRun(
|
|
|
799
799
|
}
|
|
800
800
|
|
|
801
801
|
if (exitCode === 0) {
|
|
802
|
+
// A harness that exited 0 having never once reached the agent is not a
|
|
803
|
+
// result, and it is the most misleading shape a run can take: every case
|
|
804
|
+
// was scored, the numbers look like measurements, and what the judge
|
|
805
|
+
// actually read was the adapter's apology on each turn. The one that
|
|
806
|
+
// prompted this presented an `api.preman.live` key to an agent on
|
|
807
|
+
// localhost, 401ed eighteen times, and finished ok.
|
|
808
|
+
//
|
|
809
|
+
// Turns are counted rather than failures, because a run where the agent
|
|
810
|
+
// answered some of the time is a real measurement of a flaky agent and
|
|
811
|
+
// deciding otherwise here would throw away the finding.
|
|
812
|
+
const stats = adapterStats();
|
|
813
|
+
if (stats && stats.turns === 0 && stats.failures > 0) {
|
|
814
|
+
const outcome = await finish(false, {
|
|
815
|
+
summary,
|
|
816
|
+
error:
|
|
817
|
+
`not one of the ${stats.failures} turns reached the agent, so nothing here was ` +
|
|
818
|
+
`measured against it. The last reason was: ${stats.lastFailure || "unknown"}`,
|
|
819
|
+
exitCode,
|
|
820
|
+
});
|
|
821
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
822
|
+
return { ...outcome, summary, artifacts };
|
|
823
|
+
}
|
|
824
|
+
|
|
802
825
|
const outcome = await finish(true, { summary, exitCode });
|
|
803
826
|
rmSync(scratch, { recursive: true, force: true });
|
|
804
827
|
return { ...outcome, summary, artifacts };
|
package/bin/eval_target.js
CHANGED
|
@@ -35,7 +35,46 @@
|
|
|
35
35
|
import { createServer } from "node:http";
|
|
36
36
|
import { createHash } from "node:crypto";
|
|
37
37
|
|
|
38
|
-
import { backendUrl, resolveApiKey } from "./shared.js";
|
|
38
|
+
import { backendUrl, resolveApiKey, storedKeyOrigin } from "./shared.js";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The credential to present to the agent under test.
|
|
42
|
+
*
|
|
43
|
+
* When the agent *is* a PreMan deployment, this is a PreMan key -- but not
|
|
44
|
+
* necessarily the one this CLI reports results with. Those are two different
|
|
45
|
+
* systems that happen to be the same product: the control plane is whichever
|
|
46
|
+
* deployment issued the saved key, and the agent is whatever answered on this
|
|
47
|
+
* machine. A run that presented an `api.preman.live` key to a backend on
|
|
48
|
+
* localhost 401ed on all eighteen turns and still reported a score, because the
|
|
49
|
+
* adapter turns a failure into a transcript the judge can read.
|
|
50
|
+
*
|
|
51
|
+
* Refusing beats warning. Every turn would fail, the whole run would be scored
|
|
52
|
+
* against the adapter's apology, and it would cost a full round of model calls
|
|
53
|
+
* to arrive at a number about nothing.
|
|
54
|
+
*/
|
|
55
|
+
function agentToken(args, baseUrl, log) {
|
|
56
|
+
const named = args.value("--agent-key", process.env.PREMAN_AGENT_KEY || "");
|
|
57
|
+
if (named) return named;
|
|
58
|
+
|
|
59
|
+
const key = resolveApiKey(args);
|
|
60
|
+
const origin = storedKeyOrigin(args);
|
|
61
|
+
if (!origin || sameDeployment(origin, baseUrl)) return key;
|
|
62
|
+
|
|
63
|
+
throw new TargetError(
|
|
64
|
+
`the agent here is ${baseUrl}, but the only PreMan key on this machine was issued by ` +
|
|
65
|
+
`${origin}. It would be refused on every turn and the run would score the refusals. ` +
|
|
66
|
+
`Set PREMAN_AGENT_KEY to a key ${baseUrl} accepts, or stop that agent so this measures ` +
|
|
67
|
+
`${origin} instead.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sameDeployment(a, b) {
|
|
72
|
+
try {
|
|
73
|
+
return new URL(a).host === new URL(b).host;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
39
78
|
|
|
40
79
|
export class TargetError extends Error {}
|
|
41
80
|
|
|
@@ -130,7 +169,7 @@ export async function discoverTarget(args, { log = () => {} } = {}) {
|
|
|
130
169
|
// The agent's own credential, held here and never sent to PreMan's
|
|
131
170
|
// control plane. The adapter is the only thing that ever presents it,
|
|
132
171
|
// which is the whole reason the adapter runs on this machine.
|
|
133
|
-
token:
|
|
172
|
+
token: agentToken(args, `http://127.0.0.1:${port}`, log),
|
|
134
173
|
label: `PreMan agent (local, port ${port})`,
|
|
135
174
|
how: `probed localhost and got a PreMan /health on port ${port}`,
|
|
136
175
|
probed,
|
|
@@ -146,7 +185,7 @@ export async function discoverTarget(args, { log = () => {} } = {}) {
|
|
|
146
185
|
return {
|
|
147
186
|
kind: "preman-chat",
|
|
148
187
|
baseUrl: configured,
|
|
149
|
-
token:
|
|
188
|
+
token: agentToken(args, configured, log),
|
|
150
189
|
label: `PreMan agent (${configured})`,
|
|
151
190
|
how: `nothing local answered, so this is the backend the CLI is configured against`,
|
|
152
191
|
probed,
|
|
@@ -463,6 +502,7 @@ export async function startAdapter(target, args, { log = () => {}, onStep = null
|
|
|
463
502
|
conversations: new Map(),
|
|
464
503
|
turns: 0,
|
|
465
504
|
failures: 0,
|
|
505
|
+
lastFailure: "",
|
|
466
506
|
streamFailures: 0,
|
|
467
507
|
log,
|
|
468
508
|
/**
|
|
@@ -500,6 +540,7 @@ export async function startAdapter(target, args, { log = () => {}, onStep = null
|
|
|
500
540
|
reply(200, result);
|
|
501
541
|
} catch (error) {
|
|
502
542
|
state.failures += 1;
|
|
543
|
+
state.lastFailure = error.message;
|
|
503
544
|
// 200 with an error string in `response`, not a 5xx. A transport failure
|
|
504
545
|
// aborts the case and loses it; an agent that said something unhelpful is
|
|
505
546
|
// a transcript the judge can score. Which of those this is depends on
|
|
@@ -519,12 +560,32 @@ export async function startAdapter(target, args, { log = () => {}, onStep = null
|
|
|
519
560
|
const url = `http://127.0.0.1:${port}/chat`;
|
|
520
561
|
log(`Adapter listening on ${url} -> ${target.label}`);
|
|
521
562
|
|
|
563
|
+
activeStats = state;
|
|
522
564
|
return {
|
|
523
565
|
url,
|
|
524
566
|
translated: true,
|
|
525
567
|
stats: state,
|
|
526
568
|
close: async () => {
|
|
569
|
+
activeStats = null;
|
|
527
570
|
await new Promise((resolve) => server.close(resolve));
|
|
528
571
|
},
|
|
529
572
|
};
|
|
530
573
|
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* The counters for the adapter this process is running, if it is running one.
|
|
577
|
+
*
|
|
578
|
+
* A module-level handle rather than an argument threaded down, because the
|
|
579
|
+
* adapter is started by the command and the counters are needed by
|
|
580
|
+
* `executeEvalRun`, which the runner loop calls — and the loop sits between
|
|
581
|
+
* them knowing nothing about either. There is at most one adapter per process:
|
|
582
|
+
* it is opened once for the whole invocation and the runs share it.
|
|
583
|
+
*
|
|
584
|
+
* Null when the target is a URL the customer named, which the CLI does not
|
|
585
|
+
* mediate and therefore cannot count.
|
|
586
|
+
*/
|
|
587
|
+
let activeStats = null;
|
|
588
|
+
|
|
589
|
+
export function adapterStats() {
|
|
590
|
+
return activeStats;
|
|
591
|
+
}
|
package/bin/shared.js
CHANGED
|
@@ -235,6 +235,20 @@ function activeLogin(args) {
|
|
|
235
235
|
return apiKeyIsExplicit(args) ? null : readStoredCredentials();
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Which deployment the saved key was minted against, or "" if that is unknown.
|
|
240
|
+
*
|
|
241
|
+
* Unknown is the honest answer for a key given on the command line or in the
|
|
242
|
+
* environment: nothing records where it came from, and guessing would be worse
|
|
243
|
+
* than admitting it. Callers presenting a key to something other than PreMan's
|
|
244
|
+
* own control plane need this to tell "this will work" from "this is a key for
|
|
245
|
+
* somewhere else".
|
|
246
|
+
*/
|
|
247
|
+
export function storedKeyOrigin(args) {
|
|
248
|
+
const login = activeLogin(args);
|
|
249
|
+
return login?.backend_url || "";
|
|
250
|
+
}
|
|
251
|
+
|
|
238
252
|
/**
|
|
239
253
|
* Which PreMan deployment this invocation talks to.
|
|
240
254
|
*
|