@danypops/papyrus 0.34.0 → 0.34.2
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.
|
@@ -77,7 +77,7 @@ export interface ContextSegmentItem {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export interface ContextSegment {
|
|
80
|
-
key: "rules" | "tasks" | "skills" | "basePrompt" | "messageHistory" | "other";
|
|
80
|
+
key: "rules" | "tasks" | "skills" | "basePrompt" | "messageHistory" | "toolDefinitions" | "other";
|
|
81
81
|
label: string;
|
|
82
82
|
estimatedTokens: number;
|
|
83
83
|
/** Drill-down items, when this segment can be broken down further. Absent for "other" -- an opaque remainder, not a real category. */
|
|
@@ -273,15 +273,15 @@ export interface ContextBreakdown {
|
|
|
273
273
|
/** contextWindow - reserveTokens, mirroring Pi's own compaction-trigger formula. Null when contextWindow is unknown. */
|
|
274
274
|
effectiveBudget: number | null;
|
|
275
275
|
/**
|
|
276
|
-
* How much the known/estimated segments (rules+tasks+skills+basePrompt+messageHistory
|
|
277
|
-
* exceed the real total, when they do. Zero means no overshoot. This must
|
|
278
|
-
* rather than only being absorbed into "unaccounted" clamping to zero -- a
|
|
279
|
-
* unaccounted segment does NOT mean
|
|
280
|
-
*
|
|
281
|
-
*
|
|
276
|
+
* How much the known/estimated segments (rules+tasks+skills+basePrompt+messageHistory+
|
|
277
|
+
* toolDefinitions) exceed the real total, when they do. Zero means no overshoot. This must
|
|
278
|
+
* stay visible rather than only being absorbed into "unaccounted" clamping to zero -- a
|
|
279
|
+
* clamped-to-zero unaccounted segment does NOT mean wire-protocol overhead is actually free;
|
|
280
|
+
* it means this estimate's other segments already consumed the entire real budget on paper.
|
|
281
|
+
* Hiding that distinction would make a genuinely nonzero cost look like zero.
|
|
282
282
|
*/
|
|
283
283
|
overshootTokens: number;
|
|
284
|
-
/** rules, tasks, skills, basePrompt, messageHistory, then "other" absorbing whatever real usage the rest don't account for. */
|
|
284
|
+
/** rules, tasks, skills, basePrompt, messageHistory, toolDefinitions, then "other" absorbing whatever real usage the rest don't account for. */
|
|
285
285
|
segments: ContextSegment[];
|
|
286
286
|
}
|
|
287
287
|
|
|
@@ -301,6 +301,8 @@ export interface BuildContextBreakdownInput {
|
|
|
301
301
|
messageHistoryItems: ContextSegmentItem[];
|
|
302
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. */
|
|
303
303
|
messageHistoryActiveTokens: number;
|
|
304
|
+
/** From buildToolDefinitionItems() against pi.getAllTools() filtered to pi.getActiveTools(). Defaults to empty when omitted. */
|
|
305
|
+
toolDefinitionItems?: ContextSegmentItem[];
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
/** Sums a possibly-nested item tree's tokens recursively -- every node's own contribution, not just top-level items. */
|
|
@@ -308,6 +310,46 @@ function sumItemTree(items: ContextSegmentItem[]): number {
|
|
|
308
310
|
return items.reduce((sum, item) => sum + item.estimatedTokens + sumItemTree(item.children ?? []), 0);
|
|
309
311
|
}
|
|
310
312
|
|
|
313
|
+
/** The subset of pi.getAllTools()'s ToolInfo this estimate actually reads -- kept minimal so this stays testable with plain object literals instead of importing Pi's own extension types. */
|
|
314
|
+
export interface ActiveToolDefinitionLike {
|
|
315
|
+
name: string;
|
|
316
|
+
description: string;
|
|
317
|
+
parameters: unknown;
|
|
318
|
+
sourceInfo: { source: string };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Tool definitions (name + description + JSON schema) are actually measurable, unlike genuine
|
|
323
|
+
* wire-protocol framework overhead (message envelope/role wrapping, cache-control markers) which
|
|
324
|
+
* really is invisible to any extension -- this is what lets "other" stop absorbing them as an
|
|
325
|
+
* unmeasured guess. Grouped by extension/package source with each tool as a drill-down child
|
|
326
|
+
* (mirrors the Tasks segment's own parent/child shape) rather than one flat list, since a real
|
|
327
|
+
* session can have dozens of active tools spread across many extensions.
|
|
328
|
+
*/
|
|
329
|
+
export function buildToolDefinitionItems(tools: ReadonlyArray<ActiveToolDefinitionLike>): ContextSegmentItem[] {
|
|
330
|
+
const bySource = new Map<string, ActiveToolDefinitionLike[]>();
|
|
331
|
+
for (const tool of tools) {
|
|
332
|
+
const list = bySource.get(tool.sourceInfo.source) ?? [];
|
|
333
|
+
list.push(tool);
|
|
334
|
+
bySource.set(tool.sourceInfo.source, list);
|
|
335
|
+
}
|
|
336
|
+
const items: ContextSegmentItem[] = [];
|
|
337
|
+
for (const [source, toolsForSource] of bySource) {
|
|
338
|
+
const children = toolsForSource
|
|
339
|
+
.map((tool) => {
|
|
340
|
+
const characters = tool.name.length + tool.description.length + JSON.stringify(tool.parameters ?? {}).length;
|
|
341
|
+
return { label: tool.name, estimatedTokens: Math.ceil(characters / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN) };
|
|
342
|
+
})
|
|
343
|
+
.sort((a, b) => b.estimatedTokens - a.estimatedTokens);
|
|
344
|
+
items.push({
|
|
345
|
+
label: `${source} (${toolsForSource.length} tool${toolsForSource.length === 1 ? "" : "s"})`,
|
|
346
|
+
estimatedTokens: children.reduce((sum, child) => sum + child.estimatedTokens, 0),
|
|
347
|
+
children,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return items.sort((a, b) => b.estimatedTokens - a.estimatedTokens);
|
|
351
|
+
}
|
|
352
|
+
|
|
311
353
|
/**
|
|
312
354
|
* Builds the Tasks segment's items from Papyrus's own real containment tree (parentIds/
|
|
313
355
|
* childIds), not a flat list -- Tasks are a genuine DAG (a task may have more than one
|
|
@@ -382,9 +424,10 @@ export function buildTaskItemTree(graph: TaskGraph): ContextSegmentItem[] {
|
|
|
382
424
|
|
|
383
425
|
/**
|
|
384
426
|
* Composes every segment Papyrus can actually measure or estimate (rules, tasks, skills
|
|
385
|
-
* catalog, cached base-prompt size, and the live session's own
|
|
386
|
-
* real total Pi reports, deriving "unaccounted" (
|
|
387
|
-
*
|
|
427
|
+
* catalog, cached base-prompt size, active tool definitions, and the live session's own
|
|
428
|
+
* message history) against the real total Pi reports, deriving "unaccounted" (genuine
|
|
429
|
+
* wire-protocol overhead -- message envelope/role wrapping, cache-control markers -- which
|
|
430
|
+
* really is invisible to any extension) as the remainder. The remainder is clamped to zero
|
|
388
431
|
* rather than shown negative -- char/4 token estimation is approximate, and a small overshoot
|
|
389
432
|
* in the known segments must not display as a nonsensical negative bucket -- but the clamp
|
|
390
433
|
* amount itself is preserved as overshootTokens rather than silently discarded, so a
|
|
@@ -426,13 +469,26 @@ export function buildContextBreakdown(input: BuildContextBreakdownInput): Contex
|
|
|
426
469
|
estimatedTokens: input.messageHistoryActiveTokens,
|
|
427
470
|
items: input.messageHistoryItems,
|
|
428
471
|
};
|
|
429
|
-
const
|
|
472
|
+
const toolDefinitionItems = input.toolDefinitionItems ?? [];
|
|
473
|
+
const toolCount = toolDefinitionItems.reduce((sum, item) => sum + (item.children?.length ?? 1), 0);
|
|
474
|
+
const toolDefinitions: ContextSegment = {
|
|
475
|
+
key: "toolDefinitions",
|
|
476
|
+
label: `Active tool definitions (${toolCount} tool${toolCount === 1 ? "" : "s"})`,
|
|
477
|
+
// Top-level sum only, NOT sumItemTree: unlike Tasks/message-history, whose parent nodes
|
|
478
|
+
// carry their own independent content genuinely additive with their children, a
|
|
479
|
+
// buildToolDefinitionItems() group node's own estimatedTokens IS the sum of its children
|
|
480
|
+
// (by construction, for a meaningful collapsed-row total) -- summing the tree here would
|
|
481
|
+
// double-count every tool once as itself and once inside its group's total.
|
|
482
|
+
estimatedTokens: toolDefinitionItems.reduce((sum, item) => sum + item.estimatedTokens, 0),
|
|
483
|
+
...(toolDefinitionItems.length > 0 ? { items: toolDefinitionItems } : {}),
|
|
484
|
+
};
|
|
485
|
+
const knownTokens = rules.estimatedTokens + tasks.estimatedTokens + skills.estimatedTokens + basePrompt.estimatedTokens + messageHistory.estimatedTokens + toolDefinitions.estimatedTokens;
|
|
430
486
|
const overshootTokens = input.totalTokens === null ? 0 : Math.max(0, knownTokens - input.totalTokens);
|
|
431
487
|
const other: ContextSegment = {
|
|
432
488
|
key: "other",
|
|
433
489
|
label: overshootTokens > 0
|
|
434
|
-
? `Unaccounted (
|
|
435
|
-
: "Unaccounted (
|
|
490
|
+
? `Unaccounted (message envelope, cache-control markers, and other wire-protocol overhead) -- estimate overshoot: other segments' estimates already exceed the real total by ~${overshootTokens} tokens, so this is a floor, not a real zero`
|
|
491
|
+
: "Unaccounted (message envelope, cache-control markers, and other wire-protocol overhead)",
|
|
436
492
|
estimatedTokens: input.totalTokens === null ? 0 : Math.max(0, input.totalTokens - knownTokens),
|
|
437
493
|
};
|
|
438
494
|
return {
|
|
@@ -440,7 +496,7 @@ export function buildContextBreakdown(input: BuildContextBreakdownInput): Contex
|
|
|
440
496
|
contextWindow: input.contextWindow,
|
|
441
497
|
effectiveBudget: input.contextWindow === null ? null : Math.max(0, input.contextWindow - reserveTokens),
|
|
442
498
|
overshootTokens,
|
|
443
|
-
segments: [rules, tasks, skills, basePrompt, messageHistory, other],
|
|
499
|
+
segments: [rules, tasks, skills, basePrompt, messageHistory, toolDefinitions, other],
|
|
444
500
|
};
|
|
445
501
|
}
|
|
446
502
|
|
package/extension/src/index.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { ActiveTaskContinuation, automaticPauseReason, shouldResumeFocusOnHumanI
|
|
|
34
34
|
import { buildTaskWidgetProjection, type TaskWidgetProjection } from "./task-widget.ts";
|
|
35
35
|
import { TASK_STATUS_PRESENTATION, taskTreeConnector } from "./task-presentation.ts";
|
|
36
36
|
import { buildContextInjection } from "./context-injection-telemetry.ts";
|
|
37
|
-
import { buildContextBreakdown, buildMessageHistoryTree, buildTaskItemTree, computeContextBudget, computeRuleBudget, DEFAULT_RESERVE_TOKENS, type ContextSegmentItem, type SessionEntryLike, type SessionTreeNodeLike } from "./context-budget.ts";
|
|
37
|
+
import { buildContextBreakdown, buildMessageHistoryTree, buildTaskItemTree, buildToolDefinitionItems, computeContextBudget, computeRuleBudget, DEFAULT_RESERVE_TOKENS, type ContextSegmentItem, type SessionEntryLike, type SessionTreeNodeLike } from "./context-budget.ts";
|
|
38
38
|
import { buildBasePromptItems } from "./base-prompt-breakdown.ts";
|
|
39
39
|
import { showContextView } from "./context-view.ts";
|
|
40
40
|
import { emitTaskFocusEvent, setTaskFocusEventBus } from "./task-focus-events.ts";
|
|
@@ -635,6 +635,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
635
635
|
const activeEntryIds = new Set((ctx.sessionManager.buildContextEntries() as SessionEntryLike[]).map((entry) => entry.id));
|
|
636
636
|
const branchEntryIds = new Set((ctx.sessionManager.getBranch() as SessionEntryLike[]).map((entry) => entry.id));
|
|
637
637
|
const messageHistory = buildMessageHistoryTree(tree, activeEntryIds, branchEntryIds);
|
|
638
|
+
const activeToolNames = new Set(pi.getActiveTools());
|
|
639
|
+
const toolDefinitionItems = buildToolDefinitionItems(pi.getAllTools().filter((tool) => activeToolNames.has(tool.name)));
|
|
638
640
|
const breakdown = buildContextBreakdown({
|
|
639
641
|
totalTokens: usage?.tokens ?? null,
|
|
640
642
|
contextWindow: ctx.model?.contextWindow ?? null,
|
|
@@ -643,6 +645,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
643
645
|
skills,
|
|
644
646
|
basePromptEstimatedTokens: lastObservedBasePromptTokens,
|
|
645
647
|
basePromptItems: lastObservedBasePromptItems,
|
|
648
|
+
toolDefinitionItems,
|
|
646
649
|
messageHistoryItems: messageHistory.items,
|
|
647
650
|
messageHistoryActiveTokens: messageHistory.activeTokens,
|
|
648
651
|
});
|
package/package.json
CHANGED
package/src/domain/artifact.ts
CHANGED
|
@@ -48,6 +48,13 @@ export interface ArtifactQuery {
|
|
|
48
48
|
limit?: number;
|
|
49
49
|
/** Trashed artifacts (see artifact-trash.ts) are excluded from every query by default; set true to include them, e.g. for a trash-listing view. */
|
|
50
50
|
includeTrashed?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Restrict to exactly these ids, still subject to every other filter (kind, trash exclusion,
|
|
53
|
+
* etc.) -- for a caller that already has a bounded candidate id set (e.g. Tasks.list's
|
|
54
|
+
* project/graph scope) and needs query()'s own trash-exclusion without a full-kind scan.
|
|
55
|
+
* Empty array is a real "match nothing", not "unset".
|
|
56
|
+
*/
|
|
57
|
+
ids?: string[];
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
export interface ArtifactGraphOptions {
|
package/src/ops.ts
CHANGED
|
@@ -290,6 +290,11 @@ export function queryArtifacts(db: Db, filter: ArtifactQuery): Artifact[] {
|
|
|
290
290
|
const conditions: string[] = [];
|
|
291
291
|
const params: unknown[] = [];
|
|
292
292
|
if (!filter.includeTrashed) conditions.push("id NOT IN (SELECT artifact_id FROM artifact_trash)");
|
|
293
|
+
if (filter.ids) {
|
|
294
|
+
if (filter.ids.length === 0) return [];
|
|
295
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(", ")})`);
|
|
296
|
+
params.push(...filter.ids);
|
|
297
|
+
}
|
|
293
298
|
if (filter.kind) { conditions.push("kind = ?"); params.push(filter.kind); }
|
|
294
299
|
if (filter.status) { conditions.push("status = ?"); params.push(filter.status); }
|
|
295
300
|
if (filter.statuses) {
|
package/src/task-service.ts
CHANGED
|
@@ -237,9 +237,16 @@ export class Tasks {
|
|
|
237
237
|
const selectedIds = selection.mode === "graph" ? this.descendantIds(selection.rootTaskId!, ids) : new Set(ids);
|
|
238
238
|
const text = filter.text?.toLowerCase();
|
|
239
239
|
const labels = filter.labels ?? [];
|
|
240
|
+
// query(), unlike get(), excludes trash by default -- a trashed task's id stays in
|
|
241
|
+
// task_scopes until the retention window actually purges it, so building this candidate
|
|
242
|
+
// set from get() per id (deliberately trash-transparent, for show/restore) would otherwise
|
|
243
|
+
// leak a trashed task back into list results for the entire grace period. Scoped by ids
|
|
244
|
+
// rather than a bare kind query, so this stays bounded to exactly the already-bounded
|
|
245
|
+
// selectedIds set instead of scanning every task in the database.
|
|
246
|
+
const notTrashed = new Map(this.artifacts.query({ kind: "task", excludeSubtype: DISCUSSION_SUBTYPE, ids: [...selectedIds] }).map((task) => [task.id, task]));
|
|
240
247
|
return [...selectedIds]
|
|
241
|
-
.map((id) =>
|
|
242
|
-
.filter((task): task is Artifact => task
|
|
248
|
+
.map((id) => notTrashed.get(id))
|
|
249
|
+
.filter((task): task is Artifact => task !== undefined)
|
|
243
250
|
.filter((task) => filter.status === undefined || task.status === filter.status)
|
|
244
251
|
.filter((task) => text === undefined || task.title.toLowerCase().includes(text) || task.body.toLowerCase().includes(text))
|
|
245
252
|
.filter((task) => labels.every((label) => task.labels.includes(label)))
|