premanmcp 1.1.14 → 1.1.16
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/decorate_tools.py +368 -0
- package/bin/eval.js +19 -5
- package/bin/eval_entry.js +134 -6
- package/bin/eval_harness.py +95 -1
- package/bin/eval_target.js +13 -8
- package/bin/eval_templates.js +275 -353
- package/bin/preman/__init__.py +267 -0
- package/bin/runner.js +2 -1
- package/bin/tool_proxy.py +218 -17
- package/package.json +2 -2
package/bin/eval_harness.py
CHANGED
|
@@ -309,11 +309,19 @@ def _turn_raw(ref, raw_result):
|
|
|
309
309
|
|
|
310
310
|
Carried on ``raw`` because assert-ai stores it verbatim and owns every other
|
|
311
311
|
field of the artifact; the backend reads it back per turn
|
|
312
|
-
(docs/superpowers/specs/2026-09-30-tool-call-capture-design.md §7.2).
|
|
312
|
+
(docs/superpowers/specs/2026-09-30-tool-call-capture-design.md §7.2). A
|
|
313
|
+
proxied agent's token counts become model-call nodes here, as spans would
|
|
314
|
+
(docs/superpowers/specs/2026-10-08-proxy-tokens-ts-design.md §5).
|
|
313
315
|
"""
|
|
314
316
|
raw = {"callable": ref}
|
|
315
317
|
capture = raw_result.get("tool_capture")
|
|
316
318
|
if isinstance(capture, dict):
|
|
319
|
+
try:
|
|
320
|
+
built = _proxy_nodes(capture)
|
|
321
|
+
if built is not None:
|
|
322
|
+
_add_nodes(raw, *built)
|
|
323
|
+
except Exception:
|
|
324
|
+
capture.pop("model_calls", None)
|
|
317
325
|
raw["tool_capture"] = _session._sanitize_endpoint_value(capture)
|
|
318
326
|
return raw
|
|
319
327
|
|
|
@@ -334,6 +342,79 @@ def _attach_capture(result, payload):
|
|
|
334
342
|
return
|
|
335
343
|
|
|
336
344
|
|
|
345
|
+
def _span_tokens_in(events) -> bool:
|
|
346
|
+
return any(isinstance(e, dict) and isinstance((e.get("raw") or {}).get("_tokens"), dict) for e in events or [])
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _proxy_nodes(capture, spans_have_tokens=False):
|
|
350
|
+
"""The proxy's token records for one turn, as the model-call nodes spans would give.
|
|
351
|
+
|
|
352
|
+
Pops ``model_calls`` so the stored capture never carries them. All or
|
|
353
|
+
nothing: one call without counts leaves the turn with none and
|
|
354
|
+
``tokens_missing`` saying why, because a partial sum reads as a complete
|
|
355
|
+
number. Returns (nodes, totals) or None.
|
|
356
|
+
"""
|
|
357
|
+
if not isinstance(capture, dict):
|
|
358
|
+
return None
|
|
359
|
+
calls = capture.pop("model_calls", None)
|
|
360
|
+
if not calls or spans_have_tokens:
|
|
361
|
+
return None
|
|
362
|
+
if not isinstance(calls, list) or not all(isinstance(c, dict) for c in calls):
|
|
363
|
+
capture["tokens_missing"] = "a model call's token counts could not be read"
|
|
364
|
+
return None
|
|
365
|
+
gap = next((c for c in calls if not (isinstance(c.get("input"), int) and isinstance(c.get("output"), int))), None)
|
|
366
|
+
if gap is not None:
|
|
367
|
+
capture["tokens_missing"] = gap.get("missing") or "a model call's token counts could not be read"
|
|
368
|
+
return None
|
|
369
|
+
nodes = [
|
|
370
|
+
{
|
|
371
|
+
"view": ["target", "combined"],
|
|
372
|
+
"actor": "target",
|
|
373
|
+
"edit": {"type": "add_message", "message": {"role": "assistant", "content": ""}},
|
|
374
|
+
"raw": {
|
|
375
|
+
"_node": c.get("kind") or "model",
|
|
376
|
+
"_model": c.get("model") or "",
|
|
377
|
+
"_tokens": {"input": c["input"], "output": c["output"]},
|
|
378
|
+
"_latency_ms": c.get("ms"),
|
|
379
|
+
"_source": "proxy",
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
for c in calls
|
|
383
|
+
]
|
|
384
|
+
totals = {
|
|
385
|
+
"total_tokens": {"input": sum(c["input"] for c in calls), "output": sum(c["output"] for c in calls)},
|
|
386
|
+
"total_latency_ms": round(sum(c["ms"] for c in calls if isinstance(c.get("ms"), (int, float))), 1),
|
|
387
|
+
"llm_call_count": len(calls),
|
|
388
|
+
}
|
|
389
|
+
return nodes, totals
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _add_nodes(raw, nodes, totals):
|
|
393
|
+
raw["trace_events"] = list(raw.get("trace_events") or []) + nodes
|
|
394
|
+
meta = dict(raw.get("trace_metadata") or {"nodes_visited": [], "tools_called": []})
|
|
395
|
+
meta.update(totals)
|
|
396
|
+
raw["trace_metadata"] = meta
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _attach_proxy_nodes(result, payload):
|
|
400
|
+
"""Token nodes for a traced turn whose spans carried no tokens (a proxied agent)."""
|
|
401
|
+
capture = payload.get("tool_capture") if isinstance(payload, dict) else None
|
|
402
|
+
if not isinstance(capture, dict):
|
|
403
|
+
return
|
|
404
|
+
try:
|
|
405
|
+
raw = result.raw if isinstance(result.raw, dict) else None
|
|
406
|
+
built = _proxy_nodes(capture, spans_have_tokens=_span_tokens_in((raw or {}).get("trace_events")))
|
|
407
|
+
if built is None or raw is None:
|
|
408
|
+
return
|
|
409
|
+
_add_nodes(raw, *built)
|
|
410
|
+
for message in reversed(result.interaction_messages):
|
|
411
|
+
if message.get("role") == "assistant" and isinstance(message.get("raw"), dict):
|
|
412
|
+
_add_nodes(message["raw"], *built)
|
|
413
|
+
return
|
|
414
|
+
except Exception:
|
|
415
|
+
capture.pop("model_calls", None) # never fail the turn, never store the raw records
|
|
416
|
+
|
|
417
|
+
|
|
337
418
|
async def _callable_run_turn(self, messages: list[Message]) -> TurnResult:
|
|
338
419
|
user_text = ""
|
|
339
420
|
for msg in reversed(messages):
|
|
@@ -570,6 +651,18 @@ if os.environ.get("PREMAN_EVAL_TRACE") == "1":
|
|
|
570
651
|
# LangGraph message trace is replaced only when both count the same calls.
|
|
571
652
|
capture = payload.get("tool_capture") if isinstance(payload, dict) else None
|
|
572
653
|
span_trace = next((t for t in reversed(holder) if t), None)
|
|
654
|
+
if (
|
|
655
|
+
span_trace is None
|
|
656
|
+
and isinstance(capture, dict)
|
|
657
|
+
and capture.get("source") == "decorated"
|
|
658
|
+
and capture.get("state") == "recorded"
|
|
659
|
+
and capture.get("trace") is None
|
|
660
|
+
):
|
|
661
|
+
# Every watched tool records itself, so no span is a confirmed
|
|
662
|
+
# zero, not a turn whose calls carry no trace.
|
|
663
|
+
capture["trace"] = {"links": "full", "known": ["step", "parent", "started", "ended"],
|
|
664
|
+
"unknown": {}, "calls": []}
|
|
665
|
+
return
|
|
573
666
|
if not isinstance(capture, dict) or span_trace is None:
|
|
574
667
|
return
|
|
575
668
|
own = capture.get("trace")
|
|
@@ -620,6 +713,7 @@ if os.environ.get("PREMAN_EVAL_TRACE") == "1":
|
|
|
620
713
|
_use_span_trace(payload, holder)
|
|
621
714
|
except Exception:
|
|
622
715
|
pass # a trace that cannot be chosen must never fail the turn
|
|
716
|
+
_attach_proxy_nodes(result, payload)
|
|
623
717
|
_attach_capture(result, payload)
|
|
624
718
|
if payload is None or any(m.get("role") == "tool" for m in result.interaction_messages):
|
|
625
719
|
return result # nothing reported, or the spans already carry the tools
|
package/bin/eval_target.js
CHANGED
|
@@ -207,7 +207,7 @@ export async function discoverTarget(args, { log = () => {}, allowSearch = true
|
|
|
207
207
|
return searchForEntry({ projectPath, args, log });
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
/** Shapes whose wrapper records tool calls:
|
|
210
|
+
/** Shapes whose wrapper records tool calls: PY_DECORATED, `_graph_calls`, or the
|
|
211
211
|
* recording proxy (PY_PROXY) in eval_templates.js. */
|
|
212
212
|
const CAPTURE_SHAPES = new Set(["py_history", "py_plain", "py_class", "other", "langgraph", "node_bridge", "cli_subprocess"]);
|
|
213
213
|
|
|
@@ -228,14 +228,19 @@ export function verifyEntry(saved) {
|
|
|
228
228
|
if (saved.sha256 && digest !== saved.sha256) return null;
|
|
229
229
|
|
|
230
230
|
// True for a wrapper the CLI generated with tool capture wired in (every
|
|
231
|
-
// shape but `node_bridge`, `cli_subprocess` and `langgraph` defines
|
|
232
|
-
// at module scope -- see
|
|
233
|
-
//
|
|
234
|
-
// `
|
|
235
|
-
//
|
|
236
|
-
//
|
|
231
|
+
// shape but `node_bridge`, `cli_subprocess` and `langgraph` defines
|
|
232
|
+
// `_DECORATED` at module scope -- see PY_DECORATED in eval_templates.js;
|
|
233
|
+
// `_TOOLS` is still recognised for wrappers approved before decorators;
|
|
234
|
+
// `langgraph` reads calls from the graph's messages in `_graph_calls`;
|
|
235
|
+
// `node_bridge` and `cli_subprocess` load the recording proxy in
|
|
236
|
+
// `_tool_proxy`). A wrapper approved before that change has no such line and
|
|
237
|
+
// keeps matching its digest, so this is read from the file rather than the
|
|
238
|
+
// approval record.
|
|
237
239
|
const capturesTools =
|
|
238
|
-
/^
|
|
240
|
+
/^_DECORATED = /m.test(source) ||
|
|
241
|
+
/^_TOOLS = /m.test(source) ||
|
|
242
|
+
/^def _graph_calls\(/m.test(source) ||
|
|
243
|
+
/^def _tool_proxy\(/m.test(source);
|
|
239
244
|
|
|
240
245
|
// Every file the approval produced, not only the wrapper.
|
|
241
246
|
//
|