system-prompt-omni 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,18 +46,35 @@ confirmation. Run `/reload` after exporting to load the new file.
46
46
 
47
47
  ## Prompt recording
48
48
 
49
- Before each provider request, the package compares the SHA-256 hash of the effective system
50
- prompt with the latest snapshot on the active session branch. When it changes, a durable custom
51
- session entry with type `pi-system-prompt` records:
49
+ Before each provider request, the package compares the SHA-256 hashes of the effective system
50
+ prompt and serialized active tool definitions with the latest snapshot on the active session
51
+ branch. When either changes, a durable custom session entry with type `pi-system-prompt` records:
52
52
 
53
53
  - the full effective system prompt, its hash, and UTF-8 byte length;
54
54
  - provider, model, API, and thinking-level metadata;
55
- - active tool names and definitions; and
55
+ - `serializedTools`, the tool-definition value copied from Pi's final provider payload and
56
+ encoded as a compact JSON string; and
56
57
  - provider-level system/developer instructions extracted from the serialized request.
57
58
 
59
+ No second tool representation is stored. Tool definitions are not rebuilt or sorted, so array
60
+ order and provider-specific structure stay identical to the `before_provider_request` payload.
61
+ For OpenAI Chat Completions, for example, the value has the same shape as
62
+ `current_harness_tools.json`:
63
+
64
+ ```json
65
+ {
66
+ "serializedTools": "[{\"type\":\"function\",\"function\":{\"name\":\"read\",\"description\":\"Read files\",\"parameters\":{\"type\":\"object\"}}}]"
67
+ }
68
+ ```
69
+
70
+ The exact representation follows the active API. OpenAI Responses, Anthropic, Google, Bedrock,
71
+ and `pi-messages` therefore retain their own transmitted tool schema rather than being converted
72
+ to the Chat Completions shape. Load this package after extensions that rewrite provider requests
73
+ so it observes their final tool order and definitions.
74
+
58
75
  The recorder never stores the complete provider payload, never adds its custom entries to the
59
- LLM context, and returns the payload unchanged. Load this package after extensions that rewrite
60
- provider requests if you want those rewritten provider instructions included in the snapshot.
76
+ LLM context, and returns the payload unchanged. Loading it last also ensures rewritten provider
77
+ instructions are included in the snapshot.
61
78
 
62
79
  ## Install
63
80
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Persist the effective system prompt when it changes.
2
+ * Persist the effective system prompt or provider tool definitions when either changes.
3
3
  *
4
4
  * Custom entries are durable but are not added to the LLM context. Load this
5
5
  * extension last if another extension rewrites the serialized provider payload
@@ -8,8 +8,9 @@
8
8
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
9
 
10
10
  import {
11
+ extractSerializedTools,
11
12
  extractProviderSystemInstructions,
12
- latestSystemPromptSha256,
13
+ latestSystemPromptSnapshotHashes,
13
14
  sha256,
14
15
  SYSTEM_PROMPT_ENTRY_SCHEMA_VERSION,
15
16
  SYSTEM_PROMPT_ENTRY_TYPE,
@@ -19,22 +20,24 @@ export default function systemPromptRecorder(pi: ExtensionAPI): void {
19
20
  pi.on("before_provider_request", (event, ctx) => {
20
21
  const systemPrompt = ctx.getSystemPrompt();
21
22
  const systemPromptSha256 = sha256(systemPrompt);
23
+ const serializedTools = extractSerializedTools(event.payload);
24
+ const serializedToolsSha256 =
25
+ serializedTools === undefined ? undefined : sha256(serializedTools);
22
26
 
23
- // Compare with the latest snapshot on the active branch. This also avoids
24
- // duplicates after process restarts, session resume, and extension reloads.
25
- if (latestSystemPromptSha256(ctx.sessionManager.getBranch()) === systemPromptSha256) {
27
+ // Compare both prompt and tool definitions with the latest snapshot on the
28
+ // active branch. This also avoids duplicates after process restarts,
29
+ // session resume, and extension reloads.
30
+ const latestHashes = latestSystemPromptSnapshotHashes(ctx.sessionManager.getBranch());
31
+ if (
32
+ latestHashes?.systemPromptSha256 === systemPromptSha256 &&
33
+ latestHashes.capturesSerializedTools &&
34
+ latestHashes.serializedToolsSha256 === serializedToolsSha256
35
+ ) {
26
36
  return undefined;
27
37
  }
28
38
 
29
39
  const providerSystemInstructions = extractProviderSystemInstructions(event.payload);
30
40
  const model = ctx.model;
31
- const activeTools = pi.getActiveTools();
32
- const toolsByName = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
33
- const tools = activeTools.flatMap((name) => {
34
- const tool = toolsByName.get(name);
35
- if (!tool) return [];
36
- return [{ name: tool.name, description: tool.description, parameters: tool.parameters }];
37
- });
38
41
 
39
42
  pi.appendEntry(SYSTEM_PROMPT_ENTRY_TYPE, {
40
43
  schemaVersion: SYSTEM_PROMPT_ENTRY_SCHEMA_VERSION,
@@ -47,8 +50,7 @@ export default function systemPromptRecorder(pi: ExtensionAPI): void {
47
50
  modelId: model?.id,
48
51
  api: model?.api,
49
52
  thinkingLevel: pi.getThinkingLevel(),
50
- activeTools,
51
- tools,
53
+ ...(serializedTools === undefined ? {} : { serializedTools }),
52
54
  providerSystemInstructions,
53
55
  });
54
56
 
@@ -1,7 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
3
  export const SYSTEM_PROMPT_ENTRY_TYPE = "pi-system-prompt";
4
- export const SYSTEM_PROMPT_ENTRY_SCHEMA_VERSION = 1;
4
+ export const SYSTEM_PROMPT_ENTRY_SCHEMA_VERSION = 2;
5
+
6
+ export interface SystemPromptSnapshotHashes {
7
+ systemPromptSha256?: string;
8
+ serializedToolsSha256?: string;
9
+ capturesSerializedTools: boolean;
10
+ }
5
11
 
6
12
  interface CustomEntryLike {
7
13
  type?: unknown;
@@ -21,6 +27,45 @@ function jsonSnapshot(value: unknown): unknown {
21
27
  }
22
28
  }
23
29
 
30
+ function valueAtPath(root: Record<string, unknown>, path: readonly string[]): unknown {
31
+ let value: unknown = root;
32
+ for (const segment of path) {
33
+ if (!isRecord(value) || !Object.prototype.hasOwnProperty.call(value, segment)) {
34
+ return undefined;
35
+ }
36
+ value = value[segment];
37
+ }
38
+ return value;
39
+ }
40
+
41
+ /**
42
+ * Serialize the tool definitions from Pi's final provider payload without
43
+ * rebuilding, sorting, or normalizing them. The paths cover Pi's current
44
+ * provider transports, including OpenAI, Anthropic, Google, Bedrock, and the
45
+ * pi-messages API.
46
+ */
47
+ export function extractSerializedTools(payload: unknown): string | undefined {
48
+ if (!isRecord(payload)) return undefined;
49
+
50
+ const toolPaths = [
51
+ ["tools"],
52
+ ["config", "tools"],
53
+ ["toolConfig", "tools"],
54
+ ["context", "tools"],
55
+ ["body", "tools"],
56
+ ["body", "config", "tools"],
57
+ ["body", "toolConfig", "tools"],
58
+ ["body", "context", "tools"],
59
+ ] as const;
60
+
61
+ for (const path of toolPaths) {
62
+ const tools = valueAtPath(payload, path);
63
+ if (tools === undefined) continue;
64
+ return JSON.stringify(tools);
65
+ }
66
+ return undefined;
67
+ }
68
+
24
69
  function pushField(
25
70
  result: Array<Record<string, unknown>>,
26
71
  root: Record<string, unknown>,
@@ -109,3 +154,31 @@ export function latestSystemPromptSha256(entries: readonly CustomEntryLike[]): s
109
154
  }
110
155
  return undefined;
111
156
  }
157
+
158
+ /** Return the hashes from the newest recorder snapshot on the active branch. */
159
+ export function latestSystemPromptSnapshotHashes(
160
+ entries: readonly CustomEntryLike[],
161
+ ): SystemPromptSnapshotHashes | undefined {
162
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
163
+ const entry = entries[index];
164
+ if (entry?.type !== "custom" || entry.customType !== SYSTEM_PROMPT_ENTRY_TYPE) continue;
165
+ if (!isRecord(entry.data)) continue;
166
+
167
+ const systemPromptSha256 =
168
+ typeof entry.data.systemPromptSha256 === "string"
169
+ ? entry.data.systemPromptSha256
170
+ : typeof entry.data.systemPrompt === "string"
171
+ ? sha256(entry.data.systemPrompt)
172
+ : undefined;
173
+ const serializedToolsSha256 =
174
+ typeof entry.data.serializedTools === "string"
175
+ ? sha256(entry.data.serializedTools)
176
+ : undefined;
177
+ const capturesSerializedTools =
178
+ typeof entry.data.schemaVersion === "number" &&
179
+ entry.data.schemaVersion >= SYSTEM_PROMPT_ENTRY_SCHEMA_VERSION;
180
+
181
+ return { systemPromptSha256, serializedToolsSha256, capturesSerializedTools };
182
+ }
183
+ return undefined;
184
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "system-prompt-omni",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Display, export, and record the effective pi coding agent system prompt",
6
6
  "keywords": ["pi-package", "pi-extension", "system-prompt", "debugging", "recorder"],