1f7eb0ac59db8eb33a1af9a7ce97823002c7480c

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Add reviewer cache keys

Diff

This diff is truncated to protect this page.

  1diff --git a/AGENTS.md b/AGENTS.md
  2index 4dc50c1befccb890aa3c2ed984e264b2622629f1..f3f312530f39d8b8b594ae48461a768a3624dcaa 100644
  3--- a/AGENTS.md
  4+++ b/AGENTS.md
  5@@ -12,11 +12,12 @@ This package provides a policy plugin for Pi. It combines host-specific precheck
  6 
  7 The package root and `./pi` export the Pi extension. `src/index.ts` re-exports `./pi/index` for Pi extension loading.
  8 
  9+Your general goal is to keep this code simple. Don't add things you're not asked for. If given a choice, do less.
 10+
 11 ## Policy flow
 12 
 13 Pi evaluates session Bash overrides and static permissions before deterministic rules. It parses valid Bash into command nodes before evaluation. Unparsed Bash does not receive a deterministic allow and falls back to LLM review.
 14 
 15-
 16diff --git a/README.md b/README.md
 17index db5cdb5ab274ba918fbd9442a1a5272202a66200..3f4f9b2f631e412146fb4f8aeae87730f9af58bf 100644
 18--- a/README.md
 19+++ b/README.md
 20@@ -36,12 +36,15 @@ Use a Pi-configured model. This uses Pi's provider transport and authentication,
 21 		"kind": "pi",
 22 		"provider": "openai-codex",
 23 		"model": "gpt-5.6-luna",
 24-		"reasoningEffort": "minimal"
 25+		"reasoningEffort": "minimal",
 26+		"promptCacheKey": "policy-engine-v1"
 27 	}
 28 }
 29 ```
 30 
 31-Pi reviewers accept `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. The selected Pi model determines the supported levels.
 32diff --git a/src/core/config.ts b/src/core/config.ts
 33index 790311e607c628b013eb3c241dc78ab18cfe6fff..45a6b36099f7c3d2879f377d5267f96a00396756 100644
 34--- a/src/core/config.ts
 35+++ b/src/core/config.ts
 36@@ -51,5 +51,15 @@ export function parseReviewerConfig(value: unknown): ReviewerConfig {
 37 
 38   const provider = stringValue(reviewer.provider)
 39   if (!provider) throw new Error("reviewer.provider must be a non-empty string")
 40-  return { kind: "pi", provider, model, reasoningEffort: reasoningEffort(reviewer.reasoningEffort) }
 41+  const promptCacheKey = stringValue(reviewer.promptCacheKey)
 42+  if (reviewer.promptCacheKey !== undefined && !promptCacheKey) {
 43+    throw new Error("reviewer.promptCacheKey must be a non-empty string")
 44+  }
 45+  return {
 46+    kind: "pi",
 47+    provider,
 48+    model,
 49+    reasoningEffort: reasoningEffort(reviewer.reasoningEffort),
 50+    promptCacheKey,
 51+  }
 52 }
 53diff --git a/src/core/review.ts b/src/core/review.ts
 54index 0747e1b142d86dc4ff924481e4bf81f9fc71634d..b3261fafc4db3cfca84028a8986b7f6e28b12118 100644
 55--- a/src/core/review.ts
 56+++ b/src/core/review.ts
 57@@ -73,7 +73,7 @@ export function createPolicyReviewer(
 58         ]
 59         const rawResponse = config.kind === "pi"
 60           ? await callPiReviewer(config, messages, 512, context)
 61-          : await callLlamaReviewer(config, messages, 512)
 62+          : await callLlamaReviewer(config, messages, 1024)
 63         const decision = parseLLMResponse(rawResponse)
 64         if (decision) return { decision, rawResponse }
 65         return {
 66diff --git a/src/core/types.ts b/src/core/types.ts
 67index e48ccb34888889ddc4f4ceddba5697ee887c7667..c0767d0178e75f99f85bf3e9f890c72bcd054ebf 100644
 68--- a/src/core/types.ts
 69+++ b/src/core/types.ts
 70@@ -55,6 +55,7 @@ export type ReviewerConfig =
 71     provider: string
 72     model: string
 73     reasoningEffort?: PiReasoningEffort
 74+    promptCacheKey?: string
 75   }
 76   | {
 77     kind: "llama.cpp"
 78diff --git a/src/pi/index.ts b/src/pi/index.ts
 79index 0e1145053d48ec697657c9eaea14eb774628b151..576f69b4c4e4fa545db36ec39be5b7c8b84cc434 100644
 80--- a/src/pi/index.ts
 81+++ b/src/pi/index.ts
 82@@ -52,7 +52,17 @@ function bypassesPiPolicy(toolName: string): boolean {
 83 }
 84 
 85 function piReviewerCaller(ctx: ExtensionContext): PiReviewerCaller {
 86diff --git a/test/core/llm.test.ts b/test/core/llm.test.ts
 87index 59caf94801e0bb1b281ce419c410d4e1a0280a46..2232e168e34ab222a4b055bcf06521c757f35c75 100644
 88--- a/test/core/llm.test.ts
 89+++ b/test/core/llm.test.ts
 90@@ -9,6 +9,7 @@ const baseUrl = process.env.POLICY_EVAL_BASE_URL?.trim()
 91 const provider = process.env.POLICY_EVAL_PROVIDER?.trim() ?? "openai-codex"
 92 const model = process.env.POLICY_EVAL_MODEL?.trim()
 93 const reasoningEffort = process.env.POLICY_EVAL_REASONING_EFFORT?.trim() ?? "minimal"
 94+const promptCacheKey = process.env.POLICY_EVAL_PROMPT_CACHE_KEY?.trim() ?? "policy-engine-evaluation-v1"
 95 
 96 function piReasoningEffort(value: string): PiReasoningEffort {
 97   if (value === "none" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max") {
 98@@ -40,9 +41,16 @@ async function createReviewer(): Promise<PolicyReviewer | undefined> {
 99   const piModel = runtime.getModel(provider, model)
100   if (!piModel) throw new Error(`Pi model is unavailable: ${provider}/${model}`)
101   const effort = piReasoningEffort(reasoningEffort)
102+  const usesOpenAIPromptCache = piModel.api === "openai-codex-responses" || piModel.api === "openai-responses"
103 
104   return createPolicyReviewer(
105-    { kind: "pi", provider, model, reasoningEffort: effort },
106+    {
107+      kind: "pi",
108+      provider,
109+      model,
110+      reasoningEffort: effort,
111+      ...(usesOpenAIPromptCache && { promptCacheKey }),
112+    },
113     async (_config, messages, _maxTokens, context) => {
114       const systemPrompt = messages.find((message) => message.role === "system")?.content
115       const userMessages: UserMessage[] = messages
116@@ -55,7 +63,11 @@ async function createReviewer(): Promise<PolicyReviewer | undefined> {
117       const response = await runtime.complete(
118         piModel,
119         { systemPrompt, messages: userMessages },
120-        { signal: context.signal, reasoningEffort: effort },
121+        {
122+          signal: context.signal,
123+          reasoningEffort: effort,
124+          ...(usesOpenAIPromptCache && { sessionId: promptCacheKey }),
125+        },
126       )
127       if (response.stopReason !== "stop") {
128         throw new Error(response.errorMessage ?? `Pi reviewer stopped: ${response.stopReason}`)
129diff --git a/test/core/review.test.ts b/test/core/review.test.ts
130deleted file mode 100644
131index 2cfc4a5cad2ac2d5a1376ddf6f43c62712cf442b..0000000000000000000000000000000000000000
132--- a/test/core/review.test.ts
133+++ /dev/null
134@@ -1,14 +0,0 @@
135-import { expect, test } from "bun:test"
136-import { llmPolicyPrompt } from "../../src/core/rules"
137-
138-test("inserts external-directory permissions before the decision instructions", () => {
139-  const prompt = llmPolicyPrompt(["/tmp/pi/*"])
140-
141-  expect(prompt).toContain("- \"/tmp/pi/*\"")
142-  expect(prompt.indexOf("- \"/tmp/pi/*\"")).toBeLessThan(prompt.indexOf("Never ask because"))
143-  expect(prompt).not.toContain("<<EXTERNAL_DIRECTORIES>>")
144-})
145-
146-test("renders a placeholder list when no external directories are configured", () => {
147-  expect(llmPolicyPrompt()).toContain("- No additional directories.")
148-})
149diff --git a/test/pi/extension.test.ts b/test/pi/extension.test.ts
150deleted file mode 100644
151index f791a24b825a843fc82ef8cc76caa0f9c1f04bfb..0000000000000000000000000000000000000000
152--- a/test/pi/extension.test.ts
153+++ /dev/null
154@@ -1,311 +0,0 @@
155-import { afterEach, beforeEach, describe, expect, test } from "bun:test"
156-import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
157-import { tmpdir } from "node:os"
158-import { join } from "node:path"
159-import PolicyEngine from "../../src/pi/index"
160-import { loadPiPolicyConfig } from "../../src/pi/config"
161-import { matchesExternalDirectory } from "../../src/pi/static-permissions"
162-
163-const originalAgentDirectory = process.env.PI_CODING_AGENT_DIR
164-const originalKey = process.env.OPENAI_API_KEY
165-const originalFetch = globalThis.fetch
166-let tempDir: string
167-
168-afterEach(() => {
169-  globalThis.fetch = originalFetch
170-  rmSync(tempDir, { recursive: true, force: true })
171-  if (originalAgentDirectory === undefined) delete process.env.PI_CODING_AGENT_DIR
172-  else process.env.PI_CODING_AGENT_DIR = originalAgentDirectory
173-  if (originalKey === undefined) delete process.env.OPENAI_API_KEY
174-  else process.env.OPENAI_API_KEY = originalKey
175-})
176-
177-beforeEach(() => {
178-  tempDir = mkdtempSync(join(tmpdir(), "policy-engine-"))
179-  process.env.PI_CODING_AGENT_DIR = tempDir
180-  process.env.OPENAI_API_KEY = "test-key"
181-})
182-
183-type ToolCallHandler = (event: { toolName: string; input: Record<string, unknown> }, ctx: any) => Promise<unknown>
184-type SessionStartHandler = (event: { reason: string }, ctx: any) => unknown
185-type CommandHandler = (args: string, ctx: any) => Promise<void>
186-type SessionEntry = { customType: string; data: unknown }
187-
188-function loadExtension() {
189-  let toolCall: ToolCallHandler | undefined
190-  let sessionStart: SessionStartHandler | undefined
191-  let allowBash: CommandHandler | undefined
192-  let skipPermissions: CommandHandler | undefined
193-  const entries: SessionEntry[] = []
194-  const events: { name: string; data: unknown }[] = []
195-  PolicyEngine({
196-    on(event: string, callback: ToolCallHandler | SessionStartHandler) {
197-      if (event === "tool_call") toolCall = callback as ToolCallHandler
198-      if (event === "session_start") sessionStart = callback as SessionStartHandler
199-    },
200-    registerCommand(name: string, command: { handler: CommandHandler }) {
201-      if (name === "pe-allow-bash") allowBash = command.handler
202-      if (name === "pe-skip-permissions") skipPermissions = command.handler
203-    },
204-    appendEntry(customType: string, data: unknown) {
205-      entries.push({ customType, data })
206-    },
207-    events: { emit(name: string, data: unknown) { events.push({ name, data }) } },
208-  } as any)
209-  if (!toolCall || !sessionStart || !allowBash || !skipPermissions) throw new Error("policy handlers were not registered")
210-  return { toolCall, sessionStart, allowBash, skipPermissions, entries, events }
211-}
212-
213-function context(
214-  hasUI = false,
215-  confirm: (title: string, message: string) => Promise<boolean> = async () => false,
216-  cwd = tempDir,
217-) {
218-  return {
219-    hasUI,
220-    model: undefined,
221-    modelRegistry: {
222-      find(provider: string, id: string) {
223-        return { provider, id }
224-      },
225-      async complete() {
226-        return {
227-          stopReason: "stop",
228-          content: [{ type: "text", text: '{"decision":"ask","reason":"reviewed","category":"uncertain"}' }],
229-        }
230-      },
231-    },
232-    cwd,
233-    sessionManager: { getSessionId: () => "test-session", getBranch: () => [] },
234-    ui: { confirm, notify() {} },
235-  }
236-}
237-
238-describe("Pi policy extension", () => {
239-  test("merges project configuration over global configuration", async () => {
240-    writeFileSync(join(tempDir, "policy-engine.json"), JSON.stringify({
241-      tools: { bash: "deny", global_tool: "allow" },
242-      externalDirectories: ["/global"],
243-    }))
244-    const projectDirectory = join(tempDir, "project")
245-    mkdirSync(join(projectDirectory, ".pi"), { recursive: true })
246-    writeFileSync(join(projectDirectory, ".pi", "policy-engine.json"), JSON.stringify({
247-      tools: { bash: "allow", project_tool: "deny" },
248-      externalDirectories: ["/project"],
249-    }))
250-
251-    expect(loadPiPolicyConfig(projectDirectory)).toMatchObject({
252-      tools: { bash: "allow", global_tool: "allow", project_tool: "deny" },
253-      externalDirectories: ["/global", "/project"],