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 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 it — **[docs/performance.md](docs/performance.md)**
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] !== "--preset"
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