@gotgenes/pi-permission-system 33.0.8 → 33.1.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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [33.1.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v33.0.8...pi-permission-system-v33.1.0) (2026-09-23)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** state the relocated tool surface in pi ≥0.86's section shape ([e9980a4](https://github.com/gotgenes/pi-packages/commit/e9980a46af93b2cf668977fc103c803c911d9b12)), closes [#962](https://github.com/gotgenes/pi-packages/issues/962)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **pi-permission-system:** keep a project's own Guidelines section on a pi ≥0.86 prompt ([4f88680](https://github.com/gotgenes/pi-packages/commit/4f88680b309219d6f21d84968101975012f01364)), closes [#962](https://github.com/gotgenes/pi-packages/issues/962)
|
|
18
|
+
* **pi-permission-system:** remove pi's own tool list and rules on a pi ≥0.86 prompt ([596dca1](https://github.com/gotgenes/pi-packages/commit/596dca1a64838162f0662ff06b6a0cc29c538bdc)), closes [#962](https://github.com/gotgenes/pi-packages/issues/962)
|
|
19
|
+
* **pi-permission-system:** keep extension-contributed rules when relocating the tool surface ([00eb11b](https://github.com/gotgenes/pi-packages/commit/00eb11b3ac5bc4c80205fc1c5ffe62d45d8ddc72)), closes [#962](https://github.com/gotgenes/pi-packages/issues/962)
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
|
|
23
|
+
* **pi-permission-system:** describe both prompt shapes in the tool-surface module comments ([5b5ca51](https://github.com/gotgenes/pi-packages/commit/5b5ca512586e2d47b887406e210f847811293192)), closes [#962](https://github.com/gotgenes/pi-packages/issues/962)
|
|
24
|
+
* **pi-permission-system:** describe the tool-surface relocation on pi ≥0.86 section prompts ([5168e40](https://github.com/gotgenes/pi-packages/commit/5168e408c1a0f07c0ae5c9668ec49914b4a2a41b)), closes [#962](https://github.com/gotgenes/pi-packages/issues/962)
|
|
25
|
+
|
|
8
26
|
## [33.0.8](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v33.0.7...pi-permission-system-v33.0.8) (2026-09-23)
|
|
9
27
|
|
|
10
28
|
|
package/docs/configuration.md
CHANGED
|
@@ -1243,11 +1243,11 @@ permission:
|
|
|
1243
1243
|
|
|
1244
1244
|
The extension integrates via Pi's lifecycle hooks:
|
|
1245
1245
|
|
|
1246
|
-
| Hook | Behavior
|
|
1247
|
-
| -------------------- |
|
|
1248
|
-
| `before_agent_start` | Filters the active tool set (restrict-only), restates the
|
|
1249
|
-
| `tool_call` | Enforces permissions for every tool invocation
|
|
1250
|
-
| `input` | Intercepts `/skill:<name>` requests and enforces skill policy
|
|
1246
|
+
| Hook | Behavior |
|
|
1247
|
+
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1248
|
+
| `before_agent_start` | Filters the active tool set (restrict-only), restates the tool list and guidelines at the end of the system prompt to match, and hides denied skills |
|
|
1249
|
+
| `tool_call` | Enforces permissions for every tool invocation |
|
|
1250
|
+
| `input` | Intercepts `/skill:<name>` requests and enforces skill policy |
|
|
1251
1251
|
|
|
1252
1252
|
Additional behaviors:
|
|
1253
1253
|
|
|
@@ -1257,10 +1257,11 @@ Additional behaviors:
|
|
|
1257
1257
|
A tool that stops being active for any other reason (another extension deactivating it, pi unregistering it) is not restored.
|
|
1258
1258
|
- On the turn a tool is restored, it is callable immediately but its `Available tools:` line reappears one turn later: pi builds the prompt parts an extension receives before the extension runs, so the restored tool has no one-line description to render until it is already active
|
|
1259
1259
|
- A tool is removed only when every value under its surface resolves to `deny`; a surface with any reachable `allow` or `ask` pattern stays available (see [Tool Surfaces](#tool-surfaces))
|
|
1260
|
-
- The
|
|
1260
|
+
- The tool list and guidelines are **relocated** rather than edited in place: the copies pi wrote are removed, and this session's own are rendered at the end of the system prompt, after the working directory pi states last.
|
|
1261
|
+
They take the shape pi writes them in: `Available tools:` and `Guidelines:` sections after a `Current working directory:` footer through pi 0.85, and `<tools>` and `<rules>` sections after a `<cwd>` section from pi 0.86.
|
|
1261
1262
|
Each session states its own tool surface, which is what keeps a subagent child's inherited prompt byte-identical to its parent's (see [ADR 0014](decisions/0014-tool-surface-is-node-local-prose.md)); the tool list moves to the end of the prompt for every session, whether or not anything is denied.
|
|
1262
1263
|
Only the copies pi wrote are removed: a custom system prompt (`.pi/SYSTEM.md`, `~/.pi/agent/SYSTEM.md`, `--system-prompt`) keeps its own text untouched, sections and all, because pi writes no tool surface of its own under one — so a prompt that lists tools itself is shown alongside this session's block rather than replaced by it.
|
|
1263
|
-
- The rendered sections follow pi's own rules: a tool is listed only when pi supplied a one-line description for it, and the guideline bullets are the allowed tools' own contributions around pi's built-in ones
|
|
1264
|
+
- The rendered sections follow pi's own rules: a tool is listed only when pi supplied a one-line description for it, and the guideline bullets are the allowed tools' own contributions, then any rules another extension added to `systemPromptOptions.promptGuidelines`, around pi's built-in ones
|
|
1264
1265
|
- The prompt is recomputed and returned on every turn but is stable across turns for a stable policy/agent, so the provider's prompt cache (tools + system prefix) is preserved rather than rewritten each turn.
|
|
1265
1266
|
A policy change is an intentional cache transition, as a mid-session agent switch already is.
|
|
1266
1267
|
- Extension-provided tools like `task`, `mcp`, and third-party tools are handled by exact registered name
|
package/package.json
CHANGED
|
@@ -15,17 +15,23 @@
|
|
|
15
15
|
* and no session edits another's bytes.
|
|
16
16
|
*
|
|
17
17
|
* Removal is bounded to the text this package or Pi wrote. The prompt is split
|
|
18
|
-
* at
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* at the cwd layer Pi writes last and unconditionally: a
|
|
19
|
+
* `Current working directory:` footer through Pi 0.85, a `<cwd>` section from
|
|
20
|
+
* 0.86. Everything after it was appended by an extension, and everything
|
|
21
|
+
* before it is Pi's own only when Pi did not build the prompt from a
|
|
22
|
+
* `customPrompt`. Under a custom prompt Pi writes no tool surface at all, so a
|
|
23
|
+
* section matched above the cwd layer is a user's or another extension's, and
|
|
24
|
+
* removing it destroyed their text (#919, #932). On the 0.86 shape even a
|
|
25
|
+
* Pi-authored head is searched only for Pi's own `<tools>` and `<rules>`,
|
|
26
|
+
* bounded to where Pi writes them, since the plain headers it no longer
|
|
27
|
+
* writes would match only a context file's text.
|
|
24
28
|
*
|
|
25
|
-
* Rendering follows `buildSystemPrompt`'s own rules
|
|
26
|
-
* when it has a snippet
|
|
27
|
-
* `promptGuidelines
|
|
28
|
-
*
|
|
29
|
+
* Rendering follows `buildSystemPrompt`'s own rules (a tool is listed only
|
|
30
|
+
* when it has a snippet; the guideline bullets are the allowed tools' own
|
|
31
|
+
* `promptGuidelines`, then other extensions' rules, around Pi's built-in
|
|
32
|
+
* ones) and its shape (plain headers or tagged sections, matching the
|
|
33
|
+
* prompt), so the block reads as the one Pi would have written for this
|
|
34
|
+
* session's real surface.
|
|
29
35
|
*/
|
|
30
36
|
|
|
31
37
|
/** What a session's tool surface renders from. */
|
|
@@ -36,6 +42,16 @@ export interface ToolSurfaceInputs {
|
|
|
36
42
|
readonly toolSnippets: Readonly<Record<string, string>>;
|
|
37
43
|
/** Guideline bullets each tool contributes, keyed by tool name. */
|
|
38
44
|
readonly guidelinesByTool: ReadonlyMap<string, readonly string[]>;
|
|
45
|
+
/**
|
|
46
|
+
* `systemPromptOptions.promptGuidelines`: bullets Pi writes into its rules
|
|
47
|
+
* after the tools' own.
|
|
48
|
+
*
|
|
49
|
+
* Only a bullet no registered tool contributes is carried. From Pi 0.86 the
|
|
50
|
+
* field holds what other extensions added; through 0.85 it *is* the tools'
|
|
51
|
+
* guidelines, flattened, and a denied tool's bullet must not return by this
|
|
52
|
+
* route.
|
|
53
|
+
*/
|
|
54
|
+
readonly promptGuidelines: readonly string[];
|
|
39
55
|
/**
|
|
40
56
|
* Whether Pi wrote the prompt's preamble itself.
|
|
41
57
|
*
|
|
@@ -75,6 +91,31 @@ const EMPTY_LIST_PLACEHOLDER = "(none)";
|
|
|
75
91
|
*/
|
|
76
92
|
const PROMPT_FOOTER_PREFIX = "Current working directory: ";
|
|
77
93
|
|
|
94
|
+
/**
|
|
95
|
+
* The tags of the section Pi renders the working directory into from 0.86,
|
|
96
|
+
* last among its own sections and in both of its branches.
|
|
97
|
+
*/
|
|
98
|
+
const CWD_SECTION_OPEN = "<cwd>";
|
|
99
|
+
const CWD_SECTION_CLOSE = "</cwd>";
|
|
100
|
+
|
|
101
|
+
/** The sections Pi 0.86+ writes its tool surface into, in the order it writes them. */
|
|
102
|
+
const TOOL_SURFACE_SECTION_NAMES = ["tools", "rules"] as const;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Opening tags of the sections Pi writes after its tool surface.
|
|
106
|
+
*
|
|
107
|
+
* Pi writes `<tools>` and `<rules>` directly under its preamble and ahead of
|
|
108
|
+
* every one of these, so a match that opens or closes past the first of them
|
|
109
|
+
* is text somebody quoted, not Pi's.
|
|
110
|
+
*/
|
|
111
|
+
const LATER_PI_SECTION_OPENS: ReadonlySet<string> = new Set([
|
|
112
|
+
"<docs>",
|
|
113
|
+
"<addendum>",
|
|
114
|
+
"<project_context>",
|
|
115
|
+
"<skills>",
|
|
116
|
+
CWD_SECTION_OPEN,
|
|
117
|
+
]);
|
|
118
|
+
|
|
78
119
|
/** Pi's two unconditional guideline bullets, in the order it writes them. */
|
|
79
120
|
const UNIVERSAL_GUIDELINES: readonly string[] = [
|
|
80
121
|
"Be concise in your responses",
|
|
@@ -93,43 +134,116 @@ export function renderToolSurface(
|
|
|
93
134
|
inputs: ToolSurfaceInputs,
|
|
94
135
|
): string {
|
|
95
136
|
const lines = normalizePrompt(systemPrompt).split("\n");
|
|
96
|
-
const tailStart =
|
|
137
|
+
const { layout, tailStart } = detectPromptLayout(lines);
|
|
97
138
|
const body = [
|
|
98
|
-
settleRegion(
|
|
99
|
-
|
|
139
|
+
settleRegion(
|
|
140
|
+
lines.slice(0, tailStart),
|
|
141
|
+
inputs.piAuthoredPreamble ? layout.removePiSurface : null,
|
|
142
|
+
),
|
|
143
|
+
settleRegion(lines.slice(tailStart), layout.removeRelocatedSurface),
|
|
100
144
|
]
|
|
101
145
|
.filter((region) => region.length > 0)
|
|
102
146
|
.join("\n")
|
|
103
147
|
.trimEnd();
|
|
104
|
-
const block =
|
|
148
|
+
const block = layout.renderBlock(toolSurfaceBullets(inputs));
|
|
105
149
|
|
|
106
150
|
return body.length > 0 ? `${body}\n\n${block}` : block;
|
|
107
151
|
}
|
|
108
152
|
|
|
153
|
+
/** Lines removed from one region of the prompt; the rest are returned in order. */
|
|
154
|
+
type RegionRemoval = (lines: readonly string[]) => string[];
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* How one prompt shape bounds and writes the tool surface.
|
|
158
|
+
*
|
|
159
|
+
* Pi has written its prompt in more than one shape, and each shape answers the
|
|
160
|
+
* same three questions differently, so the shape is decided once per prompt
|
|
161
|
+
* and every answer is read off the layout it selected.
|
|
162
|
+
*/
|
|
163
|
+
interface PromptLayout {
|
|
164
|
+
/** Pi's own tool surface, removed from the head when Pi wrote the preamble. */
|
|
165
|
+
readonly removePiSurface: RegionRemoval;
|
|
166
|
+
/** A relocated block (this package's or a peer's), removed from the tail. */
|
|
167
|
+
readonly removeRelocatedSurface: RegionRemoval;
|
|
168
|
+
/** This session's block, in this layout's shape. */
|
|
169
|
+
readonly renderBlock: (bullets: ToolSurfaceBullets) => string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A prompt's layout, and the line at which the text extensions appended begins. */
|
|
173
|
+
interface DetectedLayout {
|
|
174
|
+
readonly layout: PromptLayout;
|
|
175
|
+
readonly tailStart: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The shape Pi wrote through 0.85: `Available tools:` and `Guidelines:`
|
|
180
|
+
* sections in its preamble, and a `Current working directory:` footer last.
|
|
181
|
+
*/
|
|
182
|
+
const HEADER_LAYOUT: PromptLayout = {
|
|
183
|
+
removePiSurface: removeToolSurfaceSections,
|
|
184
|
+
removeRelocatedSurface: removeToolSurfaceSections,
|
|
185
|
+
renderBlock: renderHeaderBlock,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The shape Pi writes from 0.86: an untagged preamble, then `<name>` sections
|
|
190
|
+
* joined by a blank line (`<tools>` and `<rules>` among them), and a `<cwd>`
|
|
191
|
+
* section last among its own.
|
|
192
|
+
*/
|
|
193
|
+
const SECTION_LAYOUT: PromptLayout = {
|
|
194
|
+
removePiSurface: removePiToolSurfaceSections,
|
|
195
|
+
removeRelocatedSurface: removeRelocatedToolSurface,
|
|
196
|
+
renderBlock: renderSectionBlock,
|
|
197
|
+
};
|
|
198
|
+
|
|
109
199
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
200
|
+
* The prompt's layout, read off the cwd layer Pi wrote, and where the text
|
|
201
|
+
* extensions appended after that layer begins.
|
|
112
202
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
203
|
+
* Pi writes its cwd layer last among its own, in both of its branches: a
|
|
204
|
+
* `Current working directory:` footer through 0.85, a `<cwd>` section from
|
|
205
|
+
* 0.86. So whichever of the two sits later is Pi's, and a line of either shape
|
|
206
|
+
* quoted in a context file or a custom prompt is always above it. The shape is
|
|
207
|
+
* told apart by which layer is present, never by version sniffing.
|
|
115
208
|
*
|
|
116
|
-
*
|
|
117
|
-
* a block appended to *that* prompt cannot be found and replaced, and
|
|
118
|
-
* preamble would collect a second one. Pi writes
|
|
119
|
-
* branches, so reaching this needs a downstream rewrite of
|
|
120
|
-
*
|
|
121
|
-
* reads the same line.
|
|
209
|
+
* A prompt carrying neither is read as the header layout and treated as all
|
|
210
|
+
* head, so a block appended to *that* prompt cannot be found and replaced, and
|
|
211
|
+
* a custom preamble would collect a second one. Pi writes a cwd layer in both
|
|
212
|
+
* branches of both renderers, so reaching this needs a downstream rewrite of
|
|
213
|
+
* Pi's whole output.
|
|
122
214
|
*/
|
|
123
|
-
function
|
|
215
|
+
function detectPromptLayout(lines: readonly string[]): DetectedLayout {
|
|
124
216
|
const footerAt = lines.findLastIndex((line) =>
|
|
125
217
|
line.startsWith(PROMPT_FOOTER_PREFIX),
|
|
126
218
|
);
|
|
127
|
-
|
|
219
|
+
const cwdCloseAt = lastCwdSectionClose(lines);
|
|
220
|
+
if (cwdCloseAt > footerAt) {
|
|
221
|
+
return { layout: SECTION_LAYOUT, tailStart: cwdCloseAt + 1 };
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
layout: HEADER_LAYOUT,
|
|
225
|
+
tailStart: footerAt === -1 ? lines.length : footerAt + 1,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Line index of the closing tag of the last `<cwd>` section, or -1 when there
|
|
231
|
+
* is none.
|
|
232
|
+
*
|
|
233
|
+
* Pi renders the section as exactly three lines (the opening tag, the
|
|
234
|
+
* directory, the closing tag), so that is the shape matched, the way the
|
|
235
|
+
* footer is matched by its prefix.
|
|
236
|
+
*/
|
|
237
|
+
function lastCwdSectionClose(lines: readonly string[]): number {
|
|
238
|
+
return lines.findLastIndex(
|
|
239
|
+
(line, index) =>
|
|
240
|
+
line === CWD_SECTION_CLOSE && lines[index - 2] === CWD_SECTION_OPEN,
|
|
241
|
+
);
|
|
128
242
|
}
|
|
129
243
|
|
|
130
244
|
/**
|
|
131
245
|
* One region's surviving text: its sections removed, when they are ours to
|
|
132
|
-
* remove
|
|
246
|
+
* remove, or returned as it arrived when `removal` is `null`.
|
|
133
247
|
*
|
|
134
248
|
* Blank runs are collapsed only where a removal opened one, so a region this
|
|
135
249
|
* pass took nothing out of is returned exactly as it arrived rather than
|
|
@@ -137,12 +251,12 @@ function extensionTailStart(lines: readonly string[]): number {
|
|
|
137
251
|
*/
|
|
138
252
|
function settleRegion(
|
|
139
253
|
lines: readonly string[],
|
|
140
|
-
|
|
254
|
+
removal: RegionRemoval | null,
|
|
141
255
|
): string {
|
|
142
|
-
if (!
|
|
256
|
+
if (!removal) {
|
|
143
257
|
return lines.join("\n");
|
|
144
258
|
}
|
|
145
|
-
const kept =
|
|
259
|
+
const kept = removal(lines);
|
|
146
260
|
const text = kept.join("\n");
|
|
147
261
|
return kept.length === lines.length ? text : collapseExtraBlankLines(text);
|
|
148
262
|
}
|
|
@@ -176,45 +290,155 @@ function removeToolSurfaceSections(lines: readonly string[]): string[] {
|
|
|
176
290
|
);
|
|
177
291
|
}
|
|
178
292
|
|
|
179
|
-
/**
|
|
180
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Remove Pi 0.86+'s own `<tools>` and `<rules>` sections, tags included.
|
|
295
|
+
*
|
|
296
|
+
* A section is Pi's only when it opens and closes before the first section Pi
|
|
297
|
+
* writes after it; the plain headers `removeToolSurfaceSections` matches are
|
|
298
|
+
* never searched here, because Pi writes none on this shape and every match
|
|
299
|
+
* would be a user's or another extension's text.
|
|
300
|
+
*/
|
|
301
|
+
function removePiToolSurfaceSections(head: readonly string[]): string[] {
|
|
302
|
+
return removeTaggedToolSurface(head, laterPiSectionStart);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Remove a relocated block from the extension tail, in either shape.
|
|
307
|
+
*
|
|
308
|
+
* This package writes the tagged shape here, and a peer writer may write
|
|
309
|
+
* either, so both are removed: the pass stays safe to apply to its own output
|
|
310
|
+
* and order-independent with a second writer.
|
|
311
|
+
*/
|
|
312
|
+
function removeRelocatedToolSurface(tail: readonly string[]): string[] {
|
|
313
|
+
return removeToolSurfaceSections(
|
|
314
|
+
removeTaggedToolSurface(tail, (lines) => lines.length),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Remove the first `<tools>` and the first `<rules>` section, each only when
|
|
320
|
+
* it closes before the line `limitOf` names.
|
|
321
|
+
*/
|
|
322
|
+
function removeTaggedToolSurface(
|
|
323
|
+
lines: readonly string[],
|
|
324
|
+
limitOf: (lines: readonly string[]) => number,
|
|
325
|
+
): string[] {
|
|
326
|
+
let remaining = [...lines];
|
|
327
|
+
for (const name of TOOL_SURFACE_SECTION_NAMES) {
|
|
328
|
+
const section = findTaggedSection(remaining, name, limitOf(remaining));
|
|
329
|
+
if (section) {
|
|
330
|
+
remaining = [
|
|
331
|
+
...remaining.slice(0, section.start),
|
|
332
|
+
...remaining.slice(section.end),
|
|
333
|
+
];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return remaining;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Line index of the first section Pi writes after its tool surface. */
|
|
340
|
+
function laterPiSectionStart(lines: readonly string[]): number {
|
|
341
|
+
const at = lines.findIndex((line) => LATER_PI_SECTION_OPENS.has(line));
|
|
342
|
+
return at === -1 ? lines.length : at;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The first `<name>` section, opening tag through closing tag, that ends
|
|
347
|
+
* before `limit`; `null` when there is none.
|
|
348
|
+
*/
|
|
349
|
+
function findTaggedSection(
|
|
350
|
+
lines: readonly string[],
|
|
351
|
+
name: string,
|
|
352
|
+
limit: number,
|
|
353
|
+
): LineSection | null {
|
|
354
|
+
const start = lines.indexOf(`<${name}>`);
|
|
355
|
+
if (start === -1 || start >= limit) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const closeAt = lines.indexOf(`</${name}>`, start + 1);
|
|
359
|
+
if (closeAt === -1 || closeAt >= limit) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
return { start, end: closeAt + 1 };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* This session's tool surface, as Pi 0.86+ would have rendered it: a `<tools>`
|
|
367
|
+
* section, when any allowed tool has a snippet, then a `<rules>` section.
|
|
368
|
+
*/
|
|
369
|
+
function renderSectionBlock(bullets: ToolSurfaceBullets): string {
|
|
370
|
+
const sections: string[] = [];
|
|
371
|
+
if (bullets.tools.length > 0) {
|
|
372
|
+
sections.push(taggedSection("tools", bullets.tools));
|
|
373
|
+
}
|
|
374
|
+
sections.push(taggedSection("rules", bullets.rules));
|
|
375
|
+
|
|
376
|
+
return sections.join("\n\n");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** A section in the form Pi 0.86+ renders: its tags on their own lines. */
|
|
380
|
+
function taggedSection(name: string, lines: readonly string[]): string {
|
|
381
|
+
return [`<${name}>`, ...lines, `</${name}>`].join("\n");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** This session's tool surface, as Pi through 0.85 would have rendered it. */
|
|
385
|
+
function renderHeaderBlock(bullets: ToolSurfaceBullets): string {
|
|
181
386
|
const sections: string[] = [];
|
|
182
387
|
|
|
183
|
-
const toolList = renderAvailableTools(
|
|
388
|
+
const toolList = renderAvailableTools(bullets.tools);
|
|
184
389
|
if (toolList) {
|
|
185
390
|
sections.push(toolList);
|
|
186
391
|
}
|
|
187
|
-
sections.push(renderGuidelines(
|
|
392
|
+
sections.push(renderGuidelines(bullets.rules));
|
|
188
393
|
|
|
189
394
|
return sections.join("\n\n");
|
|
190
395
|
}
|
|
191
396
|
|
|
397
|
+
/** The `Available tools:` section, or `null` when it lists no tool. */
|
|
398
|
+
function renderAvailableTools(bullets: readonly string[]): string | null {
|
|
399
|
+
return bullets.length > 0
|
|
400
|
+
? [AVAILABLE_TOOLS_SECTION_HEADER, ...bullets].join("\n")
|
|
401
|
+
: null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The `Guidelines:` section. */
|
|
405
|
+
function renderGuidelines(bullets: readonly string[]): string {
|
|
406
|
+
return [GUIDELINES_SECTION_HEADER, ...bullets].join("\n");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** The bullets a tool-surface block wraps, whatever shape wraps them. */
|
|
410
|
+
interface ToolSurfaceBullets {
|
|
411
|
+
/** `- name: snippet` lines; empty when no allowed tool has a snippet. */
|
|
412
|
+
readonly tools: readonly string[];
|
|
413
|
+
/** `- rule` lines, in `buildSystemPrompt`'s order. */
|
|
414
|
+
readonly rules: readonly string[];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function toolSurfaceBullets(inputs: ToolSurfaceInputs): ToolSurfaceBullets {
|
|
418
|
+
return { tools: toolBullets(inputs), rules: ruleBullets(inputs) };
|
|
419
|
+
}
|
|
420
|
+
|
|
192
421
|
/**
|
|
193
|
-
*
|
|
194
|
-
* those tools has a snippet.
|
|
422
|
+
* One bullet per allowed tool that has a snippet.
|
|
195
423
|
*
|
|
196
424
|
* Pi lists a tool only when the caller supplied a one-line snippet for it, so
|
|
197
425
|
* a tool without one is left unlisted here too rather than rendered bare.
|
|
198
426
|
*/
|
|
199
|
-
function
|
|
200
|
-
|
|
427
|
+
function toolBullets(inputs: ToolSurfaceInputs): string[] {
|
|
428
|
+
return inputs.allowedTools
|
|
201
429
|
.map((toolName) => ({ toolName, snippet: inputs.toolSnippets[toolName] }))
|
|
202
430
|
.filter((tool) => Boolean(tool.snippet))
|
|
203
431
|
.map((tool) => `- ${tool.toolName}: ${tool.snippet}`);
|
|
204
|
-
|
|
205
|
-
return bullets.length > 0
|
|
206
|
-
? [AVAILABLE_TOOLS_SECTION_HEADER, ...bullets].join("\n")
|
|
207
|
-
: null;
|
|
208
432
|
}
|
|
209
433
|
|
|
210
434
|
/**
|
|
211
|
-
* The
|
|
435
|
+
* The guideline bullets for the allowed set.
|
|
212
436
|
*
|
|
213
437
|
* Mirrors `buildSystemPrompt`'s assembly: its conditional file-exploration
|
|
214
|
-
* bullet first, then each allowed tool's own contributions, then
|
|
215
|
-
* unconditional bullets — de-duplicated in first-seen order, as Pi does.
|
|
438
|
+
* bullet first, then each allowed tool's own contributions, then the bullets
|
|
439
|
+
* other extensions added, then its two unconditional bullets — de-duplicated in first-seen order, as Pi does.
|
|
216
440
|
*/
|
|
217
|
-
function
|
|
441
|
+
function ruleBullets(inputs: ToolSurfaceInputs): string[] {
|
|
218
442
|
const bullets: string[] = [];
|
|
219
443
|
const seen = new Set<string>();
|
|
220
444
|
const addGuideline = (guideline: string): void => {
|
|
@@ -237,14 +461,27 @@ function renderGuidelines(inputs: ToolSurfaceInputs): string {
|
|
|
237
461
|
}
|
|
238
462
|
}
|
|
239
463
|
|
|
464
|
+
for (const guideline of extensionGuidelines(inputs)) {
|
|
465
|
+
addGuideline(guideline);
|
|
466
|
+
}
|
|
467
|
+
|
|
240
468
|
for (const guideline of UNIVERSAL_GUIDELINES) {
|
|
241
469
|
addGuideline(guideline);
|
|
242
470
|
}
|
|
243
471
|
|
|
244
|
-
return
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
472
|
+
return bullets.map((bullet) => `- ${bullet}`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** The `promptGuidelines` bullets no registered tool contributes. */
|
|
476
|
+
function extensionGuidelines(inputs: ToolSurfaceInputs): string[] {
|
|
477
|
+
const toolGuidelines = new Set(
|
|
478
|
+
[...inputs.guidelinesByTool.values()]
|
|
479
|
+
.flat()
|
|
480
|
+
.map((guideline) => guideline.trim()),
|
|
481
|
+
);
|
|
482
|
+
return inputs.promptGuidelines.filter(
|
|
483
|
+
(guideline) => !toolGuidelines.has(guideline.trim()),
|
|
484
|
+
);
|
|
248
485
|
}
|
|
249
486
|
|
|
250
487
|
/**
|
|
@@ -24,10 +24,13 @@ interface BeforeAgentStartPayload {
|
|
|
24
24
|
* wrote — including in a child, whose inherited identity carries none.
|
|
25
25
|
* `customPrompt` says whether Pi wrote a preamble at all: under one, it
|
|
26
26
|
* writes no tool surface, so there is nothing of Pi's to remove.
|
|
27
|
+
* `promptGuidelines` carries rules other extensions added, which removing
|
|
28
|
+
* Pi's own rules section would otherwise drop.
|
|
27
29
|
*/
|
|
28
30
|
systemPromptOptions?: {
|
|
29
31
|
customPrompt?: string;
|
|
30
32
|
toolSnippets?: Record<string, string>;
|
|
33
|
+
promptGuidelines?: readonly string[];
|
|
31
34
|
};
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -109,6 +112,7 @@ export class AgentPrepHandler {
|
|
|
109
112
|
allowedTools,
|
|
110
113
|
toolSnippets: event.systemPromptOptions?.toolSnippets ?? {},
|
|
111
114
|
guidelinesByTool: registered.guidelinesByTool,
|
|
115
|
+
promptGuidelines: event.systemPromptOptions?.promptGuidelines ?? [],
|
|
112
116
|
// Pi's own `if (customPrompt)` test, so an empty string reads here the
|
|
113
117
|
// way it reads there: as no custom prompt at all.
|
|
114
118
|
piAuthoredPreamble: !event.systemPromptOptions?.customPrompt,
|