premanmcp 1.1.4 → 1.1.5
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 +61 -0
- package/bin/eval_target.js +53 -17
- package/package.json +1 -1
package/bin/eval.js
CHANGED
|
@@ -575,6 +575,58 @@ export function readProgress(cwd, job) {
|
|
|
575
575
|
};
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Whether the tool calls the agent made are in the transcript the judge read.
|
|
580
|
+
*
|
|
581
|
+
* A behaviour like `unchecked_account_claim` is defined entirely in terms of
|
|
582
|
+
* tool calls: its taxonomy tells the judge to read the events and not to infer
|
|
583
|
+
* a lookup from the wording of a reply. So a transcript that lost them is not a
|
|
584
|
+
* degraded measurement, it is the opposite measurement -- an agent that checked
|
|
585
|
+
* before answering is indistinguishable from one that made the figure up, and
|
|
586
|
+
* the run reports the second. It happened for every run of that suite until the
|
|
587
|
+
* adapter's event field names were corrected, and it exited 0 each time.
|
|
588
|
+
*
|
|
589
|
+
* Compared against the adapter's own count rather than checked for zero on its
|
|
590
|
+
* own, because zero tool calls is a legitimate result: an agent asked a general
|
|
591
|
+
* question should not call anything. The failure is the disagreement.
|
|
592
|
+
*
|
|
593
|
+
* Returns "" when there is nothing wrong, so the caller can tell "fine" from
|
|
594
|
+
* "unreadable".
|
|
595
|
+
*/
|
|
596
|
+
export function missingToolEvidence(dir, stats) {
|
|
597
|
+
const forwarded = Number(stats?.toolCalls);
|
|
598
|
+
if (!Number.isFinite(forwarded) || forwarded <= 0) return "";
|
|
599
|
+
|
|
600
|
+
let recorded = 0;
|
|
601
|
+
try {
|
|
602
|
+
const text = readFileSync(path.join(dir, "inference_set.jsonl"), "utf8");
|
|
603
|
+
for (const line of text.split("\n")) {
|
|
604
|
+
if (!line.trim()) continue;
|
|
605
|
+
let row;
|
|
606
|
+
try {
|
|
607
|
+
row = JSON.parse(line);
|
|
608
|
+
} catch {
|
|
609
|
+
continue; // A row we cannot read is not evidence that evidence is missing.
|
|
610
|
+
}
|
|
611
|
+
for (const event of row.events || []) {
|
|
612
|
+
if (event?.edit?.type === "tool_call") recorded += 1;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
} catch {
|
|
616
|
+
// No transcript to read is a different failure, and the harness's own exit
|
|
617
|
+
// code reports it. Claiming this one on top would be guessing.
|
|
618
|
+
return "";
|
|
619
|
+
}
|
|
620
|
+
if (recorded > 0) return "";
|
|
621
|
+
|
|
622
|
+
return (
|
|
623
|
+
`the agent made ${forwarded} tool call(s) and none of them reached the transcript, so ` +
|
|
624
|
+
`the judge scored it as an agent that called nothing. Every case that answered from a ` +
|
|
625
|
+
`lookup would be marked unsupported. This is the adapter's event channel, not the ` +
|
|
626
|
+
`agent: check that the events it sends still match the fields assert-ai reads.`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
578
630
|
// ── Handing the run over to a surface ───────────────────────────────────
|
|
579
631
|
|
|
580
632
|
/** How long to hold a leased run waiting for somebody to be watching it. */
|
|
@@ -943,6 +995,15 @@ export async function executeEvalRun(
|
|
|
943
995
|
return { ...outcome, summary, artifacts };
|
|
944
996
|
}
|
|
945
997
|
|
|
998
|
+
// The same shape of failure one layer in: the agent was reached, it acted,
|
|
999
|
+
// and the record of it acting did not survive the trip to the judge.
|
|
1000
|
+
const blind = missingToolEvidence(artifacts, stats);
|
|
1001
|
+
if (blind) {
|
|
1002
|
+
const outcome = await finish(false, { summary, error: blind, exitCode });
|
|
1003
|
+
tidy(home);
|
|
1004
|
+
return { ...outcome, summary, artifacts };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
946
1007
|
const outcome = await finish(true, { summary, exitCode });
|
|
947
1008
|
tidy(home);
|
|
948
1009
|
return { ...outcome, summary, artifacts };
|
package/bin/eval_target.js
CHANGED
|
@@ -293,7 +293,7 @@ function readBody(request) {
|
|
|
293
293
|
/**
|
|
294
294
|
* One turn against PreMan's own agent.
|
|
295
295
|
*
|
|
296
|
-
* Tool calls come back in `turn.
|
|
296
|
+
* Tool calls come back in `turn.tool_calls` and are forwarded as adapter-shaped
|
|
297
297
|
* `events`, which is the difference between the judge seeing a transcript and
|
|
298
298
|
* the judge seeing what the agent *did*. `HTTPEndpointSession` promotes those to
|
|
299
299
|
* first-class interaction messages; without them a tool-using agent is scored on
|
|
@@ -443,7 +443,18 @@ async function premanTurn(target, args, { message, history }, state) {
|
|
|
443
443
|
.digest("hex");
|
|
444
444
|
state.conversations.set(nextKey, conversationId);
|
|
445
445
|
|
|
446
|
-
|
|
446
|
+
// Counted here rather than derived from the artifacts later, because these
|
|
447
|
+
// two numbers answer different questions: this is what the agent did, and
|
|
448
|
+
// what the transcript holds is what the judge got to see. A run where they
|
|
449
|
+
// disagree is the failure this counter exists to make visible.
|
|
450
|
+
//
|
|
451
|
+
// Counting the events rather than `turn.tool_calls` keeps it a count of what
|
|
452
|
+
// was forwarded: a call this adapter dropped is not one the transcript is
|
|
453
|
+
// missing. `tool_result` is one call, per `eventsFrom`.
|
|
454
|
+
const events = eventsFrom(turn);
|
|
455
|
+
state.toolCalls += events.filter((event) => event.role === "tool_result").length;
|
|
456
|
+
|
|
457
|
+
return { response: reply, events };
|
|
447
458
|
}
|
|
448
459
|
|
|
449
460
|
/**
|
|
@@ -457,29 +468,53 @@ async function premanTurn(target, args, { message, history }, state) {
|
|
|
457
468
|
* agent checked before answering was reading a transcript in which checking
|
|
458
469
|
* was invisible.
|
|
459
470
|
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
471
|
+
* The field names are assert-ai's `AdapterEvent`, and they are the whole
|
|
472
|
+
* difference between an event and nothing. Its normalizer keys on `role` and
|
|
473
|
+
* skips any event whose role is not one of its three, without logging -- so a
|
|
474
|
+
* plausible-looking `{type, name, arguments}` is dropped silently and produces
|
|
475
|
+
* exactly the same transcript as an agent that called nothing. Every run of
|
|
476
|
+
* `unchecked_account_claim` before this was scored against one: 24 cases, zero
|
|
477
|
+
* tool calls recorded, while the agent was answering with live figures that
|
|
478
|
+
* matched the workspace.
|
|
479
|
+
*
|
|
480
|
+
* One `tool_result` per call, and deliberately no `tool_call` event alongside
|
|
481
|
+
* it. `tool_result` is the one that becomes a tool call in the artifacts: a
|
|
482
|
+
* `tool_call` event is an assistant message carrying `tool_calls`, which the
|
|
483
|
+
* inference stage only remembers, while the `ToolCallEdit` that
|
|
484
|
+
* `edit.type == "tool_call"` refers to -- what the judge and the optimizer both
|
|
485
|
+
* count -- is written when the tool *message* arrives. Sending both records the
|
|
486
|
+
* same single call and adds an assistant message with empty content for each
|
|
487
|
+
* one, because the endpoint session attaches a `raw` to every event and the
|
|
488
|
+
* stage keeps any message that has one. Blank assistant turns are the last
|
|
489
|
+
* thing to show a judge deciding whether the agent answered from a lookup.
|
|
490
|
+
*
|
|
491
|
+
* Whether the tool ran travels in the result's text, in the taxonomy's own
|
|
492
|
+
* words, because text is what survives: of everything an event can carry, only
|
|
493
|
+
* `content`, `tool_name`, `tool_args` and `tool_call_id` reach the artifacts. A
|
|
494
|
+
* risky tool is not run — the broker records a proposal and waits for a person —
|
|
495
|
+
* and a judge that read the request as the deed would score a deletion that
|
|
496
|
+
* never occurred. What the tool returned is not carried at all: the backend's
|
|
497
|
+
* record is name, arguments and executed, which is what "did it look this up
|
|
498
|
+
* before answering" needs and no more.
|
|
464
499
|
*/
|
|
465
|
-
function eventsFrom(turn) {
|
|
500
|
+
export function eventsFrom(turn) {
|
|
466
501
|
const calls = Array.isArray(turn.tool_calls) ? turn.tool_calls : [];
|
|
467
502
|
const events = [];
|
|
468
503
|
for (const call of calls) {
|
|
469
504
|
const name = String(call?.name || "");
|
|
470
505
|
if (!name) continue;
|
|
506
|
+
const args = call.arguments;
|
|
471
507
|
events.push({
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
508
|
+
role: "tool_result",
|
|
509
|
+
content:
|
|
510
|
+
call.executed === false
|
|
511
|
+
? `executed: false — ${name} is a risky action held for the user's approval. It ` +
|
|
512
|
+
`did not run and returned no data.`
|
|
513
|
+
: `executed: true — ${name} ran and returned its result to the agent. The result ` +
|
|
514
|
+
`itself is not carried into this transcript.`,
|
|
515
|
+
tool_name: name,
|
|
516
|
+
tool_args: args && typeof args === "object" && !Array.isArray(args) ? args : {},
|
|
475
517
|
});
|
|
476
|
-
if (call.executed === false) {
|
|
477
|
-
events.push({
|
|
478
|
-
type: "tool_result",
|
|
479
|
-
name,
|
|
480
|
-
result: { executed: false, reason: "awaiting the user's approval" },
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
518
|
}
|
|
484
519
|
return events;
|
|
485
520
|
}
|
|
@@ -515,6 +550,7 @@ export async function startAdapter(target, args, { log = () => {}, onStep = null
|
|
|
515
550
|
failures: 0,
|
|
516
551
|
lastFailure: "",
|
|
517
552
|
streamFailures: 0,
|
|
553
|
+
toolCalls: 0,
|
|
518
554
|
log,
|
|
519
555
|
/**
|
|
520
556
|
* One thing the agent said it was doing, on its way to the dashboard.
|