pi-warden 0.5.2 → 0.5.3
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 +2 -0
- package/dist/extension.js +19 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/saver.d.ts +37 -0
- package/dist/saver.js +55 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,6 +96,8 @@ Written code gets a `security_risk` question in the existing action request: har
|
|
|
96
96
|
|
|
97
97
|
For a single text block of at least `context.tailMinChars`, the same output request asks Jev to choose `all`, `errors_and_summary`, or `summary_only`. The full result stays when P(keep all) exceeds `1 - context.confidence` (default: keep when Jev gives `all` more than 0.2), when the probability is missing, or on any request error. Code—not Jev—keeps bounded diagnostic/head/tail excerpts. An exact copy is written to an owner-only temporary directory before replacement; the excerpt links to it. If storage fails, the original stays. Multiple text blocks keep their layout and are not compressed. Trace and widget show the actual bytes saved. No extra steer is persisted for compression alone.
|
|
98
98
|
|
|
99
|
+
**Measuring it.** `/warden status` keeps a session ledger so the saver's value is a number, not a claim: how many outputs were large enough to consider, how many Jev compressed, the bytes removed (≈ tokens at 4 bytes each), the *token-turns spared* (removed tokens × every later turn, which is what a prompt cache would otherwise have re-read), and **recalls** — how often the agent went back and read the stored full output. A recall means the excerpt was not enough; a recall rate above about 10% says `context.confidence` is too low for your work, near 0% says it could go lower. Every compression and recall also appears in the trace with the running ledger. The quality effect (less context rot) does not show in a counter; compare a scripted task with `context.enabled` on and off if you want that number.
|
|
100
|
+
|
|
99
101
|
This MVP is always tail-only: set `context.enabled: false` to stop compression. It never prunes a warm prefix, changes the system prompt, or edits historical session entries. Full-output files can contain secrets; they remain in the OS temporary directory until you or the OS remove them. Excerpts can omit relevant evidence; read the linked file before making a decision that needs it.
|
|
100
102
|
|
|
101
103
|
### Task continuity
|
package/dist/extension.js
CHANGED
|
@@ -11,6 +11,7 @@ import { redact } from "./redact.js";
|
|
|
11
11
|
import { AttemptWindow, evaluateStuck, formatStuck, makeAttempt, resultFailed, stuckNudge } from "./stuck.js";
|
|
12
12
|
import { openTracePanel } from "./panel.js";
|
|
13
13
|
import { completeConfig, shapeWarning } from "./shape.js";
|
|
14
|
+
import { ContextLedger, formatLedger } from "./saver.js";
|
|
14
15
|
import { actionDetails, doneDetails, proseDetails, stuckDetails, Trace } from "./trace.js";
|
|
15
16
|
import { proseTokens, renderTemplate, TOKEN_NAMES } from "./widget.js";
|
|
16
17
|
export const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; and redacted tool-output samples for security and tail compression. Compression stores an exact, owner-only copy in a temporary file on this machine. Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
@@ -105,6 +106,7 @@ export default function wardenExtension(pi) {
|
|
|
105
106
|
let doneNudged = false;
|
|
106
107
|
const prose = new ProseTrend();
|
|
107
108
|
const slopCounts = { stub: 0, comments: 0, dead: 0, hedging: 0 };
|
|
109
|
+
const ledger = new ContextLedger();
|
|
108
110
|
// A partially updated module graph can hand this build a config without the sections it expects; see shape.ts.
|
|
109
111
|
let shapeReported = false;
|
|
110
112
|
const configFor = (ctx) => {
|
|
@@ -182,6 +184,7 @@ export default function wardenExtension(pi) {
|
|
|
182
184
|
evidence = emptyEvidence();
|
|
183
185
|
doneNudged = false;
|
|
184
186
|
prose.reset();
|
|
187
|
+
ledger.reset();
|
|
185
188
|
for (const symptom of Object.keys(slopCounts))
|
|
186
189
|
slopCounts[symptom] = 0;
|
|
187
190
|
if (ctx.hasUI)
|
|
@@ -196,9 +199,19 @@ export default function wardenExtension(pi) {
|
|
|
196
199
|
pi.on("agent_start", async () => {
|
|
197
200
|
evidence = emptyEvidence();
|
|
198
201
|
});
|
|
202
|
+
// Every turn that runs after a compression is a turn that did not carry the removed text.
|
|
203
|
+
pi.on("turn_end", async () => {
|
|
204
|
+
ledger.turnEnd();
|
|
205
|
+
});
|
|
199
206
|
pi.on("tool_call", async (event, ctx) => {
|
|
200
207
|
const config = configFor(ctx);
|
|
201
|
-
if (!config.enabled
|
|
208
|
+
if (!config.enabled)
|
|
209
|
+
return;
|
|
210
|
+
// A read of a stored full output means the excerpt was not enough; that is the number that tunes context.confidence.
|
|
211
|
+
const recalled = ledger.noteAccess(JSON.stringify(event.input));
|
|
212
|
+
if (recalled)
|
|
213
|
+
record(ctx, config, "context", renderTemplate(config.widget.context, { tool: event.toolName, retention: "full output recalled" }), [`the agent went back to ${recalled}`, formatLedger(ledger.snapshot())]);
|
|
214
|
+
if (!config.action.enabled || !config.action.tools.includes(event.toolName))
|
|
202
215
|
return;
|
|
203
216
|
stats.inspected++;
|
|
204
217
|
const task = latestUserPrompt(ctx);
|
|
@@ -283,6 +296,8 @@ export default function wardenExtension(pi) {
|
|
|
283
296
|
noteError(ctx, output.error, output.errorCode);
|
|
284
297
|
let content = event.content;
|
|
285
298
|
const notice = securityNotice(output);
|
|
299
|
+
if (config.context.enabled && textBlocks.length === 1 && text.length >= config.context.tailMinChars)
|
|
300
|
+
ledger.candidate();
|
|
286
301
|
const excerpt = compressOutput(text, output.retention);
|
|
287
302
|
if (excerpt && !ctx.signal?.aborted) {
|
|
288
303
|
try {
|
|
@@ -291,9 +306,11 @@ export default function wardenExtension(pi) {
|
|
|
291
306
|
const bytesSaved = Buffer.byteLength(text) - Buffer.byteLength(replacement) - (notice ? Buffer.byteLength(notice) * 2 + 4 : 0);
|
|
292
307
|
if (bytesSaved > 0) {
|
|
293
308
|
content = content.map(part => part.type === "text" ? { ...part, text: replacement } : part);
|
|
309
|
+
ledger.record(path, bytesSaved);
|
|
294
310
|
record(ctx, config, "context", renderTemplate(config.widget.context, { tool: event.toolName, retention: output.retention, bytesSaved: String(bytesSaved) }), [
|
|
295
311
|
`retention: ${output.retention}; confidence ${output.confidence?.toFixed(2)}; ${output.model}; ${output.elapsedMs} ms`,
|
|
296
312
|
`saved ${bytesSaved} bytes; full output: ${path}`,
|
|
313
|
+
formatLedger(ledger.snapshot()),
|
|
297
314
|
]);
|
|
298
315
|
}
|
|
299
316
|
}
|
|
@@ -427,6 +444,7 @@ export default function wardenExtension(pi) {
|
|
|
427
444
|
`pi-warden: ${config.enabled ? `guarding ${config.action.tools.join(", ")} (${guards})` : "off"}; mode ${activeMode(config, ctx.hasUI)}; TypeSafe judgments ${source ? `enabled via ${source}` : "disabled (run /warden enable)"}; key ${key ? key.source === "stored" ? "stored (shared with pi-typesafe)" : "from TYPESAFE_API_KEY" : "missing (run /warden enable)"}.`,
|
|
428
445
|
`Session: ${stats.inspected} inspected, ${stats.judged} judged, ${stats.warned} warned, ${stats.held} held, ${stats.approved} approved on retry, ${stats.slop} slop notes, ${stats.stuck}/${stats.stuckChecks} stuck, ${stats.unverified}/${stats.doneChecks} unverified done, ${stats.proseNudges}/${stats.proseChecks} prose nudges, ${stats.errors} TypeSafe errors; ${usage?.requestsStarted ?? 0}/${config.maxRequests} requests. Steers are ${config.steerVisible ? "shown in the transcript" : "hidden from the transcript (trace panel shows them)"}.`,
|
|
429
446
|
`Thresholds: irreversible warn ${config.action.irreversible.warn} / hold ${config.action.irreversible.confirm}; off-task warn ${config.action.offTask.warn} / hold ${config.action.offTask.confirm}; stuck same-strategy ${config.stuck.sameStrategy} after ${config.stuck.minFailures} failures; done claims ${config.done.claimsDone}; slop ${config.slop.threshold}, prose ${config.slop.prose.threshold} in ${config.slop.prose.trend}/3 replies; failOpen ${config.action.failOpen}.`,
|
|
447
|
+
formatLedger(ledger.snapshot()),
|
|
430
448
|
`Config: ${userConfigPath()}${ctx.isProjectTrusted() ? ` and ${projectConfigPath(ctx.cwd)}` : ""}.`,
|
|
431
449
|
widget.size ? `Last: ${[...widget.values()].join(" | ")}` : "No guarded activity yet this session.",
|
|
432
450
|
`Trace: ${trace.entries().length} events (/warden trace${shortcut ? `, ${shortcut}` : ""}, or click the status line in fullscreen mode; each toggles the sidebar). Widget templates in config.widget: action tokens ${TOKEN_NAMES.action.map(name => `{${name}}`).join(" ")}.`,
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export { evaluateOutput, buildOutputRequest, outputQuestions, compressOutput, sa
|
|
|
11
11
|
export type { Retention, OutputVerdict, OutputOptions } from "./output.js";
|
|
12
12
|
export { redact } from "./redact.js";
|
|
13
13
|
export { completeConfig, shapeWarning, EXPECTED_SCHEMA } from "./shape.js";
|
|
14
|
+
export { ContextLedger, formatLedger } from "./saver.js";
|
|
15
|
+
export type { ContextLedgerSnapshot } from "./saver.js";
|
|
14
16
|
export type { ShapeResult } from "./shape.js";
|
|
15
17
|
export { CONFIG_SCHEMA, defaultConfig, loadConfig, applyUserOverrides, applyProjectOverrides, isMode, userConfigPath, projectConfigPath, readUserConfig, writeUserConfig, setUserSetting, PACKAGE_NAME } from "./config.js";
|
|
16
18
|
export type { ActionGuardConfig, StuckGuardConfig, DoneGuardConfig, SlopGuardConfig, ProseConfig, SecurityConfig, ContextConfig, Threshold, WardenConfig, WardenMode, LoadOptions } from "./config.js";
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { classifyToolResult, recordOutcome, emptyEvidence, needsDoneCheck, final
|
|
|
6
6
|
export { evaluateOutput, buildOutputRequest, outputQuestions, compressOutput, saveOutput, securityNotice } from "./output.js";
|
|
7
7
|
export { redact } from "./redact.js";
|
|
8
8
|
export { completeConfig, shapeWarning, EXPECTED_SCHEMA } from "./shape.js";
|
|
9
|
+
export { ContextLedger, formatLedger } from "./saver.js";
|
|
9
10
|
export { CONFIG_SCHEMA, defaultConfig, loadConfig, applyUserOverrides, applyProjectOverrides, isMode, userConfigPath, projectConfigPath, readUserConfig, writeUserConfig, setUserSetting, PACKAGE_NAME } from "./config.js";
|
|
10
11
|
export { commandOf, outputReportsFailure, COMMAND_TOOLS } from "./tools.js";
|
|
11
12
|
export { renderTemplate, actionTokens, stuckTokens, doneTokens, proseTokens, defaultWidgetConfig, DEFAULT_TEMPLATES, TOKEN_NAMES } from "./widget.js";
|
package/dist/saver.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session accounting for the context saver, so its value can be measured instead of assumed:
|
|
3
|
+
* how often large outputs appear, how many were compressed, how much was removed, how many turns that removal was
|
|
4
|
+
* spared from (token-turns), and how often the agent went back for the full text (a recall means the excerpt was
|
|
5
|
+
* not enough). Deterministic; no requests.
|
|
6
|
+
*/
|
|
7
|
+
export interface ContextLedgerSnapshot {
|
|
8
|
+
/** Single-text-block outputs at or above tailMinChars, i.e. candidates for compression. */
|
|
9
|
+
large: number;
|
|
10
|
+
compressed: number;
|
|
11
|
+
bytesSaved: number;
|
|
12
|
+
/** Assistant turns completed since the session started. */
|
|
13
|
+
turns: number;
|
|
14
|
+
/** Sum over turns of the bytes that were no longer in context on that turn, in rough tokens (bytes / 4). */
|
|
15
|
+
tokenTurnsSaved: number;
|
|
16
|
+
/** Times the agent read a stored full output after compression. */
|
|
17
|
+
recalls: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class ContextLedger {
|
|
20
|
+
private large;
|
|
21
|
+
private compressed;
|
|
22
|
+
private bytesSaved;
|
|
23
|
+
private turns;
|
|
24
|
+
private tokenTurnsSaved;
|
|
25
|
+
private recalls;
|
|
26
|
+
private readonly stored;
|
|
27
|
+
candidate(): void;
|
|
28
|
+
record(path: string, bytesSaved: number): void;
|
|
29
|
+
/** One LLM turn finished: everything removed so far was absent from this turn's prompt. */
|
|
30
|
+
turnEnd(): void;
|
|
31
|
+
/** True when `text` (a path, command, or serialized input) mentions a stored full output for the first time. */
|
|
32
|
+
noteAccess(text: string): string | undefined;
|
|
33
|
+
snapshot(): ContextLedgerSnapshot;
|
|
34
|
+
reset(): void;
|
|
35
|
+
}
|
|
36
|
+
/** One line for /warden status. Says when there is nothing to report instead of printing zeros. */
|
|
37
|
+
export declare function formatLedger(snapshot: ContextLedgerSnapshot): string;
|
package/dist/saver.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export class ContextLedger {
|
|
2
|
+
large = 0;
|
|
3
|
+
compressed = 0;
|
|
4
|
+
bytesSaved = 0;
|
|
5
|
+
turns = 0;
|
|
6
|
+
tokenTurnsSaved = 0;
|
|
7
|
+
recalls = 0;
|
|
8
|
+
stored = new Map();
|
|
9
|
+
candidate() {
|
|
10
|
+
this.large++;
|
|
11
|
+
}
|
|
12
|
+
record(path, bytesSaved) {
|
|
13
|
+
this.compressed++;
|
|
14
|
+
this.bytesSaved += bytesSaved;
|
|
15
|
+
this.stored.set(path, { recalled: false });
|
|
16
|
+
}
|
|
17
|
+
/** One LLM turn finished: everything removed so far was absent from this turn's prompt. */
|
|
18
|
+
turnEnd() {
|
|
19
|
+
this.turns++;
|
|
20
|
+
this.tokenTurnsSaved += Math.round(this.bytesSaved / 4);
|
|
21
|
+
}
|
|
22
|
+
/** True when `text` (a path, command, or serialized input) mentions a stored full output for the first time. */
|
|
23
|
+
noteAccess(text) {
|
|
24
|
+
for (const [path, state] of this.stored) {
|
|
25
|
+
if (!text.includes(path))
|
|
26
|
+
continue;
|
|
27
|
+
if (!state.recalled) {
|
|
28
|
+
state.recalled = true;
|
|
29
|
+
this.recalls++;
|
|
30
|
+
}
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
snapshot() {
|
|
36
|
+
return { large: this.large, compressed: this.compressed, bytesSaved: this.bytesSaved, turns: this.turns, tokenTurnsSaved: this.tokenTurnsSaved, recalls: this.recalls };
|
|
37
|
+
}
|
|
38
|
+
reset() {
|
|
39
|
+
this.large = 0;
|
|
40
|
+
this.compressed = 0;
|
|
41
|
+
this.bytesSaved = 0;
|
|
42
|
+
this.turns = 0;
|
|
43
|
+
this.tokenTurnsSaved = 0;
|
|
44
|
+
this.recalls = 0;
|
|
45
|
+
this.stored.clear();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** One line for /warden status. Says when there is nothing to report instead of printing zeros. */
|
|
49
|
+
export function formatLedger(snapshot) {
|
|
50
|
+
if (snapshot.large === 0)
|
|
51
|
+
return "Context saver: no tool output large enough to consider this session.";
|
|
52
|
+
const kb = (snapshot.bytesSaved / 1024).toFixed(1);
|
|
53
|
+
const recallRate = snapshot.compressed ? Math.round((snapshot.recalls / snapshot.compressed) * 100) : 0;
|
|
54
|
+
return `Context saver: ${snapshot.large} large outputs, ${snapshot.compressed} compressed, ${kb} KB removed (~${Math.round(snapshot.bytesSaved / 4)} tokens), ~${snapshot.tokenTurnsSaved} token-turns spared over ${snapshot.turns} turns, ${snapshot.recalls} recall${snapshot.recalls === 1 ? "" : "s"} of the full output (${recallRate}%).`;
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-warden",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Guardrails for Pi built on pi-typesafe that steer the agent instead of interrupting you: Jev judges irreversible and off-task tool calls before they run, detects stuck loops, checks unverified done claims, and flags slop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|