llm-output-guard 1.10.0 → 1.11.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/README.md +5 -1
- package/dist/bin.cjs +48 -2
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +48 -2
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,6 +142,10 @@ A turn carrying tool calls is judged by its **arguments**, never its prose —
|
|
|
142
142
|
that is the difference between an agent working through a list and an agent
|
|
143
143
|
stuck on one item.
|
|
144
144
|
|
|
145
|
+
**[Try it on a run →](https://edwinsatya.github.io/llm-output-guard/)** — the
|
|
146
|
+
playground has an *agent run* mode: pick a trace, see which turns form the
|
|
147
|
+
cycle. The healthy traps are the half worth looking at.
|
|
148
|
+
|
|
145
149
|
**[docs/agent-loops.md](docs/agent-loops.md)**
|
|
146
150
|
|
|
147
151
|
## The hard part is not catching loops
|
|
@@ -267,7 +271,7 @@ because only one of those is evidence.
|
|
|
267
271
|
- **Scores, not booleans.** Detectors report 0–1 and leave the threshold decision to you.
|
|
268
272
|
- **Abstains rather than guesses.** Samples too short to judge score 0.
|
|
269
273
|
- **Hand-rolled LZ77** rather than `node:zlib`, so the package stays runtime-agnostic.
|
|
270
|
-
- **Sub-millisecond**, 0.383 ms at 500 B and 1.014 ms at 32 KB, with one detector accounting for most of it. `npm run bench` reproduces
|
|
274
|
+
- **Sub-millisecond**, 0.383 ms at 500 B and 1.014 ms at 32 KB, with one detector accounting for most of it. A 12-turn agent run costs 0.075 ms. `npm run bench` reproduces both — **[docs/performance.md](docs/performance.md)**
|
|
271
275
|
- **Chinese, Japanese and Thai** are handled where they differ: `TAIL_LOOP` switches to character mode, `REPETITION` is blind and says so — **[docs/script-coverage.md](docs/script-coverage.md)**
|
|
272
276
|
|
|
273
277
|
## Stability
|
package/dist/bin.cjs
CHANGED
|
@@ -1086,6 +1086,12 @@ function runCheck(args) {
|
|
|
1086
1086
|
let presetName = "chat";
|
|
1087
1087
|
const presetAt = args.indexOf("--preset");
|
|
1088
1088
|
if (presetAt !== -1) {
|
|
1089
|
+
if (asTrace) {
|
|
1090
|
+
process.stderr.write(
|
|
1091
|
+
"--preset applies to responses, not runs. --trace scores AGENT_LOOP, which has one threshold: use --max-agent-loop.\n"
|
|
1092
|
+
);
|
|
1093
|
+
return 2;
|
|
1094
|
+
}
|
|
1089
1095
|
const name = args[presetAt + 1];
|
|
1090
1096
|
if (!name || !Object.hasOwn(presets, name)) {
|
|
1091
1097
|
process.stderr.write(
|
|
@@ -1097,8 +1103,41 @@ function runCheck(args) {
|
|
|
1097
1103
|
preset = presets[name];
|
|
1098
1104
|
presetName = name;
|
|
1099
1105
|
}
|
|
1106
|
+
const traceOptions = {};
|
|
1107
|
+
const numeric = [
|
|
1108
|
+
["--window", "window"],
|
|
1109
|
+
["--min-turns", "minTurns"],
|
|
1110
|
+
["--max-agent-loop", "maxAgentLoop"]
|
|
1111
|
+
];
|
|
1112
|
+
const VALUED = /* @__PURE__ */ new Set(["--preset", "--ignore-tools", ...numeric.map(([flag]) => flag)]);
|
|
1113
|
+
for (const [flag, key] of numeric) {
|
|
1114
|
+
const at = args.indexOf(flag);
|
|
1115
|
+
if (at === -1) continue;
|
|
1116
|
+
const value = Number(args[at + 1]);
|
|
1117
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1118
|
+
process.stderr.write(`${flag} expects a number, got: ${args[at + 1] ?? "(nothing)"}
|
|
1119
|
+
`);
|
|
1120
|
+
return 2;
|
|
1121
|
+
}
|
|
1122
|
+
traceOptions[key] = value;
|
|
1123
|
+
}
|
|
1124
|
+
const ignoreAt = args.indexOf("--ignore-tools");
|
|
1125
|
+
if (ignoreAt !== -1) {
|
|
1126
|
+
const names = (args[ignoreAt + 1] ?? "").split(",").map((n) => n.trim()).filter(Boolean);
|
|
1127
|
+
if (names.length === 0) {
|
|
1128
|
+
process.stderr.write("--ignore-tools expects a comma-separated list of tool names\n");
|
|
1129
|
+
return 2;
|
|
1130
|
+
}
|
|
1131
|
+
traceOptions.ignoreTools = names;
|
|
1132
|
+
}
|
|
1133
|
+
const usedTraceFlag = [...VALUED].find((f) => f !== "--preset" && args.includes(f));
|
|
1134
|
+
if (!asTrace && usedTraceFlag) {
|
|
1135
|
+
process.stderr.write(`${usedTraceFlag} only applies with --trace
|
|
1136
|
+
`);
|
|
1137
|
+
return 2;
|
|
1138
|
+
}
|
|
1100
1139
|
const files = args.filter(
|
|
1101
|
-
(arg, i) => !arg.startsWith("--") && args[i - 1]
|
|
1140
|
+
(arg, i) => !arg.startsWith("--") && !VALUED.has(args[i - 1] ?? "")
|
|
1102
1141
|
);
|
|
1103
1142
|
return (async () => {
|
|
1104
1143
|
const items = [];
|
|
@@ -1173,7 +1212,7 @@ function runCheck(args) {
|
|
|
1173
1212
|
}
|
|
1174
1213
|
const checked = asTrace ? traces.map(({ label, turns }) => ({
|
|
1175
1214
|
label: `${label} (${turns.length} turns)`,
|
|
1176
|
-
verdict: checkTrace(turns)
|
|
1215
|
+
verdict: checkTrace(turns, traceOptions)
|
|
1177
1216
|
})) : items.map(({ label, text }) => ({
|
|
1178
1217
|
label,
|
|
1179
1218
|
verdict: checkOutput(text, preset)
|
|
@@ -1209,6 +1248,13 @@ Options
|
|
|
1209
1248
|
--preset <name> chat | strictJson | longForm | lenient (default chat)
|
|
1210
1249
|
--jsonl read input as JSONL, one logged response per line
|
|
1211
1250
|
--trace read input as agent runs, one run per line
|
|
1251
|
+
|
|
1252
|
+
With --trace, scoring the run instead of the response:
|
|
1253
|
+
--ignore-tools <a,b> tools whose calls are not compared \u2014 a job poller,
|
|
1254
|
+
a sleep, a clock. By shape those are a loop
|
|
1255
|
+
--max-agent-loop <n> cycle-coverage threshold (default 0.4)
|
|
1256
|
+
--window <n> trailing turns inspected (default 12)
|
|
1257
|
+
--min-turns <n> turns required before judging at all (default 4)
|
|
1212
1258
|
--json emit a verdict per response as JSONL, for calibrate
|
|
1213
1259
|
--quiet no per-response output; the exit code is the answer
|
|
1214
1260
|
|