pi-warden 1.7.0 → 1.11.2
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/CHANGELOG.md +84 -0
- package/dist/config.d.ts +12 -1
- package/dist/config.js +9 -1
- package/dist/conscience.d.ts +2 -1
- package/dist/conscience.js +3 -3
- package/dist/dedupe.d.ts +8 -0
- package/dist/dedupe.js +34 -0
- package/dist/extension.js +142 -41
- package/dist/filter.d.ts +5 -4
- package/dist/filter.js +5 -5
- package/dist/guard.d.ts +13 -3
- package/dist/guard.js +372 -58
- package/dist/relevance.d.ts +4 -3
- package/dist/relevance.js +2 -2
- package/dist/rules-calibrate.d.ts +2 -3
- package/dist/rules-calibrate.js +3 -4
- package/dist/rules-lint.d.ts +3 -3
- package/dist/rules-lint.js +2 -1
- package/dist/rules.d.ts +26 -4
- package/dist/rules.js +37 -12
- package/dist/script-bodies.d.ts +8 -0
- package/dist/script-bodies.js +144 -1
- package/dist/shape.d.ts +1 -1
- package/dist/shape.js +3 -1
- package/dist/trial.d.ts +54 -0
- package/dist/trial.js +81 -0
- package/dist/turn-rules.d.ts +4 -5
- package/dist/turn-rules.js +4 -4
- package/package.json +4 -4
package/dist/extension.js
CHANGED
|
@@ -6,7 +6,7 @@ import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
6
6
|
import { defaultHostDirs } from "./host-dirs.js";
|
|
7
7
|
import * as tuiModule from "@earendil-works/pi-tui";
|
|
8
8
|
const MouseRegion = tuiModule.MouseRegion;
|
|
9
|
-
import { authState, backendHost, createTypeSafe, describeAuth, resolveBackend,
|
|
9
|
+
import { authState, backendHost, createTypeSafe, describeAuth, resolveBackend, chunkEvaluationRequest } from "pi-typesafe";
|
|
10
10
|
import { ensureApiKey } from "pi-typesafe/ui";
|
|
11
11
|
import { backendName, describeBackend, disclosureFor, judgeOptions, loginStoresKey } from "./backend.js";
|
|
12
12
|
import { ActionGuard } from "./action-guard.js";
|
|
@@ -17,7 +17,7 @@ import * as configModule from "./config.js";
|
|
|
17
17
|
import { applyUserOverrides, defaultConfig, getNestedValue, isMode, loadConfig, PACKAGE_NAME, parseConfigValue, PROJECT_CONFIG_FILE, projectConfigPath, readUserConfig, setNestedValue, setUserSetting, userConfigPath, writeUserConfig } from "./config.js";
|
|
18
18
|
import { checkSkipReason, classifyToolResult, doneNudge, emptyEvidence, evaluateDone, finalAssistantText, formatDone, isVisualCheck, localUrl, needsDoneCheck, projectCheckCommand, recordOutcome as recordDoneOutcome, recordUi } from "./done.js";
|
|
19
19
|
import { causeNudge, causePreFilter, emptyCauseActivity, evaluateCause, formatCause, recordCauseActivity } from "./cause.js";
|
|
20
|
-
import { createdScratch, describeAsked, evaluateAction, formatVerdictTokens, higher, inertPathRules, intentSteer, largeOutputNotice, movedInTargets, offTaskSteer, pruneScratch, scratchCandidates, SCRATCH_PATHS_ENV, scratchPaths, shouldProceedMessage, SLOP_LABELS, SteerRepeatWindow, steerReason, stripDataText, unknownExemptIds, wardenHostPaths, writeSinkTargets, isVisibleCommand } from "./guard.js";
|
|
20
|
+
import { createdScratch, describeAsked, evaluateAction, formatVerdictTokens, higher, inertPathRules, intentSteer, largeOutputNotice, movedInTargets, offTaskSteer, pruneScratch, scratchCandidates, SCRATCH_PATHS_ENV, scratchPaths, scoreFreeReason, shouldProceedMessage, SLOP_LABELS, SteerRepeatWindow, steerReason, stripDataText, unknownExemptIds, wardenHostPaths, writeSinkTargets, isVisibleCommand } from "./guard.js";
|
|
21
21
|
import { commandOf } from "./tools.js";
|
|
22
22
|
import { mergeWrites, shellWrites } from "./shell-writes.js";
|
|
23
23
|
import { formatHolds, HoldLedger, HoldLog, holdLogPath, mutatingCall, outcomeNote, regretsAt, textRegrets } from "./holds.js";
|
|
@@ -27,7 +27,7 @@ import { compressOutput, duplicateNote, evaluateOutput, isGenericExcerpt, mergeO
|
|
|
27
27
|
import { filterOutput } from "./filter.js";
|
|
28
28
|
import { classifyRecall, detectSearchTool, recallInstruction } from "./recall.js";
|
|
29
29
|
import { maskSecrets, redact } from "./redact.js";
|
|
30
|
-
import { formatRules, pathNoteSteer, projectPath, RulesGuard, rulesSteer, RULES_FILE, FALLBACK_FILES, turnRulesFor } from "./rules.js";
|
|
30
|
+
import { askChunks, formatRules, pathNoteSteer, projectPath, questionLimit, RulesGuard, rulesSteer, RULES_FILE, FALLBACK_FILES, turnRulesFor } from "./rules.js";
|
|
31
31
|
import { evaluateTurnRun, SHELL_RULES_CHECKS, snapshotTree, turnSteer } from "./turn-rules.js";
|
|
32
32
|
import { checkRules, formatRulesCheck } from "./rules-lint.js";
|
|
33
33
|
import { parseRulesAuditArgs, parseBenchArgs, runRulesAudit, runBench, formatRulesAudit, formatBench } from "./rules-audit.js";
|
|
@@ -50,11 +50,12 @@ import { openConfigPanel, openTracePanel } from "./panel.js";
|
|
|
50
50
|
import { completeConfig, shapeWarning, taskSpine } from "./shape.js";
|
|
51
51
|
import { ContextLedger, formatFilterLedger, formatLedger } from "./saver.js";
|
|
52
52
|
import { buildCuratorRequest, curatedRuleList, curatedRules, CURATOR_TIMEOUT_MS, CURATOR_TYPE, CuratorLedger, formatCuratedRules, formatCurator, ruleScores } from "./curator.js";
|
|
53
|
-
import { SeenText, collapseRuns,
|
|
53
|
+
import { SeenText, collapseRuns, savedResultIn, seenItems } from "./dedupe.js";
|
|
54
54
|
import { buildCompactSnapshot, compactAppendix, recallText } from "./compact.js";
|
|
55
55
|
import { formatCompaction, relevanceCompaction } from "./relevance.js";
|
|
56
56
|
import { applyLoopAction, formatLoopsForUser, formatOpenLoops, LOOP_ACTIONS, LOOP_CHARS, loopsFingerprint, loopsPath, openLoops, readLoops, updateLoops } from "./loops.js";
|
|
57
57
|
import { changePrefsStore, emptyWordCounts, evaluatePrefs, forgetPref, formatPrefs, LESSON_CHARS, prefsMessage, prefsStorePath, readPrefsStore, recordLesson, scanPreferences } from "./prefs.js";
|
|
58
|
+
import { armFor, isSuppressed, steerDeliveryKind, TRIAL_ENTRY_TYPE, trialText } from "./trial.js";
|
|
58
59
|
import { formatWake, newReports, reportLabel, triageReport, WakePolicy } from "./subagent.js";
|
|
59
60
|
import { actionDetails, causeDetails, doneDetails, proseDetails, rulesDetails, runawayDetails, stuckDetails, Trace } from "./trace.js";
|
|
60
61
|
import { TraceFile, judgmentsState, traceDir, traceFilePath } from "./trace-file.js";
|
|
@@ -340,6 +341,9 @@ export function guardCurrentSections(result) {
|
|
|
340
341
|
config.subagent = { ...config.subagent, observer: false };
|
|
341
342
|
if (typeof config.widget.subagent !== "string")
|
|
342
343
|
config.widget = { ...config.widget, subagent: DEFAULT_TEMPLATES.subagent };
|
|
344
|
+
// The trial section is new and off by default: a stale shape module hands the config over without it, and no delivery changes.
|
|
345
|
+
if (typeof config.trial !== "object" || config.trial === null || typeof config.trial.silentHalf !== "boolean")
|
|
346
|
+
config.trial = { silentHalf: false };
|
|
343
347
|
return { config, missing };
|
|
344
348
|
}
|
|
345
349
|
/** Native Pi registration; importing the root library does not load this module. */
|
|
@@ -443,6 +447,38 @@ export default function wardenExtension(host) {
|
|
|
443
447
|
let holdLog;
|
|
444
448
|
/** The session id of the open session; every hold row of it carries it. */
|
|
445
449
|
let activeSessionId;
|
|
450
|
+
/** The silent-half trial arm of the open session: "live" unless the user turned the trial on and the id hashed silent. */
|
|
451
|
+
let trialArm = "live";
|
|
452
|
+
/**
|
|
453
|
+
* One line of the trial record, appended as a custom session entry: Pi keeps it in the session log and never adds it
|
|
454
|
+
* to the model's context. A host without the entry API keeps the session running; the failure is local and loud once.
|
|
455
|
+
*/
|
|
456
|
+
let trialRecordFailed = false;
|
|
457
|
+
const appendTrial = (record) => {
|
|
458
|
+
try {
|
|
459
|
+
if (typeof pi.appendEntry === "function")
|
|
460
|
+
pi.appendEntry(TRIAL_ENTRY_TYPE, record);
|
|
461
|
+
else if (!trialRecordFailed) {
|
|
462
|
+
trialRecordFailed = true;
|
|
463
|
+
console.warn("pi-warden: the host has no appendEntry; the trial record cannot be written");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
if (trialRecordFailed)
|
|
468
|
+
return;
|
|
469
|
+
trialRecordFailed = true;
|
|
470
|
+
console.warn("pi-warden: trial record failed:", error instanceof Error ? error.message : String(error));
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
/** Whether the silent arm withholds this delivery; a withheld delivery is recorded, redacted and clipped to 300 chars. */
|
|
474
|
+
const trialHolds = (kind, guard, text) => {
|
|
475
|
+
if (trialArm !== "silent" || !isSuppressed(kind))
|
|
476
|
+
return false;
|
|
477
|
+
appendTrial({ v: 1, record: "suppressed", kind, guard, at: Date.now(), text: trialText(redact(text)) });
|
|
478
|
+
return true;
|
|
479
|
+
};
|
|
480
|
+
/** The same test for a delivery whose kind is not yet narrowed; false keeps it live. */
|
|
481
|
+
const trialHoldsKind = (kind, guard, text) => trialArm === "silent" && isSuppressed(kind) ? trialHolds(kind, guard, text) : false;
|
|
446
482
|
// Local rules verdict log for this session; a write failure is silent to the agent and shown once in the trace.
|
|
447
483
|
let rulesLog;
|
|
448
484
|
// Allowed calls of the turn the user just replied to; the regret question about them rides the next action request.
|
|
@@ -609,6 +645,8 @@ export default function wardenExtension(host) {
|
|
|
609
645
|
const last = [...branch].reverse().find(entry => entry.type === "custom_message" && entry.customType === LOOPS_TYPE);
|
|
610
646
|
if (last?.content === content)
|
|
611
647
|
return;
|
|
648
|
+
if (trialHolds("loops", "loops", content))
|
|
649
|
+
return;
|
|
612
650
|
pi.sendMessage({ customType: LOOPS_TYPE, content, display: config.steerVisible });
|
|
613
651
|
}
|
|
614
652
|
catch (error) {
|
|
@@ -897,7 +935,7 @@ export default function wardenExtension(host) {
|
|
|
897
935
|
const manager = ctx.sessionManager;
|
|
898
936
|
// Compaction-aware when the host has it: text a compaction summarized is no longer in context, so nothing may point to it.
|
|
899
937
|
const entries = typeof manager.buildContextEntries === "function" ? manager.buildContextEntries() : typeof manager.getBranch === "function" ? manager.getBranch() : [];
|
|
900
|
-
seenText.sync(entries.
|
|
938
|
+
seenText.sync(seenItems(entries, ctx.model?.provider));
|
|
901
939
|
};
|
|
902
940
|
/** `text` with each run that repeats context replaced by a pointer line, or undefined when nothing repeats or the copy cannot be stored. */
|
|
903
941
|
const dedupeText = async (ctx, config, text, source) => {
|
|
@@ -1044,7 +1082,14 @@ export default function wardenExtension(host) {
|
|
|
1044
1082
|
}
|
|
1045
1083
|
stats.steers++;
|
|
1046
1084
|
steersThisRun++;
|
|
1047
|
-
const { display, ...delivery } = options ?? {
|
|
1085
|
+
const { display, kind, ...delivery } = options ?? {};
|
|
1086
|
+
// A call site that names only a kind, or nothing, keeps the old default; one that passes its own delivery options keeps them exactly.
|
|
1087
|
+
if (delivery.deliverAs === undefined && delivery.triggerTurn === undefined)
|
|
1088
|
+
delivery.deliverAs = "steer";
|
|
1089
|
+
// The silent arm records what it would have said and sends nothing, after the same budget and repeat checks as
|
|
1090
|
+
// the live arm, so both arms spend a run's steers alike.
|
|
1091
|
+
if (trialHoldsKind(kind ?? steerDeliveryKind(names[0] ?? "action"), names.join(", "), content))
|
|
1092
|
+
return false;
|
|
1048
1093
|
pi.sendMessage({ customType: `${PACKAGE_NAME}-steer`, content, display: display ?? config.steerVisible }, delivery);
|
|
1049
1094
|
if (delivery.triggerTurn)
|
|
1050
1095
|
wardenContinuation = true;
|
|
@@ -1062,6 +1107,8 @@ export default function wardenExtension(host) {
|
|
|
1062
1107
|
const notices = pendingNotices;
|
|
1063
1108
|
pendingNotices = [];
|
|
1064
1109
|
for (const notice of notices) {
|
|
1110
|
+
if (trialHolds(notice.customType === CURATOR_TYPE ? "turn-rules" : "conscience", notice.guard, notice.content))
|
|
1111
|
+
continue;
|
|
1065
1112
|
pi.sendMessage({ customType: notice.customType, content: notice.content, display: config.steerVisible }, { deliverAs: "steer" });
|
|
1066
1113
|
record(ctx, config, notice.guard, notice.delivered, notice.details);
|
|
1067
1114
|
}
|
|
@@ -1288,6 +1335,7 @@ export default function wardenExtension(host) {
|
|
|
1288
1335
|
regretCandidates = [];
|
|
1289
1336
|
const sessionId = typeof ctx.sessionManager.getSessionId === "function" ? ctx.sessionManager.getSessionId() : String(process.pid);
|
|
1290
1337
|
activeSessionId = sessionId;
|
|
1338
|
+
trialArm = "live";
|
|
1291
1339
|
holdLog = new HoldLog(holdLogPath(sessionId, new Date(), dirs));
|
|
1292
1340
|
rulesLog = new RulesLog(ctx.cwd, sessionId, { path: rulesLogPath(ctx.cwd, dirs), onFailure: message => record(ctx, loadConfig({ dirs }), "rules", "rules log write failed", [message]) });
|
|
1293
1341
|
unsubscribeTraceFile?.();
|
|
@@ -1389,6 +1437,13 @@ export default function wardenExtension(host) {
|
|
|
1389
1437
|
pi.sendMessage({ customType: `${PACKAGE_NAME}-status`, content: msg, display: true });
|
|
1390
1438
|
}
|
|
1391
1439
|
const opening = configFor(ctx);
|
|
1440
|
+
// ── Silent-half trial ──
|
|
1441
|
+
// The arm is fixed for the whole session from its id, so a resume or reload keeps it. With the setting off, no
|
|
1442
|
+
// record is written and no delivery changes; only the user's config file can turn it on.
|
|
1443
|
+
if (opening.trial.silentHalf === true) {
|
|
1444
|
+
trialArm = armFor(sessionId);
|
|
1445
|
+
appendTrial({ v: 1, record: "arm", arm: trialArm, sessionId, at: Date.now() });
|
|
1446
|
+
}
|
|
1392
1447
|
// A resumed or reloaded session already carries the message; sending it again would repeat it in the context.
|
|
1393
1448
|
const openingBranch = typeof ctx.sessionManager?.getBranch === "function" ? ctx.sessionManager.getBranch() : [];
|
|
1394
1449
|
const prefsSent = openingBranch.some(entry => entry.type === "custom_message" && entry.customType === PREFS_TYPE);
|
|
@@ -1396,7 +1451,7 @@ export default function wardenExtension(host) {
|
|
|
1396
1451
|
try {
|
|
1397
1452
|
const message = prefsMessage((await evaluatedPrefs(ctx)).items);
|
|
1398
1453
|
// Not a steer; one message per session; do not spend a steer unit.
|
|
1399
|
-
if (message)
|
|
1454
|
+
if (message && !trialHolds("prefs", "prefs", message))
|
|
1400
1455
|
pi.sendMessage({ customType: PREFS_TYPE, content: message, display: opening.steerVisible });
|
|
1401
1456
|
}
|
|
1402
1457
|
catch (error) {
|
|
@@ -1414,6 +1469,17 @@ export default function wardenExtension(host) {
|
|
|
1414
1469
|
* says why in the trace, and an answer that arrives after the run ended is dropped, also with a trace line.
|
|
1415
1470
|
*/
|
|
1416
1471
|
const curateTurnStart = (ctx, config, prompt) => {
|
|
1472
|
+
// A newer Pi also fires this event for a run a custom message starts (pi.sendMessage with triggerTurn), and that
|
|
1473
|
+
// run has no operator prompt. There is no request to judge for rule relevance, so the reminder skips with a trace
|
|
1474
|
+
// line instead of crashing on `undefined`, like the skips below.
|
|
1475
|
+
if (typeof prompt !== "string") {
|
|
1476
|
+
record(ctx, config, "rules", "warden · rules · turn start · skipped", [
|
|
1477
|
+
"trigger: before_agent_start",
|
|
1478
|
+
"skipReason: custom_trigger",
|
|
1479
|
+
"the run started from a custom message, not an operator prompt; no rule question was sent",
|
|
1480
|
+
]);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1417
1483
|
// A prompt the conscience already skips needs no rules reminder either: a short continuation or a relayed child
|
|
1418
1484
|
// report carries no work of its own, so no question is sent. The reason is traced either way.
|
|
1419
1485
|
const gated = redact(prompt).slice(0, 2000);
|
|
@@ -1453,7 +1519,7 @@ export default function wardenExtension(host) {
|
|
|
1453
1519
|
const branch = typeof ctx.sessionManager?.getBranch === "function" ? ctx.sessionManager.getBranch() : [];
|
|
1454
1520
|
const request = buildCuratorRequest(prompt, taskSpine(branch, prompt), rules);
|
|
1455
1521
|
const started = Date.now();
|
|
1456
|
-
void
|
|
1522
|
+
void askChunks(judge, chunkEvaluationRequest(request, { maxQuestions: questionLimit(judge) }), { timeoutMs: Math.min(config.timeoutMs, CURATOR_TIMEOUT_MS), ...(ctx.signal ? { signal: ctx.signal } : {}) })
|
|
1457
1523
|
.then(answer => {
|
|
1458
1524
|
const elapsedMs = Date.now() - started;
|
|
1459
1525
|
// The run this request belonged to has ended: nothing may be appended after it (a delivery must never start a turn).
|
|
@@ -1528,14 +1594,20 @@ export default function wardenExtension(host) {
|
|
|
1528
1594
|
// re-append the same text, which is what keeps it in the prompt instead of being diffed away.
|
|
1529
1595
|
const wasteOptions = event.systemPromptOptions;
|
|
1530
1596
|
if (config.enabled && config.waste.enabled && config.waste.tip && wasteOptions) {
|
|
1531
|
-
|
|
1597
|
+
// The silent arm withholds the tip and records it once, the way the live arm traces it once.
|
|
1598
|
+
const held = trialArm === "silent";
|
|
1599
|
+
if (held && !wasteTipOffered)
|
|
1600
|
+
trialHolds("waste", "waste", WASTE_TIP);
|
|
1601
|
+
if (!held)
|
|
1602
|
+
wasteOptions.appendSystemPrompt = withWasteTip(wasteOptions.appendSystemPrompt);
|
|
1532
1603
|
if (!wasteTipOffered) {
|
|
1533
1604
|
wasteTipOffered = true;
|
|
1534
|
-
|
|
1535
|
-
"
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1605
|
+
if (!held)
|
|
1606
|
+
record(ctx, config, "waste", "warden · waste · session tip · instructions_supplied", [
|
|
1607
|
+
"trigger: before_agent_start",
|
|
1608
|
+
"delivery: appended to the system prompt (appendSystemPrompt)",
|
|
1609
|
+
`tip: ${WASTE_TIP}`,
|
|
1610
|
+
]);
|
|
1539
1611
|
}
|
|
1540
1612
|
}
|
|
1541
1613
|
fullOutputs = new Map();
|
|
@@ -1578,6 +1650,17 @@ export default function wardenExtension(host) {
|
|
|
1578
1650
|
loadedSkillNames = new Set();
|
|
1579
1651
|
queuedRevision++;
|
|
1580
1652
|
clearFileIdentityCache();
|
|
1653
|
+
// The same custom-message case as the rules reminder: no operator prompt, nothing to assess. The state above
|
|
1654
|
+
// still resets, so nothing from an older prompt leaks into a later one.
|
|
1655
|
+
const prompt = event.prompt;
|
|
1656
|
+
if (typeof prompt !== "string") {
|
|
1657
|
+
record(ctx, config, "conscience", "warden · conscience · turn start · skipped", [
|
|
1658
|
+
"trigger: before_agent_start",
|
|
1659
|
+
"skipReason: custom_trigger",
|
|
1660
|
+
"the run started from a custom message, not an operator prompt; no assessment was sent",
|
|
1661
|
+
]);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1581
1664
|
const judge = judgeFor(config);
|
|
1582
1665
|
// Get the resolved skill catalog from the event's system prompt options (spec §3 rule 1)
|
|
1583
1666
|
const skills = event.systemPromptOptions?.skills ?? [];
|
|
@@ -1590,17 +1673,17 @@ export default function wardenExtension(host) {
|
|
|
1590
1673
|
toolInfos = [];
|
|
1591
1674
|
}
|
|
1592
1675
|
// Detect explicit /skill:name invocation (spec §3 rule 5)
|
|
1593
|
-
const explicitSkillMatch =
|
|
1676
|
+
const explicitSkillMatch = prompt.match(/\/skill:([\w-]+)/);
|
|
1594
1677
|
if (explicitSkillMatch) {
|
|
1595
1678
|
record(ctx, config, "conscience", `explicit skill invocation: ${explicitSkillMatch[1]}`, ["trigger: input", `explicit_skill: ${explicitSkillMatch[1]}`]);
|
|
1596
1679
|
}
|
|
1597
1680
|
else {
|
|
1598
1681
|
// Redact and truncate prompt to 2000 chars (spec §6 payload limit)
|
|
1599
|
-
const redactedPrompt = redact(
|
|
1600
|
-
const truncationRecorded =
|
|
1682
|
+
const redactedPrompt = redact(prompt).slice(0, 2000);
|
|
1683
|
+
const truncationRecorded = prompt.length > 2000 ? [`prompt truncated from ${prompt.length} to 2000 chars`] : [];
|
|
1601
1684
|
// Build recent context from session branch (up to 4 messages, 500 chars each)
|
|
1602
1685
|
const branch = typeof ctx.sessionManager?.getBranch === "function" ? ctx.sessionManager.getBranch() : [];
|
|
1603
|
-
const spine = taskSpine(branch,
|
|
1686
|
+
const spine = taskSpine(branch, prompt);
|
|
1604
1687
|
const recentMessages = [];
|
|
1605
1688
|
for (const entry of branch.slice(-6)) {
|
|
1606
1689
|
if (recentMessages.length >= 4)
|
|
@@ -1660,7 +1743,7 @@ export default function wardenExtension(host) {
|
|
|
1660
1743
|
// The evaluate result carries the model that answered; the activation gate compares the policy's model against
|
|
1661
1744
|
// it. Empty until the first evaluate call, and an empty model never matches a policy, so the gate fails closed.
|
|
1662
1745
|
let judgeModel = "";
|
|
1663
|
-
const judgeAdapter = judge ? { evaluate: async (req) => { const r = await judge.evaluate(req); judgeModel = r.model; return { answers: r.answers }; } } : undefined;
|
|
1746
|
+
const judgeAdapter = judge ? { limits: judge.limits, evaluate: async (req) => { const r = await judge.evaluate(req); judgeModel = r.model; return { answers: r.answers }; } } : undefined;
|
|
1664
1747
|
const result = await assess(redactedPrompt, recentContext, skills, toolInfos, activeSkills, suppliedSkills, { judge: judgeAdapter, judgmentsOff: judgmentsOffReason(config), config: config.conscience, sharedTimeoutMs: config.timeoutMs, now: () => Date.now(), globalIndex: globalIndexFile, projectIndex: projectIndexFile }, spine);
|
|
1665
1748
|
// Check generation and run after await: a result that belongs to an ended or replaced run must not deliver.
|
|
1666
1749
|
if (conscienceGeneration !== myGeneration || myPrompt !== promptEpoch || !agentRunActive) {
|
|
@@ -1814,7 +1897,7 @@ export default function wardenExtension(host) {
|
|
|
1814
1897
|
toolInfos = [];
|
|
1815
1898
|
}
|
|
1816
1899
|
const activeSkills = cachedSkills.map(s => s.name);
|
|
1817
|
-
const judgeAdapter = judge ? { evaluate: async (req) => { const r = await judge.evaluate(req); return { answers: r.answers }; } } : undefined;
|
|
1900
|
+
const judgeAdapter = judge ? { limits: judge.limits, evaluate: async (req) => { const r = await judge.evaluate(req); return { answers: r.answers }; } } : undefined;
|
|
1818
1901
|
try {
|
|
1819
1902
|
const result = await assess(redactedText, "", cachedSkills, toolInfos, activeSkills, [], { judge: judgeAdapter, config: config.conscience, sharedTimeoutMs: config.timeoutMs, now: () => Date.now(), globalIndex: globalIndexFile, projectIndex: projectIndexFile }, spine);
|
|
1820
1903
|
if (conscienceGeneration !== myGeneration)
|
|
@@ -1917,7 +2000,7 @@ export default function wardenExtension(host) {
|
|
|
1917
2000
|
const redactedPrompt = redact(prompt ?? "").slice(0, 2000);
|
|
1918
2001
|
const spine = taskSpine(typeof ctx.sessionManager?.getBranch === "function" ? ctx.sessionManager.getBranch() : [], prompt ?? undefined);
|
|
1919
2002
|
const activeSkills = cachedSkills.map(s => s.name);
|
|
1920
|
-
const judgeAdapter = judge ? { evaluate: async (req) => { const r = await judge.evaluate(req); return { answers: r.answers }; } } : undefined;
|
|
2003
|
+
const judgeAdapter = judge ? { limits: judge.limits, evaluate: async (req) => { const r = await judge.evaluate(req); return { answers: r.answers }; } } : undefined;
|
|
1921
2004
|
try {
|
|
1922
2005
|
const result = await assess(redactedPrompt, "", cachedSkills, toolInfos, activeSkills, [], { judge: judgeAdapter, config: config.conscience, sharedTimeoutMs: config.timeoutMs, now: () => Date.now(), globalIndex: globalIndexFile, projectIndex: projectIndexFile }, spine);
|
|
1923
2006
|
if (conscienceGeneration !== myGeneration)
|
|
@@ -2091,6 +2174,8 @@ export default function wardenExtension(host) {
|
|
|
2091
2174
|
}
|
|
2092
2175
|
const deliveryVerdict = agentDeliveryVerdict(verdict);
|
|
2093
2176
|
const deliveryReasons = deliveryVerdict.reasons;
|
|
2177
|
+
// The agent reads reasons without their scores; the trace, holds, and learning records keep them.
|
|
2178
|
+
const agentReasons = deliveryReasons.map(scoreFreeReason);
|
|
2094
2179
|
if (regretCandidates.length && verdict.judgment?.regretted !== undefined) {
|
|
2095
2180
|
settleRegret(config, { regretted: regretsAt(verdict.judgment.regretted), target: verdict.judgment.regretTarget, probability: verdict.judgment.regretted, via: "jev" });
|
|
2096
2181
|
}
|
|
@@ -2279,7 +2364,7 @@ export default function wardenExtension(host) {
|
|
|
2279
2364
|
}
|
|
2280
2365
|
notes.push(...pathNotes);
|
|
2281
2366
|
if (notes.length) {
|
|
2282
|
-
const delivered = steer(config, [...noteGuards], notes.join("\n\n"));
|
|
2367
|
+
const delivered = steer(config, [...noteGuards], notes.join("\n\n"), { kind: "action-notes" });
|
|
2283
2368
|
const notePath = verdict.summary.path ?? shell?.writes[0]?.path;
|
|
2284
2369
|
if (delivered)
|
|
2285
2370
|
watchSteer(ctx, config, noteKinds, notePath ? { path: notePath } : {});
|
|
@@ -2291,7 +2376,7 @@ export default function wardenExtension(host) {
|
|
|
2291
2376
|
const warnSteer = (reasons) => {
|
|
2292
2377
|
if (!reasons.length || !adaptiveSend(ctx, config, "warn-headless"))
|
|
2293
2378
|
return false;
|
|
2294
|
-
const sent = steer(config, "action", `pi-warden: this ${event.toolName} call ran with a warning (${reasons.join("; ")}). Nobody sees this in a headless run, so it is on you: if the flagged risk is expected, continue; otherwise fix it or ask the user before building on it
|
|
2379
|
+
const sent = steer(config, "action", `pi-warden: this ${event.toolName} call ran with a warning (${reasons.map(scoreFreeReason).join("; ")}). Nobody sees this in a headless run, so it is on you: if the flagged risk is expected, continue; otherwise fix it or ask the user before building on it.`, { kind: "warn-headless" });
|
|
2295
2380
|
if (sent)
|
|
2296
2381
|
watchSteer(ctx, config, ["warn-headless"]);
|
|
2297
2382
|
return sent;
|
|
@@ -2315,7 +2400,7 @@ export default function wardenExtension(host) {
|
|
|
2315
2400
|
if (ctx.hasUI && config.notices)
|
|
2316
2401
|
ctx.ui.notify(`warden · blocked ${event.toolName}: ${verdict.reasons.join("; ")}`, "error");
|
|
2317
2402
|
notifyDesktop(ctx, config, `Blocked ${event.toolName}: ${verdict.reasons.join("; ")}. A deny rule matched; the call never ran.`);
|
|
2318
|
-
return { block: true, reason: `pi-warden blocked this ${event.toolName} call (${
|
|
2403
|
+
return { block: true, reason: `pi-warden blocked this ${event.toolName} call (${agentReasons.join("; ")}). A deny rule matched; this command is not allowed to run. Ask the user if this is genuinely required.` };
|
|
2319
2404
|
}
|
|
2320
2405
|
if (verdict.level !== "confirm") {
|
|
2321
2406
|
deliverNotes(true);
|
|
@@ -2323,7 +2408,7 @@ export default function wardenExtension(host) {
|
|
|
2323
2408
|
return undefined;
|
|
2324
2409
|
}
|
|
2325
2410
|
const reasons = verdict.reasons.join("; ");
|
|
2326
|
-
const deliveredReasons =
|
|
2411
|
+
const deliveredReasons = agentReasons.join("; ");
|
|
2327
2412
|
// A user-defined confirm rule prompts the user in every mode, advise included: the operator wrote the rule to be
|
|
2328
2413
|
// asked. Advise mode keeps Jev holds advisory; it does not soften a prompt the operator asked for by name.
|
|
2329
2414
|
// The action defaults to dialog at parse time (the reason one writes such a rule); hold restores steer semantics.
|
|
@@ -2533,7 +2618,8 @@ export default function wardenExtension(host) {
|
|
|
2533
2618
|
storedPath = earlier.path ?? await saveOutput(text);
|
|
2534
2619
|
const replacement = `${duplicateNote(text, earlier.tool)}\n\n${recallInstruction(recallTool, storedPath)}`;
|
|
2535
2620
|
const bytesSaved = Buffer.byteLength(text) - Buffer.byteLength(replacement);
|
|
2536
|
-
|
|
2621
|
+
// The silent arm records the duplicate note and leaves the tool result whole.
|
|
2622
|
+
if (bytesSaved > 0 && !trialHolds("context-compression", "context", replacement)) {
|
|
2537
2623
|
content = content.map(part => part.type === "text" ? { ...part, text: replacement } : part);
|
|
2538
2624
|
ledger.duplicate(bytesSaved);
|
|
2539
2625
|
ledger.remember(key, earlier.tool, storedPath);
|
|
@@ -2591,6 +2677,9 @@ export default function wardenExtension(host) {
|
|
|
2591
2677
|
}
|
|
2592
2678
|
if (!compressed && verdict.confidence !== undefined)
|
|
2593
2679
|
recordKeptWhole(config, event.toolName, verdict, `text block ${textIndex} of ${blockVerdicts.length}: `);
|
|
2680
|
+
// The silent arm records the compressed block and leaves the original, with its security banner, in place.
|
|
2681
|
+
if (compressed && replacement && trialHolds("context-compression", "context", replacement))
|
|
2682
|
+
replacement = undefined;
|
|
2594
2683
|
if (!replacement && blockNotice)
|
|
2595
2684
|
replacement = `${blockNotice}\n\n${blockText}\n\n${blockNotice}`;
|
|
2596
2685
|
if (replacement)
|
|
@@ -2609,7 +2698,9 @@ export default function wardenExtension(host) {
|
|
|
2609
2698
|
const replacement = `${filtered?.ok ? filtered.text : excerpt}\n\n${recallInstruction(recallTool, path)}`;
|
|
2610
2699
|
const bytesSaved = Buffer.byteLength(text) - Buffer.byteLength(replacement) - (notice ? Buffer.byteLength(notice) * 2 + 4 : 0);
|
|
2611
2700
|
if (bytesSaved > 0) {
|
|
2612
|
-
|
|
2701
|
+
// The silent arm records the excerpt and leaves the tool result whole; the security banner is placed below.
|
|
2702
|
+
if (!trialHolds("context-compression", "context", replacement))
|
|
2703
|
+
content = content.map(part => part.type === "text" ? { ...part, text: replacement } : part);
|
|
2613
2704
|
compressed = true;
|
|
2614
2705
|
ledger.record(path, bytesSaved, { tool: event.toolName, bytes: Buffer.byteLength(text), kind: filtered?.ok ? "filtered" : generic ? "excerpt" : "parser", ...(filtered?.ok ? { keptChars: filtered.keptChars } : {}) });
|
|
2615
2706
|
storedPath = path;
|
|
@@ -2631,11 +2722,12 @@ export default function wardenExtension(host) {
|
|
|
2631
2722
|
if (!compressed && !ctx.signal?.aborted && output.confidence !== undefined)
|
|
2632
2723
|
recordKeptWhole(config, event.toolName, output);
|
|
2633
2724
|
}
|
|
2634
|
-
// A recall brings the stored text back on purpose, and a duplicate note has nothing left to cut.
|
|
2635
|
-
|
|
2725
|
+
// A recall brings the stored text back on purpose, and a duplicate note has nothing left to cut. A result the
|
|
2726
|
+
// provider saved to a file never reached the model, so a read of that file is never a repeat.
|
|
2727
|
+
if (config.context.enabled && config.context.dedupeRuns && !earlier && !recallRead && !savedResultIn(JSON.stringify(event.input), ctx.model?.provider) && !ctx.signal?.aborted) {
|
|
2636
2728
|
syncSeen(ctx);
|
|
2637
2729
|
const deduped = await dedupeContent(ctx, config, content, event.toolName);
|
|
2638
|
-
if (deduped)
|
|
2730
|
+
if (deduped && !trialHolds("context-compression", "context", `repeated runs collapsed in a ${event.toolName} result`))
|
|
2639
2731
|
content = deduped;
|
|
2640
2732
|
}
|
|
2641
2733
|
if (key && !earlier)
|
|
@@ -2675,7 +2767,7 @@ export default function wardenExtension(host) {
|
|
|
2675
2767
|
const saved = await saveOutput(text);
|
|
2676
2768
|
const diffNote = stuckDiff(prevEntry.text, text, { diffLimit: config.stuck.diffLimit, tailLimit: config.stuck.tailLimit, fullPath: saved });
|
|
2677
2769
|
const currentLen = Buffer.byteLength(content.map(part => part.type === "text" ? part.text : "").join("\n"));
|
|
2678
|
-
if (Buffer.byteLength(diffNote) < currentLen) {
|
|
2770
|
+
if (Buffer.byteLength(diffNote) < currentLen && !trialHolds("context-compression", "context", diffNote)) {
|
|
2679
2771
|
content = [{ type: "text", text: diffNote }, ...content.filter(part => part.type !== "text")];
|
|
2680
2772
|
}
|
|
2681
2773
|
fullOutputs.set(currentKey, { text, path: saved });
|
|
@@ -2689,7 +2781,7 @@ export default function wardenExtension(host) {
|
|
|
2689
2781
|
// Byte-identical outputs: one-line note reusing the existing saved path.
|
|
2690
2782
|
const diffNote = stuckDiff(text, text, { diffLimit: config.stuck.diffLimit, tailLimit: config.stuck.tailLimit, fullPath: prevEntry.path });
|
|
2691
2783
|
const currentLen = Buffer.byteLength(content.map(part => part.type === "text" ? part.text : "").join("\n"));
|
|
2692
|
-
if (Buffer.byteLength(diffNote) < currentLen) {
|
|
2784
|
+
if (Buffer.byteLength(diffNote) < currentLen && !trialHolds("context-compression", "context", diffNote)) {
|
|
2693
2785
|
content = [{ type: "text", text: diffNote }, ...content.filter(part => part.type !== "text")];
|
|
2694
2786
|
}
|
|
2695
2787
|
}
|
|
@@ -2709,6 +2801,8 @@ export default function wardenExtension(host) {
|
|
|
2709
2801
|
const withWaste = (current) => {
|
|
2710
2802
|
if (!wasteNudge)
|
|
2711
2803
|
return current;
|
|
2804
|
+
if (trialHolds("waste", "waste", wasteNudge.text))
|
|
2805
|
+
return current;
|
|
2712
2806
|
const base = current?.content ?? event.content;
|
|
2713
2807
|
const lastText = base.reduce((last, part, index) => part.type === "text" ? index : last, -1);
|
|
2714
2808
|
if (lastText < 0)
|
|
@@ -2798,10 +2892,12 @@ export default function wardenExtension(host) {
|
|
|
2798
2892
|
const source = message.role === "custom" ? `${message.customType} message` : "user message";
|
|
2799
2893
|
if (typeof message.content === "string") {
|
|
2800
2894
|
const text = await dedupeText(ctx, config, message.content, source);
|
|
2801
|
-
|
|
2895
|
+
if (text === undefined || trialHolds("context-compression", "context", text))
|
|
2896
|
+
return undefined;
|
|
2897
|
+
return { message: { ...message, content: text } };
|
|
2802
2898
|
}
|
|
2803
2899
|
const content = await dedupeContent(ctx, config, message.content, source);
|
|
2804
|
-
return content ? { message: { ...message, content } } : undefined;
|
|
2900
|
+
return content && !trialHolds("context-compression", "context", `repeated runs collapsed in a ${source}`) ? { message: { ...message, content } } : undefined;
|
|
2805
2901
|
});
|
|
2806
2902
|
// The agent has caught up and Pi will not continue on its own: the one moment a wake costs the user nothing.
|
|
2807
2903
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
@@ -2843,7 +2939,8 @@ export default function wardenExtension(host) {
|
|
|
2843
2939
|
if (!appendix)
|
|
2844
2940
|
return;
|
|
2845
2941
|
// Not a steer; one message per compaction; do not spend a steer unit.
|
|
2846
|
-
|
|
2942
|
+
if (!trialHolds("compact-evidence", "context", appendix))
|
|
2943
|
+
pi.sendMessage({ customType: `${PACKAGE_NAME}-compact-evidence`, content: appendix, display: config.steerVisible });
|
|
2847
2944
|
}
|
|
2848
2945
|
catch (error) {
|
|
2849
2946
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2902,6 +2999,9 @@ export default function wardenExtension(host) {
|
|
|
2902
2999
|
// Pi checks the signal only after the hook returns; an aborted compaction is not replaced.
|
|
2903
3000
|
if (event.signal.aborted)
|
|
2904
3001
|
return fallback("aborted", [counts], stats);
|
|
3002
|
+
// The silent arm records what the compaction would have kept and lets Pi's own summary run.
|
|
3003
|
+
if (trialHolds("context-compression", "context", `relevance compaction kept ${stats.kept} of ${stats.candidates}`))
|
|
3004
|
+
return undefined;
|
|
2905
3005
|
compactions.replaced++;
|
|
2906
3006
|
compactions.last = stats;
|
|
2907
3007
|
record(ctx, config, "context", `warden · context · relevance compaction · kept ${stats.kept} of ${stats.candidates}`, [
|
|
@@ -3026,7 +3126,7 @@ export default function wardenExtension(host) {
|
|
|
3026
3126
|
const spine = taskSpine(typeof ctx.sessionManager?.getBranch === "function" ? ctx.sessionManager.getBranch() : [], prompt ?? undefined);
|
|
3027
3127
|
const activeSkills = cachedSkills.map(s => s.name);
|
|
3028
3128
|
let judgeModel = "";
|
|
3029
|
-
const judgeAdapter = judge ? { evaluate: async (req) => { const r = await judge.evaluate(req); judgeModel = r.model; return { answers: r.answers }; } } : undefined;
|
|
3129
|
+
const judgeAdapter = judge ? { limits: judge.limits, evaluate: async (req) => { const r = await judge.evaluate(req); judgeModel = r.model; return { answers: r.answers }; } } : undefined;
|
|
3030
3130
|
try {
|
|
3031
3131
|
const result = await assess(redactedPrompt, "", cachedSkills, toolInfos, activeSkills, [], { judge: judgeAdapter, config: config.conscience, sharedTimeoutMs: config.timeoutMs, now: () => Date.now(), globalIndex: globalIndexFile, projectIndex: projectIndexFile }, spine);
|
|
3032
3132
|
if (conscienceGeneration !== myGeneration)
|
|
@@ -3913,8 +4013,9 @@ export default function wardenExtension(host) {
|
|
|
3913
4013
|
// Accept both the full format and entries-only; warden always writes the final metadata.
|
|
3914
4014
|
let globalResult = rawGlobal ? validateIndex(rawGlobal) : { file: undefined, rejections: [] };
|
|
3915
4015
|
let projectResult = rawProject ? validateIndex(rawProject) : { file: undefined, rejections: [] };
|
|
3916
|
-
// Entries-only fallback: read raw JSON and extract entries array
|
|
3917
|
-
|
|
4016
|
+
// Entries-only fallback: read raw JSON and extract entries array. It must not depend on rawGlobal/rawProject:
|
|
4017
|
+
// the strict read returns nothing for the entries-only shape the index prompt asks for.
|
|
4018
|
+
if (!globalResult.file) {
|
|
3918
4019
|
try {
|
|
3919
4020
|
const parsed = JSON.parse(readFileSync(globalPath, "utf8"));
|
|
3920
4021
|
if (parsed && typeof parsed === "object" && Array.isArray(parsed.entries)) {
|
|
@@ -3927,7 +4028,7 @@ export default function wardenExtension(host) {
|
|
|
3927
4028
|
record(ctx, config, "conscience", `global entries-only parse: ${msg}`, ["trigger: warden_index"]);
|
|
3928
4029
|
}
|
|
3929
4030
|
}
|
|
3930
|
-
if (!projectResult.file
|
|
4031
|
+
if (!projectResult.file) {
|
|
3931
4032
|
try {
|
|
3932
4033
|
const parsed = JSON.parse(readFileSync(projectPath, "utf8"));
|
|
3933
4034
|
if (parsed && typeof parsed === "object" && Array.isArray(parsed.entries)) {
|
|
@@ -3943,9 +4044,9 @@ export default function wardenExtension(host) {
|
|
|
3943
4044
|
}
|
|
3944
4045
|
// Collect rejections from both files
|
|
3945
4046
|
const allRejections = [];
|
|
3946
|
-
if (
|
|
4047
|
+
if (!globalResult.file)
|
|
3947
4048
|
allRejections.push(...globalResult.rejections.map(r => `global index: entry ${r}`));
|
|
3948
|
-
if (
|
|
4049
|
+
if (!projectResult.file)
|
|
3949
4050
|
allRejections.push(...projectResult.rejections.map(r => `project index: entry ${r}`));
|
|
3950
4051
|
if (allRejections.length > 0) {
|
|
3951
4052
|
report(`Index rejected:\n${allRejections.map(r => ` ${r}`).join("\n")}\nOld files kept.`, "warning");
|
package/dist/filter.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { JsonValue
|
|
1
|
+
import type { JsonValue } from "pi-typesafe";
|
|
2
2
|
import type { JudgmentsOffReason } from "./backend.js";
|
|
3
3
|
import type { FilterConfig } from "./config.js";
|
|
4
4
|
import type { TaskSpine } from "./shape.js";
|
|
5
|
+
import type { LimitedJudge } from "./rules.js";
|
|
5
6
|
/**
|
|
6
7
|
* Context filter (beta, off by default). Where the saver would keep only the blind head/diagnostic/tail excerpt, Jev
|
|
7
8
|
* scores line-bounded chunks of the output for the agent's current task, and code keeps the chunks that at least
|
|
@@ -15,7 +16,7 @@ export type FilterFallback = JudgmentsOffReason | "cooldown" | "timeout" | "erro
|
|
|
15
16
|
export declare const FILTER_TAIL_CHARS = 1000;
|
|
16
17
|
/** pi-typesafe refuses a request body over 64 KiB of JSON; the margin covers the model field and normalisation. */
|
|
17
18
|
export declare const FILTER_REQUEST_BYTES = 60000;
|
|
18
|
-
/** pi-typesafe's question limit per request. */
|
|
19
|
+
/** pi-typesafe's question limit per request; a backend with a lower one gets its own. */
|
|
19
20
|
export declare const FILTER_MAX_QUESTIONS = 32;
|
|
20
21
|
export declare const FILTER_NOTE = "Passages selected for the current task; omitted text is in the full-output file.";
|
|
21
22
|
export interface FilterInput {
|
|
@@ -30,7 +31,7 @@ export interface FilterInput {
|
|
|
30
31
|
}
|
|
31
32
|
export interface FilterOptions {
|
|
32
33
|
config: FilterConfig;
|
|
33
|
-
judge:
|
|
34
|
+
judge: LimitedJudge;
|
|
34
35
|
signal?: AbortSignal | undefined;
|
|
35
36
|
}
|
|
36
37
|
export type FilterResult = {
|
|
@@ -64,7 +65,7 @@ export declare function filterState(input: FilterInput): {
|
|
|
64
65
|
[key: string]: JsonValue;
|
|
65
66
|
};
|
|
66
67
|
/** As few requests as the size and question limits allow; every chunk lands in exactly one, named `c<index+1>`. */
|
|
67
|
-
export declare function buildFilterBatches(chunks: readonly string[], input: FilterInput, maxBytes?: number): Batch[];
|
|
68
|
+
export declare function buildFilterBatches(chunks: readonly string[], input: FilterInput, maxBytes?: number, maxQuestions?: number): Batch[];
|
|
68
69
|
/**
|
|
69
70
|
* Chunks scoring at least `minScore`, in original order, within `maxKeptChars` including the tail. When more qualify
|
|
70
71
|
* than fit, the highest scores win and the original order is restored. Returns the kept text with gap markers, or
|
package/dist/filter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ask, score } from "pi-typesafe";
|
|
1
|
+
import { ask, DEFAULT_BACKEND_LIMITS, score } from "pi-typesafe";
|
|
2
2
|
import { redact } from "./redact.js";
|
|
3
3
|
/**
|
|
4
4
|
* Context filter (beta, off by default). Where the saver would keep only the blind head/diagnostic/tail excerpt, Jev
|
|
@@ -16,7 +16,7 @@ export const FILTER_RUBRIC = [
|
|
|
16
16
|
export const FILTER_TAIL_CHARS = 1000;
|
|
17
17
|
/** pi-typesafe refuses a request body over 64 KiB of JSON; the margin covers the model field and normalisation. */
|
|
18
18
|
export const FILTER_REQUEST_BYTES = 60_000;
|
|
19
|
-
/** pi-typesafe's question limit per request. */
|
|
19
|
+
/** pi-typesafe's question limit per request; a backend with a lower one gets its own. */
|
|
20
20
|
export const FILTER_MAX_QUESTIONS = 32;
|
|
21
21
|
export const FILTER_NOTE = "Passages selected for the current task; omitted text is in the full-output file.";
|
|
22
22
|
/** Chunks at line boundaries near `size`; a line is split only when it alone is longer than `size`. Joined, they are `text`. */
|
|
@@ -57,7 +57,7 @@ export function filterState(input) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
/** As few requests as the size and question limits allow; every chunk lands in exactly one, named `c<index+1>`. */
|
|
60
|
-
export function buildFilterBatches(chunks, input, maxBytes = FILTER_REQUEST_BYTES) {
|
|
60
|
+
export function buildFilterBatches(chunks, input, maxBytes = FILTER_REQUEST_BYTES, maxQuestions = FILTER_MAX_QUESTIONS) {
|
|
61
61
|
const base = filterState(input);
|
|
62
62
|
const batches = [];
|
|
63
63
|
let batch;
|
|
@@ -65,7 +65,7 @@ export function buildFilterBatches(chunks, input, maxBytes = FILTER_REQUEST_BYTE
|
|
|
65
65
|
for (const [index, chunk] of chunks.entries()) {
|
|
66
66
|
const id = `c${index + 1}`;
|
|
67
67
|
const field = redact(chunk);
|
|
68
|
-
if (batch && batch.ids.length < FILTER_MAX_QUESTIONS) {
|
|
68
|
+
if (batch && batch.ids.length < Math.min(FILTER_MAX_QUESTIONS, maxQuestions)) {
|
|
69
69
|
const next = { state: { ...batch.state, [id]: field }, questions: { ...batch.questions, [id]: question(id) }, ids: [...batch.ids, index] };
|
|
70
70
|
if (size(next) <= maxBytes) {
|
|
71
71
|
batch = next;
|
|
@@ -149,7 +149,7 @@ export function filteredHeader(text) {
|
|
|
149
149
|
export async function filterOutput(text, input, options) {
|
|
150
150
|
const started = Date.now();
|
|
151
151
|
const chunks = chunkLines(text, options.config.chunkChars);
|
|
152
|
-
const batches = buildFilterBatches(chunks, input);
|
|
152
|
+
const batches = buildFilterBatches(chunks, input, FILTER_REQUEST_BYTES, options.judge.limits?.maxQuestions ?? DEFAULT_BACKEND_LIMITS.maxQuestions);
|
|
153
153
|
const results = await Promise.all(batches.map(batch => ask(options.judge, { state: batch.state, questions: batch.questions }, { timeoutMs: options.config.timeoutMs, ...(options.signal ? { signal: options.signal } : {}) })));
|
|
154
154
|
const elapsedMs = Date.now() - started;
|
|
155
155
|
const base = { chunks: chunks.length, requests: batches.length, elapsedMs };
|
package/dist/guard.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Stats } from "node:fs";
|
|
|
2
2
|
import type { IntegrationErrorCode, Judge, Questions } from "pi-typesafe";
|
|
3
3
|
import type { ActionGuardConfig, ArmingRule, CommandRule, LargeOutputConfig, PathRule, RulesConfig, SecurityConfig, SlopGuardConfig } from "./config.js";
|
|
4
4
|
import type { TaskSpine } from "./shape.js";
|
|
5
|
-
import type { RulesSourceConfig } from "./rules.js";
|
|
5
|
+
import type { LimitedJudge, RulesSourceConfig } from "./rules.js";
|
|
6
6
|
import type { HostDirs } from "./host-dirs.js";
|
|
7
7
|
import { type ScriptSource } from "./script-bodies.js";
|
|
8
8
|
export type Level = "allow" | "warn" | "confirm" | "deny";
|
|
@@ -198,7 +198,7 @@ export type { Judge } from "pi-typesafe";
|
|
|
198
198
|
export interface EvaluateOptions {
|
|
199
199
|
config: ActionGuardConfig;
|
|
200
200
|
/** Omit to run offline pattern checks only (no consent, no network). */
|
|
201
|
-
judge?:
|
|
201
|
+
judge?: LimitedJudge | undefined;
|
|
202
202
|
signal?: AbortSignal | undefined;
|
|
203
203
|
/** Adds quality questions for write/edit content to the same request. */
|
|
204
204
|
slop?: SlopGuardConfig | undefined;
|
|
@@ -268,6 +268,14 @@ export interface ScannedCommand {
|
|
|
268
268
|
* (`python3 - <<EOF`) are kept when the script calls out to a shell or process API.
|
|
269
269
|
*/
|
|
270
270
|
export declare function stripDataText(command: string): ScannedCommand;
|
|
271
|
+
/**
|
|
272
|
+
* The command text Jev and the pattern floor read: shell comments removed, and each backslash-newline the shell removes
|
|
273
|
+
* joined (outside quotes, in double quotes and `${…}`, and in a heredoc body with an unquoted delimiter); quotes,
|
|
274
|
+
* escapes, heredoc bodies, `$#`, `${#…}`, and a `#` mid-word kept. Quote state carries across lines. A construct the
|
|
275
|
+
* scan does not model (backticks, arithmetic, `case` inside `$(…)`, quotes inside `${…}`), or an unclosed quote,
|
|
276
|
+
* returns the command unchanged.
|
|
277
|
+
*/
|
|
278
|
+
export declare function stripShellComments(command: string): string;
|
|
271
279
|
/**
|
|
272
280
|
* Real paths this call moves or links data into, when the destination lies strictly inside a volatile temp root or a
|
|
273
281
|
* scratch root the host declared. The session records them, so a later `rm` of a path equal to, inside, or above one is
|
|
@@ -681,9 +689,11 @@ export declare function largeOutputSteer(verdict: Verdict): string;
|
|
|
681
689
|
export declare function largeOutputNotice(verdict: Verdict, steered: Set<string>): string | undefined;
|
|
682
690
|
/** Offline stand-in for the approval question when TypeSafe is not available. */
|
|
683
691
|
export declare function textApproves(task: string | undefined): boolean;
|
|
692
|
+
/** A reason with its Jev score removed, for text the agent reads: the words stay, the number goes. */
|
|
693
|
+
export declare function scoreFreeReason(reason: string): string;
|
|
684
694
|
/**
|
|
685
695
|
* The text the agent receives when a call is held. It explains the judgment and the two acceptable next moves,
|
|
686
|
-
* so the model re-plans instead of retrying. Contains no command text (the model already has it) and no secrets.
|
|
696
|
+
* so the model re-plans instead of retrying. Contains no command text (the model already has it), no scores, and no secrets.
|
|
687
697
|
*/
|
|
688
698
|
export declare function steerReason(verdict: Verdict, options: {
|
|
689
699
|
canApprove: boolean;
|