@danypops/papyrus 0.13.2 → 0.13.3
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/extension/src/context-budget.ts +145 -23
- package/extension/src/context-view.ts +15 -6
- package/extension/src/index.ts +11 -12
- package/package.json +1 -1
- package/src/constants.ts +12 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN } from "../../src/constants.ts";
|
|
3
|
+
import { CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN, CONTEXT_TREE_MAX_DEPTH, CONTEXT_TREE_MAX_NODES } from "../../src/constants.ts";
|
|
4
4
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
5
|
+
import type { TaskGraph } from "../../src/task-service.ts";
|
|
5
6
|
import { discoverSkillDirectories, scanSkillCatalogFootprint, type SkillCatalogFootprint } from "./skill-catalog-footprint.ts";
|
|
6
7
|
import { ruleInjectionPreview } from "./rules.ts";
|
|
7
8
|
|
|
@@ -66,6 +67,13 @@ export const DEFAULT_RESERVE_TOKENS = 16_384;
|
|
|
66
67
|
export interface ContextSegmentItem {
|
|
67
68
|
label: string;
|
|
68
69
|
estimatedTokens: number;
|
|
70
|
+
/**
|
|
71
|
+
* Recursive children, when this item has real hierarchy of its own -- conversation history
|
|
72
|
+
* (Pi's session entries form a genuine tree via id/parentId, docs/session-format.md) and
|
|
73
|
+
* Papyrus Tasks (containment via parentIds/childIds) both do; Rules and Skills don't, so
|
|
74
|
+
* their items simply omit this field, degenerating to a flat one-level tree.
|
|
75
|
+
*/
|
|
76
|
+
children?: ContextSegmentItem[];
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
export interface ContextSegment {
|
|
@@ -85,15 +93,21 @@ export interface ContextSegment {
|
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
/**
|
|
88
|
-
* Session
|
|
89
|
-
*
|
|
90
|
-
*
|
|
96
|
+
* Session entries and tree nodes as SessionManager exposes them (docs/session-format.md,
|
|
97
|
+
* SessionTreeNode from @earendil-works/pi-coding-agent): a subset covering only the fields
|
|
98
|
+
* this estimate reads, so this stays testable with plain object literals instead of
|
|
99
|
+
* importing pi's own session types.
|
|
91
100
|
*/
|
|
92
|
-
export interface
|
|
101
|
+
export interface SessionEntryLike {
|
|
102
|
+
id: string;
|
|
93
103
|
type: string;
|
|
94
104
|
message?: unknown;
|
|
95
105
|
summary?: string;
|
|
96
106
|
}
|
|
107
|
+
export interface SessionTreeNodeLike {
|
|
108
|
+
entry: SessionEntryLike;
|
|
109
|
+
children: SessionTreeNodeLike[];
|
|
110
|
+
}
|
|
97
111
|
|
|
98
112
|
function messageContentCharacters(message: unknown): number {
|
|
99
113
|
if (typeof message !== "object" || message === null) return 0;
|
|
@@ -120,22 +134,84 @@ function messageContentCharacters(message: unknown): number {
|
|
|
120
134
|
return characters;
|
|
121
135
|
}
|
|
122
136
|
|
|
137
|
+
function messageSnippet(message: unknown, maxLength = 48): string {
|
|
138
|
+
if (typeof message !== "object" || message === null) return "";
|
|
139
|
+
const record = message as Record<string, unknown>;
|
|
140
|
+
if (record["role"] === "bashExecution") return String(record["command"] ?? "");
|
|
141
|
+
const content = record["content"];
|
|
142
|
+
const text = typeof content === "string"
|
|
143
|
+
? content
|
|
144
|
+
: Array.isArray(content)
|
|
145
|
+
? content.map((block) => (typeof block === "object" && block !== null && (block as Record<string, unknown>)["type"] === "text" ? String((block as Record<string, unknown>)["text"] ?? "") : "")).join(" ")
|
|
146
|
+
: "";
|
|
147
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
148
|
+
return collapsed.length > maxLength ? `${collapsed.slice(0, maxLength - 1)}…` : collapsed;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function entryLabel(entry: SessionEntryLike): string {
|
|
152
|
+
if (entry.type === "compaction") return "compaction summary";
|
|
153
|
+
if (entry.type === "branch_summary") return "branch summary";
|
|
154
|
+
const role = typeof entry.message === "object" && entry.message !== null ? (entry.message as Record<string, unknown>)["role"] : undefined;
|
|
155
|
+
const prefix = typeof role === "string" ? role : entry.type;
|
|
156
|
+
const snippet = messageSnippet(entry.message);
|
|
157
|
+
return snippet ? `${prefix}: ${snippet}` : prefix;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface MessageHistoryTree {
|
|
161
|
+
/** One item per real tree root (ordinarily one, the session's first entry). */
|
|
162
|
+
items: ContextSegmentItem[];
|
|
163
|
+
/** Sum of tokens for entries on the CURRENT active path only -- what actually feeds the LLM's context right now, unlike content sitting in an abandoned /tree branch. */
|
|
164
|
+
activeTokens: number;
|
|
165
|
+
/** True if the walk hit CONTEXT_TREE_MAX_DEPTH or CONTEXT_TREE_MAX_NODES, or found a cycle -- the tree shown is a bounded prefix, not necessarily the complete session. */
|
|
166
|
+
truncated: boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
123
169
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
170
|
+
* Walks Pi's own real session tree (ctx.sessionManager.getTree(), docs/session-format.md --
|
|
171
|
+
* entries form a genuine tree via id/parentId, not just the linear current-branch path) to
|
|
172
|
+
* estimate the conversation's context contribution AND surface branches explored via /tree
|
|
173
|
+
* that are no longer on the active path -- content that cost real tokens to generate but is
|
|
174
|
+
* NOT currently part of the context window. Bounded and cycle-safe (CONTEXT_TREE_MAX_DEPTH /
|
|
175
|
+
* CONTEXT_TREE_MAX_NODES): a session file is external, mutable state, and this deliberately
|
|
176
|
+
* hardens past a confirmed real gap in Pi's own getBranch() (no cycle guard at all) rather
|
|
177
|
+
* than assuming the tree can never be malformed.
|
|
131
178
|
*/
|
|
132
|
-
export function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
179
|
+
export function buildMessageHistoryTree(roots: ReadonlyArray<SessionTreeNodeLike>, activeEntryIds: ReadonlySet<string>): MessageHistoryTree {
|
|
180
|
+
const visited = new Set<string>();
|
|
181
|
+
let truncated = false;
|
|
182
|
+
let activeTokens = 0;
|
|
183
|
+
let nodesVisited = 0;
|
|
184
|
+
|
|
185
|
+
function visit(node: SessionTreeNodeLike, depth: number): ContextSegmentItem | null {
|
|
186
|
+
if (nodesVisited >= CONTEXT_TREE_MAX_NODES || depth > CONTEXT_TREE_MAX_DEPTH) { truncated = true; return null; }
|
|
187
|
+
if (visited.has(node.entry.id)) { truncated = true; return null; } // cycle guard
|
|
188
|
+
visited.add(node.entry.id);
|
|
189
|
+
nodesVisited++;
|
|
190
|
+
|
|
191
|
+
const entry = node.entry;
|
|
192
|
+
const characters = entry.type === "message"
|
|
193
|
+
? messageContentCharacters(entry.message)
|
|
194
|
+
: entry.type === "compaction" || entry.type === "branch_summary"
|
|
195
|
+
? (entry.summary ?? "").length
|
|
196
|
+
: 0;
|
|
197
|
+
const tokens = Math.ceil(characters / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN);
|
|
198
|
+
const isActive = activeEntryIds.has(entry.id);
|
|
199
|
+
if (isActive) activeTokens += tokens;
|
|
200
|
+
|
|
201
|
+
const children = node.children
|
|
202
|
+
.map((child) => visit(child, depth + 1))
|
|
203
|
+
.filter((item): item is ContextSegmentItem => item !== null);
|
|
204
|
+
if (tokens === 0 && children.length === 0) return null; // no content, no descendants with content -- nothing to show
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
label: isActive ? entryLabel(entry) : `${entryLabel(entry)} (inactive branch)`,
|
|
208
|
+
estimatedTokens: tokens,
|
|
209
|
+
...(children.length > 0 ? { children } : {}),
|
|
210
|
+
};
|
|
137
211
|
}
|
|
138
|
-
|
|
212
|
+
|
|
213
|
+
const items = roots.map((root) => visit(root, 0)).filter((item): item is ContextSegmentItem => item !== null);
|
|
214
|
+
return { items, activeTokens, truncated };
|
|
139
215
|
}
|
|
140
216
|
|
|
141
217
|
export interface ContextBreakdown {
|
|
@@ -163,13 +239,58 @@ export interface BuildContextBreakdownInput {
|
|
|
163
239
|
contextWindow: number | null;
|
|
164
240
|
reserveTokens?: number;
|
|
165
241
|
ruleBudget: ContextBudget["rules"];
|
|
166
|
-
/** Open tasks contributing to the injected task-context summary,
|
|
242
|
+
/** Open tasks contributing to the injected task-context summary, nested by containment (parentIds/childIds) so the Tasks segment reflects the real Task tree, not a flat list. */
|
|
167
243
|
taskItems: ContextSegmentItem[];
|
|
168
244
|
skills: SkillCatalogFootprint;
|
|
169
245
|
/** Pi's own base system prompt size, cached from the last observed before_agent_start turn. Null before any turn has run yet. */
|
|
170
246
|
basePromptEstimatedTokens: number | null;
|
|
171
|
-
/** From
|
|
172
|
-
|
|
247
|
+
/** From buildMessageHistoryTree() against the live session's real tree (ctx.sessionManager.getTree()). */
|
|
248
|
+
messageHistoryItems: ContextSegmentItem[];
|
|
249
|
+
/** 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. */
|
|
250
|
+
messageHistoryActiveTokens: number;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Sums a possibly-nested item tree's tokens recursively -- every node's own contribution, not just top-level items. */
|
|
254
|
+
function sumItemTree(items: ContextSegmentItem[]): number {
|
|
255
|
+
return items.reduce((sum, item) => sum + item.estimatedTokens + sumItemTree(item.children ?? []), 0);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Builds the Tasks segment's items from Papyrus's own real containment tree (parentIds/
|
|
260
|
+
* childIds), not a flat list -- Tasks are a genuine DAG (a task may have more than one
|
|
261
|
+
* parent, a deliberate design decision, not a defect: see /tasks contain). Open tasks only
|
|
262
|
+
* (done/canceled tasks are filtered first, matching taskContext()'s own "only open work
|
|
263
|
+
* matters" rule); a task whose real parent is done/canceled or otherwise filtered out
|
|
264
|
+
* becomes a root in THIS projection rather than being silently dropped. A task reachable
|
|
265
|
+
* from more than one open parent is shown once, under whichever parent this bounded walk
|
|
266
|
+
* reaches first -- the same spanning-tree compromise already applied to the task widget
|
|
267
|
+
* (extension/src/task-widget.ts) for the identical multi-parent-DAG-in-a-bounded-view
|
|
268
|
+
* problem, not a new inconsistency.
|
|
269
|
+
*/
|
|
270
|
+
export function buildTaskItemTree(graph: TaskGraph): ContextSegmentItem[] {
|
|
271
|
+
const byId = new Map(graph.nodes.map((node) => [node.task.id, node]));
|
|
272
|
+
const openIds = new Set(graph.nodes.filter((node) => node.task.status !== "done" && node.task.status !== "canceled").map((node) => node.task.id));
|
|
273
|
+
const visited = new Set<string>();
|
|
274
|
+
|
|
275
|
+
function visit(id: string, depth: number): ContextSegmentItem | null {
|
|
276
|
+
if (depth > CONTEXT_TREE_MAX_DEPTH || visited.size >= CONTEXT_TREE_MAX_NODES || visited.has(id) || !openIds.has(id)) return null;
|
|
277
|
+
visited.add(id);
|
|
278
|
+
const node = byId.get(id);
|
|
279
|
+
if (!node) return null;
|
|
280
|
+
const characters = node.task.title.length + node.task.body.length;
|
|
281
|
+
const tokens = Math.ceil(characters / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN);
|
|
282
|
+
const children = node.childIds
|
|
283
|
+
.filter((childId) => openIds.has(childId))
|
|
284
|
+
.map((childId) => visit(childId, depth + 1))
|
|
285
|
+
.filter((item): item is ContextSegmentItem => item !== null);
|
|
286
|
+
return { label: node.task.title, estimatedTokens: tokens, ...(children.length > 0 ? { children } : {}) };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const rootIds = [...openIds].filter((id) => {
|
|
290
|
+
const node = byId.get(id)!;
|
|
291
|
+
return node.parentIds.length === 0 || !node.parentIds.some((parentId) => openIds.has(parentId));
|
|
292
|
+
});
|
|
293
|
+
return rootIds.map((id) => visit(id, 0)).filter((item): item is ContextSegmentItem => item !== null);
|
|
173
294
|
}
|
|
174
295
|
|
|
175
296
|
/**
|
|
@@ -196,7 +317,7 @@ export function buildContextBreakdown(input: BuildContextBreakdownInput): Contex
|
|
|
196
317
|
const tasks: ContextSegment = {
|
|
197
318
|
key: "tasks",
|
|
198
319
|
label: "Papyrus Tasks",
|
|
199
|
-
estimatedTokens: input.taskItems
|
|
320
|
+
estimatedTokens: sumItemTree(input.taskItems),
|
|
200
321
|
items: input.taskItems,
|
|
201
322
|
};
|
|
202
323
|
const skills: ContextSegment = {
|
|
@@ -214,7 +335,8 @@ export function buildContextBreakdown(input: BuildContextBreakdownInput): Contex
|
|
|
214
335
|
const messageHistory: ContextSegment = {
|
|
215
336
|
key: "messageHistory",
|
|
216
337
|
label: "Conversation message history",
|
|
217
|
-
estimatedTokens: input.
|
|
338
|
+
estimatedTokens: input.messageHistoryActiveTokens,
|
|
339
|
+
items: input.messageHistoryItems,
|
|
218
340
|
};
|
|
219
341
|
const knownTokens = rules.estimatedTokens + tasks.estimatedTokens + skills.estimatedTokens + basePrompt.estimatedTokens + messageHistory.estimatedTokens;
|
|
220
342
|
const overshootTokens = input.totalTokens === null ? 0 : Math.max(0, knownTokens - input.totalTokens);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionCommandContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { matchesKey, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { ContextBreakdown, ContextSegment } from "./context-budget.ts";
|
|
3
|
+
import type { ContextBreakdown, ContextSegment, ContextSegmentItem } from "./context-budget.ts";
|
|
4
4
|
|
|
5
5
|
const VISIBLE_ROWS = 24;
|
|
6
6
|
|
|
@@ -23,6 +23,8 @@ export interface ContextRow {
|
|
|
23
23
|
key: ContextSegment["key"];
|
|
24
24
|
isHeader: boolean;
|
|
25
25
|
text: string;
|
|
26
|
+
/** Nesting depth for indentation -- 0 for a segment header or a top-level item, deeper for real tree children (message history branches, Task containment). */
|
|
27
|
+
depth: number;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
function formatTokenCount(tokens: number): string {
|
|
@@ -41,6 +43,13 @@ function percentOf(part: number, whole: number): string {
|
|
|
41
43
|
* items are also all zero is dropped entirely; a segment with a nonzero total is always kept
|
|
42
44
|
* even if all its items individually round to zero (the total itself is real signal).
|
|
43
45
|
*/
|
|
46
|
+
/** Recursively flattens one item and its real tree children (message history branches, Task containment) into indented rows, sorted biggest-first at each level -- a parent always immediately precedes its own children, never scrambled by a global sort. */
|
|
47
|
+
function flattenItem(item: ContextSegmentItem, key: ContextSegment["key"], depth: number, rows: ContextRow[]): void {
|
|
48
|
+
rows.push({ key, isHeader: false, depth, text: `${item.estimatedTokens.toString().padStart(6)} tok ${item.label}` });
|
|
49
|
+
const children = (item.children ?? []).filter((child) => child.estimatedTokens > 0).sort((a, b) => b.estimatedTokens - a.estimatedTokens);
|
|
50
|
+
for (const child of children) flattenItem(child, key, depth + 1, rows);
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
export function buildContextRows(breakdown: ContextBreakdown): ContextRow[] {
|
|
45
54
|
const rows: ContextRow[] = [];
|
|
46
55
|
const denominator = breakdown.totalTokens ?? breakdown.segments.reduce((sum, segment) => sum + segment.estimatedTokens, 0);
|
|
@@ -54,11 +63,10 @@ export function buildContextRows(breakdown: ContextBreakdown): ContextRow[] {
|
|
|
54
63
|
rows.push({
|
|
55
64
|
key: segment.key,
|
|
56
65
|
isHeader: true,
|
|
66
|
+
depth: 0,
|
|
57
67
|
text: `${segment.label} — ${segment.estimatedTokens} tok (${percentOf(segment.estimatedTokens, denominator)})`,
|
|
58
68
|
});
|
|
59
|
-
for (const item of items)
|
|
60
|
-
rows.push({ key: segment.key, isHeader: false, text: ` ${item.estimatedTokens.toString().padStart(6)} tok ${item.label}` });
|
|
61
|
-
}
|
|
69
|
+
for (const item of items) flattenItem(item, segment.key, 1, rows);
|
|
62
70
|
}
|
|
63
71
|
return rows;
|
|
64
72
|
}
|
|
@@ -104,7 +112,8 @@ class ContextViewport {
|
|
|
104
112
|
|
|
105
113
|
this.visibleWindow().forEach(({ row, index }) => {
|
|
106
114
|
const gutter = theme.fg(SEGMENT_COLORS[row.key], "▌");
|
|
107
|
-
const
|
|
115
|
+
const indent = " ".repeat(row.depth);
|
|
116
|
+
const text = row.isHeader ? theme.bold(row.text) : `${indent}${row.text}`;
|
|
108
117
|
lines.push(truncateToWidth(`${gutter} ${text}`, contentWidth, ""));
|
|
109
118
|
void index;
|
|
110
119
|
});
|
|
@@ -162,7 +171,7 @@ function fallbackReport(breakdown: ContextBreakdown): string {
|
|
|
162
171
|
: "Real usage: not yet reported";
|
|
163
172
|
const overshootLine = breakdown.overshootTokens > 0 ? [`Estimates exceed real total by ~${breakdown.overshootTokens} tok -- sizes below are approximate, not exact`] : [];
|
|
164
173
|
const rows = buildContextRows(breakdown);
|
|
165
|
-
const rowLines = rows.length > 0 ? rows.map((row) => row.text) : ["(nothing observed yet)"];
|
|
174
|
+
const rowLines = rows.length > 0 ? rows.map((row) => (row.isHeader ? row.text : `${" ".repeat(row.depth)}${row.text}`)) : ["(nothing observed yet)"];
|
|
166
175
|
return [totalLine, ...overshootLine, "", ...rowLines].join("\n");
|
|
167
176
|
}
|
|
168
177
|
|
package/extension/src/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ 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, computeContextBudget, computeRuleBudget,
|
|
30
|
+
import { buildContextBreakdown, buildMessageHistoryTree, buildTaskItemTree, computeContextBudget, computeRuleBudget, type SessionEntryLike, type SessionTreeNodeLike } from "./context-budget.ts";
|
|
31
31
|
import { showContextView } from "./context-view.ts";
|
|
32
32
|
import { emitTaskFocusEvent, setTaskFocusEventBus } from "./task-focus-events.ts";
|
|
33
33
|
import { renderPapyrusToolCall, renderPapyrusToolResult } from "./tool-rendering/index.ts";
|
|
@@ -421,28 +421,27 @@ export default async function (pi: ExtensionAPI) {
|
|
|
421
421
|
handler: async (_args, ctx) => {
|
|
422
422
|
try {
|
|
423
423
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
424
|
-
const [rules,
|
|
424
|
+
const [rules, taskGraph] = await Promise.all([
|
|
425
425
|
callService<Record<string, unknown>, Array<Pick<Artifact, "id" | "title" | "body" | "extra">>>("rules.injectable", { project_root: ctx.cwd, session_id: sessionId }),
|
|
426
|
-
callService<Record<string, unknown>,
|
|
426
|
+
callService<Record<string, unknown>, TaskGraph>("tasks.graph", { project_root: ctx.cwd, session_id: sessionId }),
|
|
427
427
|
]);
|
|
428
428
|
const { skills } = computeContextBudget(rules, ctx.cwd);
|
|
429
429
|
const ruleBudget = computeRuleBudget(rules);
|
|
430
430
|
const usage = ctx.getContextUsage?.();
|
|
431
|
-
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
.filter((task) => task.status !== "done" && task.status !== "canceled")
|
|
437
|
-
.map((task) => ({ label: task.title, estimatedTokens: Math.ceil((task.title.length + task.body.length) / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN) }));
|
|
431
|
+
// Real tree (not just the linear current-branch path): surfaces content sitting in an
|
|
432
|
+
// abandoned /tree branch, which cost real tokens to generate but isn't in context now.
|
|
433
|
+
const tree = ctx.sessionManager.getTree() as unknown as SessionTreeNodeLike[];
|
|
434
|
+
const activeEntryIds = new Set((ctx.sessionManager.getBranch() as unknown as SessionEntryLike[]).map((entry) => entry.id));
|
|
435
|
+
const messageHistory = buildMessageHistoryTree(tree, activeEntryIds);
|
|
438
436
|
const breakdown = buildContextBreakdown({
|
|
439
437
|
totalTokens: usage?.tokens ?? null,
|
|
440
438
|
contextWindow: ctx.model?.contextWindow ?? null,
|
|
441
439
|
ruleBudget,
|
|
442
|
-
taskItems,
|
|
440
|
+
taskItems: buildTaskItemTree(taskGraph),
|
|
443
441
|
skills,
|
|
444
442
|
basePromptEstimatedTokens: lastObservedBasePromptTokens,
|
|
445
|
-
|
|
443
|
+
messageHistoryItems: messageHistory.items,
|
|
444
|
+
messageHistoryActiveTokens: messageHistory.activeTokens,
|
|
446
445
|
});
|
|
447
446
|
await showContextView(ctx, breakdown);
|
|
448
447
|
} catch (error) {
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -30,6 +30,18 @@ export const PAPYRUS_TASK_FOCUS_CHANNEL = "papyrus.task-focus.v1";
|
|
|
30
30
|
export const PAPYRUS_TASK_FOCUS_SCHEMA = "papyrus.task-focus/v1";
|
|
31
31
|
export const CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN = 4;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Bounds for walking Pi's real session tree (getTree()) and Papyrus's own Task containment
|
|
35
|
+
* tree when estimating /context's message-history and task segments. Both are genuine trees
|
|
36
|
+
* built from external, mutable state (a session file; the live Task graph) -- depth and
|
|
37
|
+
* total-node bounds are a defensive measure against a corrupted/adversarial parentId chain
|
|
38
|
+
* forming an accidental cycle, matching the same cycle-safety discipline already applied to
|
|
39
|
+
* ConversationJournal traversal and deliberately hardening past a real, confirmed gap in
|
|
40
|
+
* Pi's own getBranch() (no cycle guard at all).
|
|
41
|
+
*/
|
|
42
|
+
export const CONTEXT_TREE_MAX_DEPTH = 200;
|
|
43
|
+
export const CONTEXT_TREE_MAX_NODES = 2000;
|
|
44
|
+
|
|
33
45
|
/**
|
|
34
46
|
* A Papyrus Rule's condition+action+body is injected into EVERY relevant turn's system
|
|
35
47
|
* prompt for the lifetime of the rule -- the same permanent, always-on-context role as an
|