pi-warden 0.35.0 → 0.38.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/CHANGELOG.md +43 -0
- package/README.md +1 -1
- package/dist/action-guard.d.ts +3 -1
- package/dist/action-guard.js +1 -1
- package/dist/compact.d.ts +63 -0
- package/dist/compact.js +84 -0
- package/dist/config.d.ts +40 -1
- package/dist/config.js +42 -2
- package/dist/conscience.d.ts +117 -0
- package/dist/conscience.js +379 -0
- package/dist/extension.d.ts +2 -2
- package/dist/extension.js +475 -12
- package/dist/extension.js.bak +492 -9
- package/dist/guard.d.ts +7 -1
- package/dist/guard.js +3 -2
- package/dist/load.d.ts +47 -0
- package/dist/load.js +248 -0
- package/dist/shape.d.ts +1 -1
- package/dist/shape.js +14 -2
- package/dist/trace.d.ts +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,49 @@ How to keep this current: add the entry in the same pull request as the change,
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
<!-- Empty. Next release starts here. -->
|
|
10
|
+
|
|
11
|
+
## 0.38.3
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- `/warden config` keeps its panel handle and toggles shut, instead of stacking a second overlay against its own header hint.
|
|
15
|
+
- A bare `/warden` runs `status` again, and `config set <key> <value>` / `get <key>` reach the setting — the argument split dropped the default action and kept only the word after the subcommand.
|
|
16
|
+
|
|
17
|
+
## 0.38.2
|
|
18
|
+
|
|
19
|
+
### Docs
|
|
20
|
+
- Conscience recommendation calibration on recorded sessions: first measurement scripts, report, and guard calibration tables.
|
|
21
|
+
|
|
22
|
+
## 0.38.1
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Verified against Pi 0.87.0; dev dependency updated.
|
|
26
|
+
|
|
27
|
+
## 0.38.0
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Conscience coach: disabled by default pending calibration. Recommends or loads skills and tools before the agent acts via `before_agent_start`. Trace-only until a measured policy ships. Load mode reads skill files from disk with path-rule checks, size bounds, frontmatter validation, and credential canary detection. 48 adversarial tests plus the fixture set.
|
|
31
|
+
|
|
32
|
+
### Docs
|
|
33
|
+
- Conscience recommendation calibration (2026-09-22): first measurement on 609 turns across 4 projects. Tool recommendation 84% precision, skill recommendation unmeasured pending human labels. No threshold meets the 95% gate; closest is 0.95 at 91%. Candidate policy recorded (not active); `docs/guards.md` updated. `eval/reports/2026-09-22-conscience-recommend/`.
|
|
34
|
+
## 0.37.1
|
|
35
|
+
|
|
36
|
+
### Docs
|
|
37
|
+
- CONTRIBUTING.md: reflecting the evidence floor for destructive patterns; version bump is optional for outside PRs. docs/guards.md: same stale wording fixed.
|
|
38
|
+
|
|
39
|
+
## 0.37.0
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- Compaction evidence appendix (`context.compactAppendix`, default true): after compaction succeeds, the extension sends a deterministic summary of session evidence — saved outputs, last checks, held actions, stuck state, and the active task — as one custom message so the agent can prefer saved paths over re-running commands. All strings are redacted. Does not spend a steer unit.
|
|
43
|
+
|
|
44
|
+
## 0.36.0
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- The resolved rules file content no longer rides the action request when the rules guard is off. `rules.enabled: false` now means no rules file content leaves the machine at all; with the guard on, the request is unchanged. `EvaluateOptions.rules` and `InspectOptions.rules` carry the switch, and a library caller that omits it keeps the earlier behaviour. The disclosure and `docs/data-handling.md` say so.
|
|
48
|
+
|
|
49
|
+
### Docs
|
|
50
|
+
- `docs/configuration.md`: `Recipe: security work` — what each guard sends off the machine and what `/warden disable` leaves behind, a local-only user profile, a lab/CTF project profile with the exemptions security work needs, and an out-of-scope deny rule. README privacy paragraph points to it.
|
|
51
|
+
|
|
9
52
|
## 0.35.0
|
|
10
53
|
|
|
11
54
|
### Added
|
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ These are project-maintained benchmarks, not universal claims. Raw reports and r
|
|
|
82
82
|
|
|
83
83
|
## Privacy
|
|
84
84
|
|
|
85
|
-
Depending on the configured judgment backend, Warden sends to your provider: a redacted summary of each guarded call, the agent's plan, up to eight redacted prior messages, and redacted tool-output samples. Secrets are stripped before anything leaves the machine. [Full details.](docs/data-handling.md)
|
|
85
|
+
Depending on the configured judgment backend, Warden sends to your provider: a redacted summary of each guarded call, the agent's plan, up to eight redacted prior messages, and redacted tool-output samples. Secrets are stripped before anything leaves the machine. [Full details.](docs/data-handling.md) Security work — pentesting, incident response, CTF — has its own configuration recipe for keeping samples local and for the friction that credentials and lab setups cause: [Recipe: security work](docs/configuration.md#recipe-security-work).
|
|
86
86
|
|
|
87
87
|
## Documentation
|
|
88
88
|
|
package/dist/action-guard.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActionGuardConfig, SecurityConfig, SlopGuardConfig } from "./config.js";
|
|
1
|
+
import type { ActionGuardConfig, RulesConfig, SecurityConfig, SlopGuardConfig } from "./config.js";
|
|
2
2
|
import type { PreviousAction, TaskMessage, Verdict } from "./guard.js";
|
|
3
3
|
import type { Judge } from "pi-typesafe";
|
|
4
4
|
/** One tool call as the agent proposed it. `id` is Pi's tool call id, stable across hooks and retries. */
|
|
@@ -25,6 +25,8 @@ export interface InspectOptions {
|
|
|
25
25
|
signal?: AbortSignal | undefined;
|
|
26
26
|
slop?: SlopGuardConfig | undefined;
|
|
27
27
|
security?: SecurityConfig | undefined;
|
|
28
|
+
/** The rules guard's switch; off keeps the rules file content out of the action request. Omitted, it is sent as before. */
|
|
29
|
+
rules?: Pick<RulesConfig, "enabled"> | undefined;
|
|
28
30
|
/** Calls allowed in the previous turn; the regret question about them rides this call's request, never a sibling's. */
|
|
29
31
|
previousActions?: readonly PreviousAction[] | undefined;
|
|
30
32
|
}
|
package/dist/action-guard.js
CHANGED
|
@@ -20,7 +20,7 @@ export class ActionGuard {
|
|
|
20
20
|
const { task } = conversation;
|
|
21
21
|
// A hold happened under an earlier prompt and the user has since replied: ask whether the reply approves this action.
|
|
22
22
|
const retryAfterHold = this.holdPending && this.lastHoldPrompt !== task;
|
|
23
|
-
const judgeCall = (tool, input, previousActions) => evaluateAction({ tool, input, cwd: options.cwd, task, context: conversation.context, plan: conversation.plan }, { config: options.config, judge: options.judge, signal: options.signal, slop: options.slop, security: options.security, retryAfterHold, previousActions });
|
|
23
|
+
const judgeCall = (tool, input, previousActions) => evaluateAction({ tool, input, cwd: options.cwd, task, context: conversation.context, plan: conversation.plan }, { config: options.config, judge: options.judge, signal: options.signal, slop: options.slop, security: options.security, rules: options.rules, retryAfterHold, previousActions });
|
|
24
24
|
// A retry after a hold stays sequential because an approval consumed by one sibling changes the question for the next.
|
|
25
25
|
if (options.judge && !retryAfterHold) {
|
|
26
26
|
for (const sibling of conversation.siblings ?? []) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface SavedOutput {
|
|
2
|
+
tool: string;
|
|
3
|
+
path: string;
|
|
4
|
+
/** Uncompressed size in bytes. */
|
|
5
|
+
bytes: number;
|
|
6
|
+
}
|
|
7
|
+
export interface CheckEntry {
|
|
8
|
+
command: string;
|
|
9
|
+
passed: boolean;
|
|
10
|
+
/** Short label: "current run", "run 2", or "current run, #3". */
|
|
11
|
+
when: string;
|
|
12
|
+
}
|
|
13
|
+
export interface HeldEntry {
|
|
14
|
+
tool: string;
|
|
15
|
+
/** Redacted preview of the call (command, path, or input); ≤120 chars. */
|
|
16
|
+
preview: string;
|
|
17
|
+
outcome: string;
|
|
18
|
+
}
|
|
19
|
+
export interface StuckState {
|
|
20
|
+
failures: number;
|
|
21
|
+
sameStrategyScore: number | undefined;
|
|
22
|
+
currentCallFamily: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface CompactSnapshot {
|
|
25
|
+
savedOutputs: readonly SavedOutput[];
|
|
26
|
+
/** Capped at 5 in the hook before this is called. */
|
|
27
|
+
checks: readonly CheckEntry[];
|
|
28
|
+
/** Capped at 10 in the hook before this is called. */
|
|
29
|
+
holds: readonly HeldEntry[];
|
|
30
|
+
stuck: StuckState | undefined;
|
|
31
|
+
activeTask: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Returns empty string for an empty snapshot. Capped at 2 000 chars. */
|
|
34
|
+
export declare function compactAppendix(snapshot: CompactSnapshot): string;
|
|
35
|
+
/**
|
|
36
|
+
* Build a `CompactSnapshot` from the extension's session-scoped state. All string values are redacted; saved-output
|
|
37
|
+
* paths are local and allowed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildCompactSnapshot(options: {
|
|
40
|
+
savedOutputs: Array<{
|
|
41
|
+
tool: string;
|
|
42
|
+
path: string;
|
|
43
|
+
bytes: number;
|
|
44
|
+
}>;
|
|
45
|
+
checks: Array<{
|
|
46
|
+
command: string;
|
|
47
|
+
passed: boolean;
|
|
48
|
+
runIndex: number;
|
|
49
|
+
indexInRun: number;
|
|
50
|
+
}>;
|
|
51
|
+
holds: Array<{
|
|
52
|
+
tool: string;
|
|
53
|
+
preview: string;
|
|
54
|
+
outcome: string;
|
|
55
|
+
}>;
|
|
56
|
+
stuck: {
|
|
57
|
+
failures: number;
|
|
58
|
+
sameStrategyScore: number | undefined;
|
|
59
|
+
currentCallFamily: string | undefined;
|
|
60
|
+
} | undefined;
|
|
61
|
+
activeTask: string | undefined;
|
|
62
|
+
runs: number;
|
|
63
|
+
}): CompactSnapshot;
|
package/dist/compact.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction evidence appendix. Deterministic, pure, no Jev request.
|
|
3
|
+
*
|
|
4
|
+
* After Pi fires `session_compact`, the extension builds a snapshot from session memory and passes it to
|
|
5
|
+
* `compactAppendix`. The returned text is sent as one custom message so the agent can prefer saved paths
|
|
6
|
+
* over re-running commands.
|
|
7
|
+
*/
|
|
8
|
+
import { redact } from "./redact.js";
|
|
9
|
+
const MAX_CHARS = 2000;
|
|
10
|
+
/** Returns empty string for an empty snapshot. Capped at 2 000 chars. */
|
|
11
|
+
export function compactAppendix(snapshot) {
|
|
12
|
+
const sections = [];
|
|
13
|
+
if (snapshot.savedOutputs.length) {
|
|
14
|
+
const items = snapshot.savedOutputs
|
|
15
|
+
.map(item => `- ${redact(item.tool)} → ${redact(item.path)} (${item.bytes} bytes)`)
|
|
16
|
+
.join("\n");
|
|
17
|
+
sections.push(`### Saved full outputs\n${items}`);
|
|
18
|
+
}
|
|
19
|
+
if (snapshot.checks.length) {
|
|
20
|
+
const items = snapshot.checks
|
|
21
|
+
.map(c => `- ${c.passed ? "✓" : "✗"} ${c.when}: ${redact(c.command)}`)
|
|
22
|
+
.join("\n");
|
|
23
|
+
sections.push(`### Last checks\n${items}`);
|
|
24
|
+
}
|
|
25
|
+
if (snapshot.holds.length) {
|
|
26
|
+
const items = snapshot.holds
|
|
27
|
+
.map(h => `- ${redact(h.tool)}: ${redact(h.outcome)}${h.preview ? ` (${redact(h.preview)})` : ""}`)
|
|
28
|
+
.join("\n");
|
|
29
|
+
sections.push(`### Held actions\n${items}`);
|
|
30
|
+
}
|
|
31
|
+
if (snapshot.stuck) {
|
|
32
|
+
const s = snapshot.stuck;
|
|
33
|
+
const family = s.currentCallFamily ? `; family: ${redact(s.currentCallFamily)}` : "";
|
|
34
|
+
const score = s.sameStrategyScore !== undefined ? `; same-strategy: ${s.sameStrategyScore.toFixed(2)}` : "";
|
|
35
|
+
sections.push(`### Stuck state\n- failures: ${s.failures}${score}${family}`);
|
|
36
|
+
}
|
|
37
|
+
if (snapshot.activeTask) {
|
|
38
|
+
sections.push(`### Active task\n${redact(snapshot.activeTask)}`);
|
|
39
|
+
}
|
|
40
|
+
if (sections.length === 0)
|
|
41
|
+
return "";
|
|
42
|
+
const body = sections.join("\n\n");
|
|
43
|
+
const appendix = [
|
|
44
|
+
"=== PI-WARDEN COMPACT EVIDENCE ===",
|
|
45
|
+
"Evidence warden kept across compaction. Prefer the saved-output paths below over re-running commands that produced them.",
|
|
46
|
+
"",
|
|
47
|
+
body,
|
|
48
|
+
"=== END PI-WARDEN COMPACT EVIDENCE ===",
|
|
49
|
+
].join("\n");
|
|
50
|
+
const marker = "\n… [truncated]";
|
|
51
|
+
if (appendix.length <= MAX_CHARS)
|
|
52
|
+
return appendix;
|
|
53
|
+
return appendix.slice(0, MAX_CHARS - marker.length) + marker;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build a `CompactSnapshot` from the extension's session-scoped state. All string values are redacted; saved-output
|
|
57
|
+
* paths are local and allowed.
|
|
58
|
+
*/
|
|
59
|
+
export function buildCompactSnapshot(options) {
|
|
60
|
+
const savedOutputs = options.savedOutputs.map(o => ({
|
|
61
|
+
tool: o.tool,
|
|
62
|
+
path: o.path,
|
|
63
|
+
bytes: o.bytes,
|
|
64
|
+
}));
|
|
65
|
+
const maxChecks = 5;
|
|
66
|
+
const checks = options.checks.slice(-maxChecks).map(c => ({
|
|
67
|
+
command: c.command,
|
|
68
|
+
passed: c.passed,
|
|
69
|
+
when: options.runs <= 1 ? "current run" : `run ${c.runIndex}${c.indexInRun > 0 ? `, #${c.indexInRun + 1}` : ""}`,
|
|
70
|
+
}));
|
|
71
|
+
const maxHolds = 10;
|
|
72
|
+
const holds = options.holds.slice(-maxHolds).map(h => ({
|
|
73
|
+
tool: h.tool,
|
|
74
|
+
preview: h.preview,
|
|
75
|
+
outcome: h.outcome,
|
|
76
|
+
}));
|
|
77
|
+
const stuck = options.stuck ? {
|
|
78
|
+
failures: options.stuck.failures,
|
|
79
|
+
sameStrategyScore: options.stuck.sameStrategyScore,
|
|
80
|
+
currentCallFamily: options.stuck.currentCallFamily,
|
|
81
|
+
} : undefined;
|
|
82
|
+
const activeTask = options.activeTask ? redact(options.activeTask) : undefined;
|
|
83
|
+
return { savedOutputs, checks, holds, stuck, activeTask };
|
|
84
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -191,6 +191,8 @@ export interface ContextConfig {
|
|
|
191
191
|
recallTool: RecallTool;
|
|
192
192
|
/** Minimum P(format) before a format-specific parser builds the excerpt instead of the generic head/tail one. */
|
|
193
193
|
formatConfidence: number;
|
|
194
|
+
/** Append a compact evidence appendix to the summary during compaction. */
|
|
195
|
+
compactAppendix: boolean;
|
|
194
196
|
}
|
|
195
197
|
export interface RunawayConfig {
|
|
196
198
|
enabled: boolean;
|
|
@@ -239,6 +241,41 @@ export interface LearningConfig {
|
|
|
239
241
|
/** Days to keep hold records in SQLite before pruning. 0 disables pruning. */
|
|
240
242
|
retentionDays: number;
|
|
241
243
|
}
|
|
244
|
+
export type ConscienceSkillMode = "off" | "recommend" | "load";
|
|
245
|
+
export interface ConscienceSkillConfig {
|
|
246
|
+
/** off: no automatic skill selection; recommend: name a skill and ask the agent to load it; load: supply instructions directly. */
|
|
247
|
+
mode: ConscienceSkillMode;
|
|
248
|
+
/** Case-sensitive skill names to exclude; * is the only wildcard. */
|
|
249
|
+
exclude: string[];
|
|
250
|
+
}
|
|
251
|
+
export interface ConscienceToolConfig {
|
|
252
|
+
/** Suggest tools including evidence/research tools; never execute or enable them directly. */
|
|
253
|
+
enabled: boolean;
|
|
254
|
+
/** Case-sensitive tool names to exclude; * is the only wildcard. */
|
|
255
|
+
exclude: string[];
|
|
256
|
+
}
|
|
257
|
+
export interface ConscienceConfig {
|
|
258
|
+
/** Master switch for the conscience module. */
|
|
259
|
+
enabled: boolean;
|
|
260
|
+
/** Skill selection settings. */
|
|
261
|
+
skills: ConscienceSkillConfig;
|
|
262
|
+
/** Tool suggestion settings. */
|
|
263
|
+
tools: ConscienceToolConfig;
|
|
264
|
+
/** Total wall-clock deadline for one assessment (ms). Effective deadline is min(this, shared timeoutMs). */
|
|
265
|
+
timeoutMs: number;
|
|
266
|
+
/** Max assessments per admitted operator prompt, including the initial. */
|
|
267
|
+
maxAssessments: number;
|
|
268
|
+
/** Max new guidance deliveries per admitted operator prompt. */
|
|
269
|
+
maxNudges: number;
|
|
270
|
+
/** Max UTF-8 bytes per skill file for automatic loading. */
|
|
271
|
+
maxSkillBytes: number;
|
|
272
|
+
/** Max cumulative automatic loading bytes per admitted prompt. */
|
|
273
|
+
maxLoadedBytes: number;
|
|
274
|
+
/** P(useful now) must reach this to select a candidate for recommendation. Default 1.0 (trace-only until calibrated per spec §7). */
|
|
275
|
+
recommendThreshold: number;
|
|
276
|
+
/** P(useful now) must reach this to auto-load. Must be >= recommendThreshold. Default 1.0 (trace-only until calibrated). */
|
|
277
|
+
loadThreshold: number;
|
|
278
|
+
}
|
|
242
279
|
export interface WardenConfig {
|
|
243
280
|
/** Master switch. false disables every guard, including offline pattern checks. */
|
|
244
281
|
enabled: boolean;
|
|
@@ -279,10 +316,12 @@ export interface WardenConfig {
|
|
|
279
316
|
steerBudget: number;
|
|
280
317
|
/** Learning and adaptation settings. */
|
|
281
318
|
learning: LearningConfig;
|
|
319
|
+
/** Broad-consideration coach: recommends or loads skills and tools before the agent acts. */
|
|
320
|
+
conscience: ConscienceConfig;
|
|
282
321
|
}
|
|
283
322
|
export declare const PACKAGE_NAME = "pi-warden";
|
|
284
323
|
/** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
|
|
285
|
-
export declare const CONFIG_SCHEMA =
|
|
324
|
+
export declare const CONFIG_SCHEMA = 7;
|
|
286
325
|
export declare const PROJECT_CONFIG_FILE = "pi-warden.json";
|
|
287
326
|
export declare function defaultConfig(): WardenConfig;
|
|
288
327
|
/** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */
|
package/dist/config.js
CHANGED
|
@@ -10,7 +10,7 @@ export function isRecallTool(value) {
|
|
|
10
10
|
}
|
|
11
11
|
export const PACKAGE_NAME = "pi-warden";
|
|
12
12
|
/** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
|
|
13
|
-
export const CONFIG_SCHEMA =
|
|
13
|
+
export const CONFIG_SCHEMA = 7;
|
|
14
14
|
export const PROJECT_CONFIG_FILE = `${PACKAGE_NAME}.json`;
|
|
15
15
|
export function defaultConfig() {
|
|
16
16
|
return {
|
|
@@ -44,7 +44,7 @@ export function defaultConfig() {
|
|
|
44
44
|
slop: { enabled: true, threshold: 0.7, prose: { enabled: true, audience: "technical", threshold: 0.7, trend: 2, minChars: 200 } },
|
|
45
45
|
security: { enabled: true, threshold: 0.7 },
|
|
46
46
|
rules: { enabled: true, threshold: 0.7, files: [], fallback: true, maxChars: 8000, exclude: [], skip: [], sensitivePaths: {} },
|
|
47
|
-
context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7 },
|
|
47
|
+
context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7, compactAppendix: true },
|
|
48
48
|
runaway: { enabled: true, repeats: 4, thinkingRepeats: 10, minChars: 400, recover: true },
|
|
49
49
|
notify: { enabled: false, cooldownMs: 10000, command: [] },
|
|
50
50
|
subagent: { enabled: true, wake: true, threshold: 0.8, cooldownMs: 120000 },
|
|
@@ -53,6 +53,19 @@ export function defaultConfig() {
|
|
|
53
53
|
notices: false,
|
|
54
54
|
steerBudget: 3,
|
|
55
55
|
learning: { adaptiveThresholds: true, patternAnalysis: true, minHoldsForAdaptive: 20, adaptationRate: 0.1, retentionDays: 365 },
|
|
56
|
+
conscience: {
|
|
57
|
+
enabled: false,
|
|
58
|
+
skills: { mode: "recommend", exclude: [] },
|
|
59
|
+
tools: { enabled: true, exclude: [] },
|
|
60
|
+
timeoutMs: 1500,
|
|
61
|
+
maxAssessments: 3,
|
|
62
|
+
maxNudges: 2,
|
|
63
|
+
maxSkillBytes: 32768,
|
|
64
|
+
maxLoadedBytes: 65536,
|
|
65
|
+
// Calibration targets from spec §7. Until measured, 1.0 keeps production trace-only.
|
|
66
|
+
recommendThreshold: 1.0,
|
|
67
|
+
loadThreshold: 1.0,
|
|
68
|
+
},
|
|
56
69
|
};
|
|
57
70
|
}
|
|
58
71
|
/** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */
|
|
@@ -408,6 +421,7 @@ function applyGuards(base, raw, timeoutMs, source) {
|
|
|
408
421
|
duplicateMinChars: positiveInteger(raw.context.duplicateMinChars, base.context.duplicateMinChars),
|
|
409
422
|
recallTool: isRecallTool(raw.context.recallTool) ? raw.context.recallTool : base.context.recallTool,
|
|
410
423
|
formatConfidence: probability(raw.context.formatConfidence, base.context.formatConfidence),
|
|
424
|
+
compactAppendix: boolean(raw.context.compactAppendix, base.context.compactAppendix),
|
|
411
425
|
} : base.context,
|
|
412
426
|
};
|
|
413
427
|
}
|
|
@@ -428,6 +442,7 @@ export function applyUserOverrides(base, raw) {
|
|
|
428
442
|
notices: boolean(raw.notices, base.notices),
|
|
429
443
|
steerBudget: typeof raw.steerBudget === "number" && Number.isInteger(raw.steerBudget) && raw.steerBudget >= 0 ? raw.steerBudget : base.steerBudget,
|
|
430
444
|
learning: applyLearning(base.learning, raw.learning),
|
|
445
|
+
conscience: applyConscience(base.conscience, raw.conscience),
|
|
431
446
|
};
|
|
432
447
|
}
|
|
433
448
|
function applyLearning(base, raw) {
|
|
@@ -441,6 +456,31 @@ function applyLearning(base, raw) {
|
|
|
441
456
|
retentionDays: Math.max(0, positiveInteger(raw.retentionDays, base.retentionDays)),
|
|
442
457
|
};
|
|
443
458
|
}
|
|
459
|
+
function applyConscience(base, raw) {
|
|
460
|
+
if (!isObject(raw))
|
|
461
|
+
return base;
|
|
462
|
+
const skillsRaw = isObject(raw.skills) ? raw.skills : undefined;
|
|
463
|
+
const toolsRaw = isObject(raw.tools) ? raw.tools : undefined;
|
|
464
|
+
const mode = typeof skillsRaw?.mode === "string" && ["off", "recommend", "load"].includes(skillsRaw.mode) ? skillsRaw.mode : base.skills.mode;
|
|
465
|
+
return {
|
|
466
|
+
enabled: boolean(raw.enabled, base.enabled),
|
|
467
|
+
skills: {
|
|
468
|
+
mode,
|
|
469
|
+
exclude: Array.isArray(skillsRaw?.exclude) ? skillsRaw.exclude.filter((x) => typeof x === "string") : base.skills.exclude,
|
|
470
|
+
},
|
|
471
|
+
tools: {
|
|
472
|
+
enabled: boolean(toolsRaw?.enabled, base.tools.enabled),
|
|
473
|
+
exclude: Array.isArray(toolsRaw?.exclude) ? toolsRaw.exclude.filter((x) => typeof x === "string") : base.tools.exclude,
|
|
474
|
+
},
|
|
475
|
+
timeoutMs: Math.max(100, Math.min(10000, typeof raw.timeoutMs === "number" ? raw.timeoutMs : base.timeoutMs)),
|
|
476
|
+
maxAssessments: Math.max(1, Math.min(10, typeof raw.maxAssessments === "number" ? raw.maxAssessments : base.maxAssessments)),
|
|
477
|
+
maxNudges: Math.max(1, Math.min(5, typeof raw.maxNudges === "number" ? raw.maxNudges : base.maxNudges)),
|
|
478
|
+
maxSkillBytes: Math.max(1024, Math.min(131072, typeof raw.maxSkillBytes === "number" ? raw.maxSkillBytes : base.maxSkillBytes)),
|
|
479
|
+
maxLoadedBytes: Math.max(1024, Math.min(262144, typeof raw.maxLoadedBytes === "number" ? raw.maxLoadedBytes : base.maxLoadedBytes)),
|
|
480
|
+
recommendThreshold: Math.max(0, Math.min(1, typeof raw.recommendThreshold === "number" ? raw.recommendThreshold : base.recommendThreshold)),
|
|
481
|
+
loadThreshold: Math.max(0, Math.min(1, typeof raw.loadThreshold === "number" ? raw.loadThreshold : base.loadThreshold)),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
444
484
|
/** Project files may tune the guards but cannot grant TypeSafe consent, change the mode, or raise budgets. */
|
|
445
485
|
export function applyProjectOverrides(base, raw) {
|
|
446
486
|
if (!isObject(raw))
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conscience module: broad-consideration coach that recommends or loads
|
|
3
|
+
* skills and tools before the agent acts.
|
|
4
|
+
*
|
|
5
|
+
* Pre-measurement: all selections are trace-only. No steers, no auto-loading.
|
|
6
|
+
*/
|
|
7
|
+
import type { Skill } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { Questions } from "pi-typesafe";
|
|
9
|
+
import type { ConscienceConfig } from "./config.js";
|
|
10
|
+
export type Disposition = "advance" | "awaiting_user" | "no_gap" | "unclear";
|
|
11
|
+
/** One candidate for assessment: a skill or a tool. */
|
|
12
|
+
export interface Candidate {
|
|
13
|
+
/** "skill" or "tool". */
|
|
14
|
+
kind: "skill" | "tool";
|
|
15
|
+
/** Stable id: skill name or tool name. */
|
|
16
|
+
id: string;
|
|
17
|
+
/** Human-readable description (sanitized: no absolute paths or URLs). */
|
|
18
|
+
description: string;
|
|
19
|
+
/** True when this candidate belongs to a category that was truncated (catalog incomplete). */
|
|
20
|
+
incomplete?: boolean;
|
|
21
|
+
/** Full skill metadata, present for kind=skill. */
|
|
22
|
+
skill?: Skill;
|
|
23
|
+
}
|
|
24
|
+
/** The result of one assessment. */
|
|
25
|
+
export interface AssessmentResult {
|
|
26
|
+
/** The disposition chosen by the shared question. */
|
|
27
|
+
disposition: Disposition;
|
|
28
|
+
/** The selected candidate, if any. */
|
|
29
|
+
selected: Candidate | null;
|
|
30
|
+
/** P(useful now) = P(direct_useful) + P(prerequisite). */
|
|
31
|
+
usefulness: number;
|
|
32
|
+
/** P(advance) from the disposition answer. */
|
|
33
|
+
pAdvance: number;
|
|
34
|
+
/** Question hash for deduplication. */
|
|
35
|
+
questionHash: string;
|
|
36
|
+
/** Time spent on this assessment (ms). */
|
|
37
|
+
elapsedMs: number;
|
|
38
|
+
/** Why no selection was made, if applicable. */
|
|
39
|
+
skipReason?: SkipReason | undefined;
|
|
40
|
+
/** Error category when skipReason is "error": timeout, network, configuration, auth, or other. */
|
|
41
|
+
errorCategory?: string | undefined;
|
|
42
|
+
/** Number of judge requests issued for this assessment. */
|
|
43
|
+
requestCount: number;
|
|
44
|
+
}
|
|
45
|
+
export type SkipReason = "disabled" | "no_consent" | "no_match" | "already_supplied" | "awaiting_user" | "timeout" | "error" | "stale" | "catalog_unavailable" | "catalog_limit" | "metadata_unsafe" | "budget" | "explicit_skill" | "origin_unknown" | "below_threshold";
|
|
46
|
+
/** Score levels for the usefulness question. Levels are ordered from least to most useful. */
|
|
47
|
+
export declare const SCORE_LEVELS: readonly ["No useful contribution to the current request, or conflicts with the supplied constraints.", "Related to the subject, but already covered, premature, or unable to resolve the current need.", "Addresses a concrete unmet need at the current step without displacing the active workflow.", "Supplies a missing prerequisite or directly applicable documented method needed for the current step."];
|
|
48
|
+
/** Strip absolute paths, URLs, and credentials from a description. */
|
|
49
|
+
export declare function sanitizeDescription(text: string): string;
|
|
50
|
+
/** Opaque IDs: c1, c2, ... in stable order. */
|
|
51
|
+
export declare function opaqueId(index: number): string;
|
|
52
|
+
/** Map candidates to opaque IDs, returning a stable ordered list. */
|
|
53
|
+
export declare function assignOpaqueIds(candidates: Candidate[]): Array<{
|
|
54
|
+
opaqueId: string;
|
|
55
|
+
candidate: Candidate;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Build assessment questions for a batch of candidates.
|
|
59
|
+
* State carries structured data; each question names its candidate field.
|
|
60
|
+
* Returns questions, the disposition key, and the opaqueId→candidate map.
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildBatchQuestions(batch: Array<{
|
|
63
|
+
opaqueId: string;
|
|
64
|
+
candidate: Candidate;
|
|
65
|
+
}>, task: string, recentContext: string, activeSkills: string[], suppliedSkills: string[]): {
|
|
66
|
+
questions: Questions;
|
|
67
|
+
dispositionKey: string;
|
|
68
|
+
idMap: Map<string, Candidate>;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Build the full state object for the judge request (spec §3 state contract).
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildState(task: string, recentContext: string, activeSkills: string[], suppliedSkills: string[], batch: Array<{
|
|
74
|
+
opaqueId: string;
|
|
75
|
+
candidate: Candidate;
|
|
76
|
+
}>): Record<string, unknown>;
|
|
77
|
+
/**
|
|
78
|
+
* Compute a deterministic hash of the assessment questions for deduplication.
|
|
79
|
+
* Recursively sorts all object keys for canonical form.
|
|
80
|
+
*/
|
|
81
|
+
export declare function questionHash(questions: Questions): string;
|
|
82
|
+
/** Filter candidates by eligibility rules, capped at MAX_ELIGIBLE per category. */
|
|
83
|
+
export declare function eligibleCandidates(skills: Skill[], tools: {
|
|
84
|
+
name: string;
|
|
85
|
+
description: string;
|
|
86
|
+
}[], config: ConscienceConfig, activeSkills: string[], suppliedSkills: string[]): {
|
|
87
|
+
candidates: Candidate[];
|
|
88
|
+
skillOverflow: boolean;
|
|
89
|
+
toolOverflow: boolean;
|
|
90
|
+
};
|
|
91
|
+
export interface Judge {
|
|
92
|
+
evaluate(request: {
|
|
93
|
+
state: unknown;
|
|
94
|
+
questions: Questions;
|
|
95
|
+
}, options?: {
|
|
96
|
+
timeoutMs?: number;
|
|
97
|
+
}): Promise<{
|
|
98
|
+
answers: Record<string, unknown>;
|
|
99
|
+
}>;
|
|
100
|
+
}
|
|
101
|
+
export interface ConscienceDeps {
|
|
102
|
+
judge: Judge | undefined;
|
|
103
|
+
config: ConscienceConfig;
|
|
104
|
+
/** Shared timeout from WardenConfig. The effective deadline is min(conscience.timeoutMs, sharedTimeoutMs). */
|
|
105
|
+
sharedTimeoutMs: number;
|
|
106
|
+
/** Current wall-clock time (ms). Injected for testability. */
|
|
107
|
+
now?: () => number;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Run assessment for the given prompt revision.
|
|
111
|
+
* Pre-measurement: returns trace-only results. No steers, no auto-loading.
|
|
112
|
+
* Issues sequential requests: skills first, then tools in the remaining envelope.
|
|
113
|
+
*/
|
|
114
|
+
export declare function assess(prompt: string, recentContext: string, skills: Skill[], tools: {
|
|
115
|
+
name: string;
|
|
116
|
+
description: string;
|
|
117
|
+
}[], activeSkills: string[], suppliedSkills: string[], deps: ConscienceDeps): Promise<AssessmentResult>;
|