pi-token-burden 0.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/LICENSE +21 -0
- package/README.md +79 -0
- package/package.json +58 -0
- package/src/index.test.ts +6 -0
- package/src/index.ts +25 -0
- package/src/parser.test.ts +156 -0
- package/src/parser.ts +244 -0
- package/src/report-view.test.ts +7 -0
- package/src/report-view.ts +595 -0
- package/src/types.ts +50 -0
- package/src/utils.test.ts +99 -0
- package/src/utils.ts +112 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Will Hampson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# pi-token-burden
|
|
2
|
+
|
|
3
|
+
See where your system prompt tokens go.
|
|
4
|
+
|
|
5
|
+
A [pi](https://github.com/mariozechner/pi) extension that parses the assembled system prompt and shows a token-budget breakdown by section. Run `/token-burden` to see how much of your context window is consumed by the base prompt, AGENTS.md files, skills, SYSTEM.md overrides, and metadata.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install git:github.com/Whamp/pi-token-burden
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or try it for a single session:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi -e git:github.com/Whamp/pi-token-burden
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Once installed, type `/token-burden` in any pi session. A TUI panel shows:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Token Burden
|
|
25
|
+
System Prompt: 12,450 tokens (49,798 chars) — 6.2% of 200,000 context window
|
|
26
|
+
|
|
27
|
+
Base prompt 1,250 tokens 10.0%
|
|
28
|
+
SYSTEM.md / APPEND_SYSTEM.md 340 tokens 2.7%
|
|
29
|
+
AGENTS.md files 2,860 tokens 23.0%
|
|
30
|
+
/home/user/.pi/agent/AGENTS.md 1,100 tokens 8.8%
|
|
31
|
+
/home/user/project/AGENTS.md 1,760 tokens 14.1%
|
|
32
|
+
Skills (42) 7,800 tokens 62.6%
|
|
33
|
+
brainstorming 180 tokens 1.4%
|
|
34
|
+
tdd 165 tokens 1.3%
|
|
35
|
+
...
|
|
36
|
+
Metadata (date/time, cwd) 200 tokens 1.6%
|
|
37
|
+
|
|
38
|
+
Press Enter or Esc to close
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Sections exceeding 10% of the system prompt are highlighted.
|
|
42
|
+
|
|
43
|
+
## Sections
|
|
44
|
+
|
|
45
|
+
| Section | What it measures |
|
|
46
|
+
| -------------------------------- | ---------------------------------------------------------------- |
|
|
47
|
+
| **Base prompt** | pi's built-in instructions, tool descriptions, guidelines |
|
|
48
|
+
| **SYSTEM.md / APPEND_SYSTEM.md** | Your custom system prompt overrides |
|
|
49
|
+
| **AGENTS.md files** | Each AGENTS.md file, listed individually |
|
|
50
|
+
| **Skills** | The `<available_skills>` block, with per-skill breakdown |
|
|
51
|
+
| **Metadata** | The `Current date and time` / `Current working directory` footer |
|
|
52
|
+
|
|
53
|
+
## Token estimation
|
|
54
|
+
|
|
55
|
+
Tokens are estimated as `ceil(chars / 4)` — the same heuristic pi uses internally. This is a rough approximation, not a tokenizer count.
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- [pi](https://github.com/mariozechner/pi) v0.55.1 or later
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/Whamp/pi-token-burden.git
|
|
65
|
+
cd pi-token-burden
|
|
66
|
+
pnpm install
|
|
67
|
+
pnpm run test # 15 tests
|
|
68
|
+
pnpm run check # lint, typecheck, format, dead code, duplicates, secrets, tests
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Test locally without installing:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pi -e ./src/index.ts
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
[MIT](LICENSE)
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-token-burden",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension that shows a token-budget breakdown of the assembled system prompt",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-extension",
|
|
7
|
+
"pi-package",
|
|
8
|
+
"token-budget"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"files": [
|
|
12
|
+
"src/",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"lint": "oxlint -c .oxlintrc.json",
|
|
19
|
+
"lint:fix": "oxlint -c .oxlintrc.json --fix",
|
|
20
|
+
"format": "oxfmt --config .oxfmtrc.jsonc",
|
|
21
|
+
"format:check": "oxfmt --config .oxfmtrc.jsonc --check",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"deadcode": "knip",
|
|
25
|
+
"duplicates": "jscpd src/",
|
|
26
|
+
"secrets": "gitleaks detect --source . --no-git",
|
|
27
|
+
"check": "bash scripts/check.sh",
|
|
28
|
+
"fix": "bash scripts/fix.sh"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^25.3.2",
|
|
32
|
+
"gitleaks": "^1.0.0",
|
|
33
|
+
"husky": "^9.1.7",
|
|
34
|
+
"jscpd": "^4.0.8",
|
|
35
|
+
"knip": "^5.85.0",
|
|
36
|
+
"lint-staged": "^16.2.7",
|
|
37
|
+
"oxfmt": "^0.35.0",
|
|
38
|
+
"oxlint": "^1.50.0",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"ultracite": "^7.2.4",
|
|
41
|
+
"vitest": "^4.0.18"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
45
|
+
"@mariozechner/pi-tui": "*",
|
|
46
|
+
"@sinclair/typebox": "*"
|
|
47
|
+
},
|
|
48
|
+
"pnpm": {
|
|
49
|
+
"onlyBuiltDependencies": [
|
|
50
|
+
"esbuild"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"pi": {
|
|
54
|
+
"extensions": [
|
|
55
|
+
"./src/index.ts"
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { parseSystemPrompt } from "./parser.js";
|
|
4
|
+
import { showReport } from "./report-view.js";
|
|
5
|
+
|
|
6
|
+
const extension: ExtensionFactory = (pi) => {
|
|
7
|
+
pi.registerCommand("token-burden", {
|
|
8
|
+
description: "Show token budget breakdown of the system prompt",
|
|
9
|
+
handler: async (_args, ctx) => {
|
|
10
|
+
const prompt = ctx.getSystemPrompt();
|
|
11
|
+
const parsed = parseSystemPrompt(prompt);
|
|
12
|
+
|
|
13
|
+
const usage = ctx.getContextUsage();
|
|
14
|
+
const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow;
|
|
15
|
+
|
|
16
|
+
if (!ctx.hasUI) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await showReport(parsed, contextWindow, ctx);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default extension;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { estimateTokens, parseSystemPrompt } from "./parser.js";
|
|
2
|
+
import type { ParsedPrompt } from "./parser.js";
|
|
3
|
+
|
|
4
|
+
describe("estimateTokens()", () => {
|
|
5
|
+
it("returns ceil(chars / 4)", () => {
|
|
6
|
+
expect(estimateTokens("")).toBe(0);
|
|
7
|
+
expect(estimateTokens("abcd")).toBe(1);
|
|
8
|
+
expect(estimateTokens("abcde")).toBe(2);
|
|
9
|
+
expect(estimateTokens("a")).toBe(1);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("parseSystemPrompt()", () => {
|
|
14
|
+
const basePrompt = [
|
|
15
|
+
"You are an expert coding assistant operating inside pi.",
|
|
16
|
+
"",
|
|
17
|
+
"Available tools:",
|
|
18
|
+
"- read: Read file contents",
|
|
19
|
+
"- bash: Execute bash commands",
|
|
20
|
+
"",
|
|
21
|
+
"Guidelines:",
|
|
22
|
+
"- Be concise",
|
|
23
|
+
"",
|
|
24
|
+
"Pi documentation (read only when the user asks about pi itself):",
|
|
25
|
+
"- Main documentation: /path/to/README.md",
|
|
26
|
+
"- Always read pi .md files completely and follow links to related docs",
|
|
27
|
+
].join("\n");
|
|
28
|
+
|
|
29
|
+
const agentsBlock = [
|
|
30
|
+
"",
|
|
31
|
+
"",
|
|
32
|
+
"# Project Context",
|
|
33
|
+
"",
|
|
34
|
+
"Project-specific instructions and guidelines:",
|
|
35
|
+
"",
|
|
36
|
+
"## /home/user/.pi/agent/AGENTS.md",
|
|
37
|
+
"",
|
|
38
|
+
"# Global Agent Guidelines",
|
|
39
|
+
"",
|
|
40
|
+
"## Before Acting",
|
|
41
|
+
"- Read files before editing.",
|
|
42
|
+
"",
|
|
43
|
+
"## /home/user/project/AGENTS.md",
|
|
44
|
+
"",
|
|
45
|
+
"# Project Rules",
|
|
46
|
+
"",
|
|
47
|
+
"- Follow TDD.",
|
|
48
|
+
].join("\n");
|
|
49
|
+
|
|
50
|
+
const skillsPreamble = [
|
|
51
|
+
"",
|
|
52
|
+
"",
|
|
53
|
+
"The following skills provide specialized instructions for specific tasks.",
|
|
54
|
+
"Use the read tool to load a skill's file when the task matches its description.",
|
|
55
|
+
"When a skill file references a relative path, resolve it against the skill directory.",
|
|
56
|
+
"",
|
|
57
|
+
].join("\n");
|
|
58
|
+
|
|
59
|
+
const skillsBlock = [
|
|
60
|
+
"<available_skills>",
|
|
61
|
+
" <skill>",
|
|
62
|
+
" <name>brainstorming</name>",
|
|
63
|
+
" <description>Explore user intent before implementation.</description>",
|
|
64
|
+
" <location>/home/user/skills/brainstorming/SKILL.md</location>",
|
|
65
|
+
" </skill>",
|
|
66
|
+
" <skill>",
|
|
67
|
+
" <name>tdd</name>",
|
|
68
|
+
" <description>Test-driven development workflow.</description>",
|
|
69
|
+
" <location>/home/user/skills/tdd/SKILL.md</location>",
|
|
70
|
+
" </skill>",
|
|
71
|
+
"</available_skills>",
|
|
72
|
+
].join("\n");
|
|
73
|
+
|
|
74
|
+
const metadata =
|
|
75
|
+
"\nCurrent date and time: Thursday, February 26, 2026\nCurrent working directory: /home/user/project";
|
|
76
|
+
|
|
77
|
+
it("parses a full system prompt into sections", () => {
|
|
78
|
+
const prompt =
|
|
79
|
+
basePrompt + agentsBlock + skillsPreamble + skillsBlock + metadata;
|
|
80
|
+
const result: ParsedPrompt = parseSystemPrompt(prompt);
|
|
81
|
+
|
|
82
|
+
expect(result.totalChars).toBe(prompt.length);
|
|
83
|
+
expect(result.totalTokens).toBe(Math.ceil(prompt.length / 4));
|
|
84
|
+
|
|
85
|
+
const labels = result.sections.map((s) => s.label);
|
|
86
|
+
expect(labels).toContain("Base prompt");
|
|
87
|
+
expect(labels).toContain("AGENTS.md files");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("parses a full system prompt with correct section labels", () => {
|
|
91
|
+
const prompt =
|
|
92
|
+
basePrompt + agentsBlock + skillsPreamble + skillsBlock + metadata;
|
|
93
|
+
const result = parseSystemPrompt(prompt);
|
|
94
|
+
|
|
95
|
+
const labels = result.sections.map((s) => s.label);
|
|
96
|
+
expect(labels).toContain("Skills (2)");
|
|
97
|
+
expect(labels).toContain("Metadata (date/time, cwd)");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("parses AGENTS.md files into children", () => {
|
|
101
|
+
const prompt = basePrompt + agentsBlock + metadata;
|
|
102
|
+
const result = parseSystemPrompt(prompt);
|
|
103
|
+
|
|
104
|
+
const agentsSection = result.sections.find((s) =>
|
|
105
|
+
s.label.includes("AGENTS.md")
|
|
106
|
+
);
|
|
107
|
+
expect(agentsSection?.children).toHaveLength(2);
|
|
108
|
+
expect(agentsSection?.children?.[0].label).toBe(
|
|
109
|
+
"/home/user/.pi/agent/AGENTS.md"
|
|
110
|
+
);
|
|
111
|
+
expect(agentsSection?.children?.[1].label).toBe(
|
|
112
|
+
"/home/user/project/AGENTS.md"
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("parses individual skills from XML", () => {
|
|
117
|
+
const prompt = basePrompt + skillsPreamble + skillsBlock + metadata;
|
|
118
|
+
const result = parseSystemPrompt(prompt);
|
|
119
|
+
|
|
120
|
+
expect(result.skills).toHaveLength(2);
|
|
121
|
+
expect(result.skills[0].name).toBe("brainstorming");
|
|
122
|
+
expect(result.skills[1].name).toBe("tdd");
|
|
123
|
+
expect(result.skills[0].chars).toBeGreaterThan(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("includes skill children in skills section", () => {
|
|
127
|
+
const prompt = basePrompt + skillsPreamble + skillsBlock + metadata;
|
|
128
|
+
const result = parseSystemPrompt(prompt);
|
|
129
|
+
|
|
130
|
+
const skillsSection = result.sections.find((s) =>
|
|
131
|
+
s.label.startsWith("Skills")
|
|
132
|
+
);
|
|
133
|
+
expect(skillsSection?.children).toHaveLength(2);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("handles a minimal prompt with no optional sections", () => {
|
|
137
|
+
const prompt = `You are a helpful assistant.${metadata}`;
|
|
138
|
+
const result = parseSystemPrompt(prompt);
|
|
139
|
+
|
|
140
|
+
expect(result.sections.length).toBeGreaterThanOrEqual(1);
|
|
141
|
+
expect(result.totalChars).toBe(prompt.length);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("detects SYSTEM.md / APPEND_SYSTEM.md content between base and project context", () => {
|
|
145
|
+
const appendContent =
|
|
146
|
+
"\n\nCustom SYSTEM.md instructions here.\nMore custom content.";
|
|
147
|
+
const prompt = basePrompt + appendContent + agentsBlock + metadata;
|
|
148
|
+
const result = parseSystemPrompt(prompt);
|
|
149
|
+
|
|
150
|
+
const systemMdSection = result.sections.find((s) =>
|
|
151
|
+
s.label.includes("SYSTEM.md")
|
|
152
|
+
);
|
|
153
|
+
expect(systemMdSection).toBeDefined();
|
|
154
|
+
expect(systemMdSection?.chars).toBeGreaterThan(0);
|
|
155
|
+
});
|
|
156
|
+
});
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse the assembled system prompt into measurable sections.
|
|
3
|
+
*
|
|
4
|
+
* The system prompt built by pi follows a predictable structure:
|
|
5
|
+
* 1. Base prompt (tools, guidelines, pi docs reference)
|
|
6
|
+
* 2. Optional SYSTEM.md / APPEND_SYSTEM.md content
|
|
7
|
+
* 3. Project Context (AGENTS.md files, each under `## <path>`)
|
|
8
|
+
* 4. Skills preamble + <available_skills> block
|
|
9
|
+
* 5. Date/time + cwd metadata
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
AgentsFileEntry,
|
|
14
|
+
ParsedPrompt,
|
|
15
|
+
PromptSection,
|
|
16
|
+
SkillEntry,
|
|
17
|
+
} from "./types.js";
|
|
18
|
+
|
|
19
|
+
export type { ParsedPrompt };
|
|
20
|
+
|
|
21
|
+
/** Token estimate using pi's built-in heuristic: ceil(chars / 4). */
|
|
22
|
+
export function estimateTokens(text: string): number {
|
|
23
|
+
return Math.ceil(text.length / 4);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Internal helpers (defined before use to satisfy no-use-before-define)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function measure(label: string, text: string): PromptSection {
|
|
31
|
+
return { label, chars: text.length, tokens: estimateTokens(text) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Return the smallest positive value, or -1 if none are positive. */
|
|
35
|
+
function firstPositive(...values: number[]): number {
|
|
36
|
+
let min = -1;
|
|
37
|
+
for (const v of values) {
|
|
38
|
+
if (v >= 0 && (min < 0 || v < min)) {
|
|
39
|
+
min = v;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return min;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Find where the base system prompt ends.
|
|
47
|
+
*
|
|
48
|
+
* The base prompt ends after the pi docs reference block. We look for
|
|
49
|
+
* "- Always read pi .md files" or "- When working on pi" as the terminal
|
|
50
|
+
* marker. Falls back to the first major section boundary.
|
|
51
|
+
*/
|
|
52
|
+
function findBasePromptEnd(
|
|
53
|
+
prompt: string,
|
|
54
|
+
projectCtxIdx: number,
|
|
55
|
+
skillsPreambleIdx: number,
|
|
56
|
+
dateLineIdx: number
|
|
57
|
+
): number {
|
|
58
|
+
const piDocsMarker = /^- (?:Always read pi|When working on pi).+$/gm;
|
|
59
|
+
let lastPiDocsEnd = -1;
|
|
60
|
+
for (const match of prompt.matchAll(piDocsMarker)) {
|
|
61
|
+
lastPiDocsEnd = match.index + match[0].length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (lastPiDocsEnd !== -1) {
|
|
65
|
+
return lastPiDocsEnd;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return firstPositive(projectCtxIdx, skillsPreambleIdx, dateLineIdx);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Parse `## /path/to/AGENTS.md` blocks inside the Project Context section. */
|
|
72
|
+
function parseAgentsFiles(contextBlock: string): AgentsFileEntry[] {
|
|
73
|
+
const files: AgentsFileEntry[] = [];
|
|
74
|
+
// Match `## ` headings that look like file paths (start with `/`).
|
|
75
|
+
const headingPattern = /^## (\/.+)$/gm;
|
|
76
|
+
const matches = [...contextBlock.matchAll(headingPattern)];
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < matches.length; i++) {
|
|
79
|
+
const [, path] = matches[i];
|
|
80
|
+
const blockStart = matches[i].index;
|
|
81
|
+
const blockEnd =
|
|
82
|
+
i + 1 < matches.length ? matches[i + 1].index : contextBlock.length;
|
|
83
|
+
const blockText = contextBlock.slice(blockStart, blockEnd);
|
|
84
|
+
files.push({
|
|
85
|
+
path,
|
|
86
|
+
chars: blockText.length,
|
|
87
|
+
tokens: estimateTokens(blockText),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return files;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Parse `<skill>` entries from the `<available_skills>` XML block. */
|
|
95
|
+
function parseSkillEntries(xmlBlock: string, out: SkillEntry[]): void {
|
|
96
|
+
const skillPattern = /<skill>([\s\S]*?)<\/skill>/g;
|
|
97
|
+
const namePattern = /<name>([\s\S]*?)<\/name>/;
|
|
98
|
+
const descPattern = /<description>([\s\S]*?)<\/description>/;
|
|
99
|
+
const locPattern = /<location>([\s\S]*?)<\/location>/;
|
|
100
|
+
|
|
101
|
+
for (const match of xmlBlock.matchAll(skillPattern)) {
|
|
102
|
+
const [fullEntry, inner] = match;
|
|
103
|
+
const name = inner.match(namePattern)?.[1]?.trim() ?? "unknown";
|
|
104
|
+
const description = inner.match(descPattern)?.[1]?.trim() ?? "";
|
|
105
|
+
const location = inner.match(locPattern)?.[1]?.trim() ?? "";
|
|
106
|
+
|
|
107
|
+
out.push({
|
|
108
|
+
name,
|
|
109
|
+
description,
|
|
110
|
+
location,
|
|
111
|
+
chars: fullEntry.length,
|
|
112
|
+
tokens: estimateTokens(fullEntry),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Compute the skills section end index, avoiding nested ternaries. */
|
|
118
|
+
function findSkillsSectionEnd(
|
|
119
|
+
availableSkillsEnd: number,
|
|
120
|
+
dateLineIdx: number,
|
|
121
|
+
promptLength: number
|
|
122
|
+
): number {
|
|
123
|
+
if (availableSkillsEnd !== -1) {
|
|
124
|
+
return availableSkillsEnd + "</available_skills>".length;
|
|
125
|
+
}
|
|
126
|
+
if (dateLineIdx !== -1) {
|
|
127
|
+
return dateLineIdx;
|
|
128
|
+
}
|
|
129
|
+
return promptLength;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Main parser
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parse a system prompt string into sections with token estimates.
|
|
138
|
+
*
|
|
139
|
+
* Uses known structural markers emitted by `buildSystemPrompt()`:
|
|
140
|
+
* - `# Project Context` heading
|
|
141
|
+
* - `The following skills provide specialized instructions` preamble
|
|
142
|
+
* - `<available_skills>` / `</available_skills>` XML block
|
|
143
|
+
* - `Current date and time:` footer
|
|
144
|
+
*/
|
|
145
|
+
export function parseSystemPrompt(prompt: string): ParsedPrompt {
|
|
146
|
+
const sections: PromptSection[] = [];
|
|
147
|
+
const skills: SkillEntry[] = [];
|
|
148
|
+
|
|
149
|
+
const projectCtxIdx = prompt.indexOf("\n\n# Project Context\n");
|
|
150
|
+
const skillsPreambleIdx = prompt.indexOf(
|
|
151
|
+
"\n\nThe following skills provide specialized instructions"
|
|
152
|
+
);
|
|
153
|
+
const availableSkillsStart = prompt.indexOf("<available_skills>");
|
|
154
|
+
const availableSkillsEnd = prompt.indexOf("</available_skills>");
|
|
155
|
+
const dateLineIdx = prompt.lastIndexOf("\nCurrent date and time:");
|
|
156
|
+
|
|
157
|
+
// 1. Base system prompt
|
|
158
|
+
const baseEnd = findBasePromptEnd(
|
|
159
|
+
prompt,
|
|
160
|
+
projectCtxIdx,
|
|
161
|
+
skillsPreambleIdx,
|
|
162
|
+
dateLineIdx
|
|
163
|
+
);
|
|
164
|
+
const baseText = baseEnd >= 0 ? prompt.slice(0, baseEnd) : prompt;
|
|
165
|
+
sections.push(measure("Base prompt", baseText));
|
|
166
|
+
|
|
167
|
+
// 2. Project Context / AGENTS.md files
|
|
168
|
+
if (projectCtxIdx !== -1) {
|
|
169
|
+
const contextStart = projectCtxIdx + 2; // skip leading \n\n
|
|
170
|
+
const contextEnd = firstPositive(skillsPreambleIdx, dateLineIdx);
|
|
171
|
+
const contextBlock =
|
|
172
|
+
contextEnd >= 0
|
|
173
|
+
? prompt.slice(contextStart, contextEnd)
|
|
174
|
+
: prompt.slice(contextStart);
|
|
175
|
+
|
|
176
|
+
const agentsFiles = parseAgentsFiles(contextBlock);
|
|
177
|
+
const children = agentsFiles.map((f) => ({
|
|
178
|
+
label: f.path,
|
|
179
|
+
chars: f.chars,
|
|
180
|
+
tokens: f.tokens,
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
sections.push({
|
|
184
|
+
...measure("AGENTS.md files", contextBlock),
|
|
185
|
+
children,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 3. Skills section
|
|
190
|
+
if (skillsPreambleIdx !== -1) {
|
|
191
|
+
const skillsSectionStart = skillsPreambleIdx + 2;
|
|
192
|
+
const skillsSectionEnd = findSkillsSectionEnd(
|
|
193
|
+
availableSkillsEnd,
|
|
194
|
+
dateLineIdx,
|
|
195
|
+
prompt.length
|
|
196
|
+
);
|
|
197
|
+
const skillsSectionText = prompt.slice(
|
|
198
|
+
skillsSectionStart,
|
|
199
|
+
skillsSectionEnd
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (availableSkillsStart !== -1 && availableSkillsEnd !== -1) {
|
|
203
|
+
const xmlBlock = prompt.slice(
|
|
204
|
+
availableSkillsStart,
|
|
205
|
+
availableSkillsEnd + "</available_skills>".length
|
|
206
|
+
);
|
|
207
|
+
parseSkillEntries(xmlBlock, skills);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const children = skills.map((s) => ({
|
|
211
|
+
label: s.name,
|
|
212
|
+
chars: s.chars,
|
|
213
|
+
tokens: s.tokens,
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
sections.push({
|
|
217
|
+
...measure(`Skills (${String(skills.length)})`, skillsSectionText),
|
|
218
|
+
children,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 4. Metadata footer
|
|
223
|
+
if (dateLineIdx !== -1) {
|
|
224
|
+
const metaText = prompt.slice(dateLineIdx + 1);
|
|
225
|
+
sections.push(measure("Metadata (date/time, cwd)", metaText));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 5. Detect SYSTEM.md / APPEND_SYSTEM.md gap
|
|
229
|
+
const nextSectionStart =
|
|
230
|
+
projectCtxIdx === -1 ? skillsPreambleIdx : projectCtxIdx;
|
|
231
|
+
|
|
232
|
+
if (baseEnd >= 0 && nextSectionStart >= 0 && nextSectionStart > baseEnd) {
|
|
233
|
+
const gap = prompt.slice(baseEnd, nextSectionStart);
|
|
234
|
+
const trimmed = gap.trim();
|
|
235
|
+
if (trimmed.length > 0) {
|
|
236
|
+
sections.splice(1, 0, measure("SYSTEM.md / APPEND_SYSTEM.md", trimmed));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const totalChars = prompt.length;
|
|
241
|
+
const totalTokens = estimateTokens(prompt);
|
|
242
|
+
|
|
243
|
+
return { sections, totalChars, totalTokens, skills };
|
|
244
|
+
}
|