@danypops/papyrus 0.15.2 → 0.16.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.
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { BuildSystemPromptOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN } from "../../src/constants.ts";
|
|
3
|
+
import type { ContextSegmentItem } from "./context-budget.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Splits Pi's base system prompt into real structural sub-segments instead of one opaque
|
|
7
|
+
* number, using BeforeAgentStartEvent's own systemPromptOptions field -- Pi's own doc comment
|
|
8
|
+
* on it: "Extensions can inspect this to understand what Pi loaded without re-discovering
|
|
9
|
+
* resources." No new hook, no new risk: before_agent_start is already wired.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately measures each INPUT's raw content size (tool snippet text, skill metadata,
|
|
12
|
+
* context file content) rather than attempting to byte-for-byte reproduce Pi's internal
|
|
13
|
+
* wrapping/tag format -- buildSystemPrompt() and formatSkillsForPrompt() are Pi-internal
|
|
14
|
+
* functions, not part of the public extension API Papyrus can call, so reproducing their
|
|
15
|
+
* exact template text here would be a real, silent drift risk if Pi ever changes it. The
|
|
16
|
+
* remainder item absorbs whatever wrapping/template text this doesn't attribute, so the
|
|
17
|
+
* segment's total always still matches the real observed prompt length exactly -- honesty
|
|
18
|
+
* preserved even though individual sub-segment sizes are approximate, matching the same
|
|
19
|
+
* known-segments-plus-honest-remainder pattern used everywhere else in this breakdown.
|
|
20
|
+
*/
|
|
21
|
+
export function buildBasePromptItems(options: BuildSystemPromptOptions, totalCharacters: number): ContextSegmentItem[] {
|
|
22
|
+
const items: ContextSegmentItem[] = [];
|
|
23
|
+
|
|
24
|
+
const toolSnippetEntries = Object.entries(options.toolSnippets ?? {});
|
|
25
|
+
// Mirrors buildSystemPrompt()'s own "- name: snippet\n" line shape closely enough to be a
|
|
26
|
+
// fair estimate without importing Pi-internal formatting code.
|
|
27
|
+
const toolSnippetsCharacters = toolSnippetEntries.reduce((sum, [name, snippet]) => sum + name.length + snippet.length + 4, 0);
|
|
28
|
+
if (toolSnippetsCharacters > 0) {
|
|
29
|
+
items.push({ label: `Tool snippets (${toolSnippetEntries.length} tools)`, estimatedTokens: toCeilTokens(toolSnippetsCharacters) });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const visibleSkills = (options.skills ?? []).filter((skill) => !skill.disableModelInvocation);
|
|
33
|
+
const skillsCharacters = visibleSkills.reduce((sum, skill) => sum + skill.name.length + skill.description.length + skill.filePath.length + 20, 0);
|
|
34
|
+
if (skillsCharacters > 0) {
|
|
35
|
+
items.push({ label: `Skills catalog (${visibleSkills.length} skills)`, estimatedTokens: toCeilTokens(skillsCharacters) });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const contextFiles = options.contextFiles ?? [];
|
|
39
|
+
const contextFilesCharacters = contextFiles.reduce((sum, file) => sum + file.path.length + file.content.length + 40, 0);
|
|
40
|
+
if (contextFilesCharacters > 0) {
|
|
41
|
+
items.push({ label: `Project context files (${contextFiles.length}, e.g. AGENTS.md)`, estimatedTokens: toCeilTokens(contextFilesCharacters) });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const knownCharacters = toolSnippetsCharacters + skillsCharacters + contextFilesCharacters;
|
|
45
|
+
const remainderCharacters = Math.max(0, totalCharacters - knownCharacters);
|
|
46
|
+
if (remainderCharacters > 0 || items.length === 0) {
|
|
47
|
+
items.push({ label: "Base template, guidelines, and formatting", estimatedTokens: toCeilTokens(remainderCharacters) });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return items;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toCeilTokens(characters: number): number {
|
|
54
|
+
return Math.ceil(characters / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN);
|
|
55
|
+
}
|
|
@@ -295,6 +295,8 @@ export interface BuildContextBreakdownInput {
|
|
|
295
295
|
skills: SkillCatalogFootprint;
|
|
296
296
|
/** Pi's own base system prompt size, cached from the last observed before_agent_start turn. Null before any turn has run yet. */
|
|
297
297
|
basePromptEstimatedTokens: number | null;
|
|
298
|
+
/** Structural sub-breakdown (tool snippets, Skills, context files, template remainder) from the same cached observation, built by buildBasePromptItems(). Empty when basePromptEstimatedTokens is null. */
|
|
299
|
+
basePromptItems?: ContextSegmentItem[];
|
|
298
300
|
/** From buildMessageHistoryTree() against the live session's real tree (ctx.sessionManager.getTree()). */
|
|
299
301
|
messageHistoryItems: ContextSegmentItem[];
|
|
300
302
|
/** buildMessageHistoryTree()'s activeTokens -- only entries on the current active path count toward the segment total; an abandoned /tree branch still appears in messageHistoryItems but contributes zero here. */
|
|
@@ -416,6 +418,7 @@ export function buildContextBreakdown(input: BuildContextBreakdownInput): Contex
|
|
|
416
418
|
label: input.basePromptEstimatedTokens === null ? "Base system prompt (not observed yet)" : "Base system prompt (Pi + host instructions)",
|
|
417
419
|
estimatedTokens: input.basePromptEstimatedTokens ?? 0,
|
|
418
420
|
...(input.basePromptEstimatedTokens === null ? { unknown: true } : {}),
|
|
421
|
+
...(input.basePromptItems && input.basePromptItems.length > 0 ? { items: input.basePromptItems } : {}),
|
|
419
422
|
};
|
|
420
423
|
const messageHistory: ContextSegment = {
|
|
421
424
|
key: "messageHistory",
|
package/extension/src/index.ts
CHANGED
|
@@ -27,7 +27,8 @@ import { ActiveTaskContinuation, automaticPauseReason, shouldResumeFocusOnHumanI
|
|
|
27
27
|
import { buildTaskWidgetProjection, type TaskWidgetProjection } from "./task-widget.ts";
|
|
28
28
|
import { TASK_STATUS_PRESENTATION, taskTreeConnector } from "./task-presentation.ts";
|
|
29
29
|
import { buildContextInjection } from "./context-injection-telemetry.ts";
|
|
30
|
-
import { buildContextBreakdown, buildMessageHistoryTree, buildTaskItemTree, computeContextBudget, computeRuleBudget, DEFAULT_RESERVE_TOKENS, type SessionEntryLike, type SessionTreeNodeLike } from "./context-budget.ts";
|
|
30
|
+
import { buildContextBreakdown, buildMessageHistoryTree, buildTaskItemTree, computeContextBudget, computeRuleBudget, DEFAULT_RESERVE_TOKENS, type ContextSegmentItem, type SessionEntryLike, type SessionTreeNodeLike } from "./context-budget.ts";
|
|
31
|
+
import { buildBasePromptItems } from "./base-prompt-breakdown.ts";
|
|
31
32
|
import { showContextView } from "./context-view.ts";
|
|
32
33
|
import { emitTaskFocusEvent, setTaskFocusEventBus } from "./task-focus-events.ts";
|
|
33
34
|
import { renderPapyrusToolCall, renderPapyrusToolResult } from "./tool-rendering/index.ts";
|
|
@@ -164,8 +165,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
164
165
|
// Cached from the most recent before_agent_start observation: Pi's own base system prompt
|
|
165
166
|
// is only ever visible transiently inside that hook's event.systemPrompt, so /context
|
|
166
167
|
// reuses the size buildContextInjection already computes every turn rather than going
|
|
167
|
-
// without it entirely.
|
|
168
|
+
// without it entirely. basePromptItems is the structural sub-breakdown built from the same
|
|
169
|
+
// event's systemPromptOptions field ("Extensions can inspect this to understand what Pi
|
|
170
|
+
// loaded without re-discovering resources", per Pi's own doc comment) -- no new hook, no new
|
|
171
|
+
// risk, just reading a field before_agent_start already hands over.
|
|
168
172
|
let lastObservedBasePromptTokens: number | null = null;
|
|
173
|
+
let lastObservedBasePromptItems: ContextSegmentItem[] = [];
|
|
169
174
|
const taskContinuation = new ActiveTaskContinuation({
|
|
170
175
|
maxTurns: TASK_DRIVER_MAX_TURNS,
|
|
171
176
|
maxUnchangedTurns: TASK_DRIVER_MAX_UNCHANGED_TURNS,
|
|
@@ -480,6 +485,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
480
485
|
taskItems: buildTaskItemTree(taskGraph),
|
|
481
486
|
skills,
|
|
482
487
|
basePromptEstimatedTokens: lastObservedBasePromptTokens,
|
|
488
|
+
basePromptItems: lastObservedBasePromptItems,
|
|
483
489
|
messageHistoryItems: messageHistory.items,
|
|
484
490
|
messageHistoryActiveTokens: messageHistory.activeTokens,
|
|
485
491
|
});
|
|
@@ -559,6 +565,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
559
565
|
});
|
|
560
566
|
previousContextInjectionFingerprint = injection.observation.fingerprint;
|
|
561
567
|
lastObservedBasePromptTokens = Math.ceil(injection.observation.before.characters / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN);
|
|
568
|
+
lastObservedBasePromptItems = buildBasePromptItems(event.systemPromptOptions, injection.observation.before.characters);
|
|
562
569
|
pi.events.emit(PAPYRUS_CONTEXT_INJECTION_CHANNEL, injection.observation);
|
|
563
570
|
if (injection.prompt !== (event.systemPrompt ?? "")) return { systemPrompt: injection.prompt };
|
|
564
571
|
} catch {
|
package/package.json
CHANGED