@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",
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],