praana 0.5.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 +124 -0
- package/bin/praana.js +17 -0
- package/bin/pran.js +17 -0
- package/dist/app-banner.d.ts +11 -0
- package/dist/app-banner.js +161 -0
- package/dist/app-controller.d.ts +44 -0
- package/dist/app-controller.js +143 -0
- package/dist/app-identity.d.ts +18 -0
- package/dist/app-identity.js +52 -0
- package/dist/auto-compact.d.ts +16 -0
- package/dist/auto-compact.js +101 -0
- package/dist/cli-args.d.ts +14 -0
- package/dist/cli-args.js +69 -0
- package/dist/compile-classic.d.ts +21 -0
- package/dist/compile-classic.js +106 -0
- package/dist/compiler.d.ts +75 -0
- package/dist/compiler.js +406 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +433 -0
- package/dist/context-engine/activity-log.d.ts +9 -0
- package/dist/context-engine/activity-log.js +109 -0
- package/dist/context-engine/artifact-store.d.ts +32 -0
- package/dist/context-engine/artifact-store.js +272 -0
- package/dist/context-engine/bm25.d.ts +3 -0
- package/dist/context-engine/bm25.js +32 -0
- package/dist/context-engine/checkpoint.d.ts +34 -0
- package/dist/context-engine/checkpoint.js +430 -0
- package/dist/context-engine/classify.d.ts +3 -0
- package/dist/context-engine/classify.js +60 -0
- package/dist/context-engine/db.d.ts +73 -0
- package/dist/context-engine/db.js +505 -0
- package/dist/context-engine/distiller.d.ts +30 -0
- package/dist/context-engine/distiller.js +67 -0
- package/dist/context-engine/engine-compiler.d.ts +23 -0
- package/dist/context-engine/engine-compiler.js +297 -0
- package/dist/context-engine/error-tracker.d.ts +21 -0
- package/dist/context-engine/error-tracker.js +74 -0
- package/dist/context-engine/event-lineage.d.ts +26 -0
- package/dist/context-engine/event-lineage.js +120 -0
- package/dist/context-engine/extraction.d.ts +26 -0
- package/dist/context-engine/extraction.js +83 -0
- package/dist/context-engine/index.d.ts +82 -0
- package/dist/context-engine/index.js +238 -0
- package/dist/context-engine/scoring.d.ts +13 -0
- package/dist/context-engine/scoring.js +47 -0
- package/dist/context-engine/state-snapshot.d.ts +8 -0
- package/dist/context-engine/state-snapshot.js +50 -0
- package/dist/context-engine/summarize.d.ts +6 -0
- package/dist/context-engine/summarize.js +32 -0
- package/dist/context-engine/telemetry.d.ts +25 -0
- package/dist/context-engine/telemetry.js +64 -0
- package/dist/context-engine/turn-digest.d.ts +50 -0
- package/dist/context-engine/turn-digest.js +250 -0
- package/dist/context-engine/turn-ledger.d.ts +18 -0
- package/dist/context-engine/turn-ledger.js +184 -0
- package/dist/context-engine/turn-recorder.d.ts +24 -0
- package/dist/context-engine/turn-recorder.js +88 -0
- package/dist/context-engine/types.d.ts +201 -0
- package/dist/context-engine/types.js +4 -0
- package/dist/context-pressure.d.ts +19 -0
- package/dist/context-pressure.js +36 -0
- package/dist/distillers/generic.d.ts +14 -0
- package/dist/distillers/generic.js +93 -0
- package/dist/distillers/git-diff.d.ts +8 -0
- package/dist/distillers/git-diff.js +119 -0
- package/dist/distillers/index.d.ts +2 -0
- package/dist/distillers/index.js +16 -0
- package/dist/distillers/npm-test.d.ts +8 -0
- package/dist/distillers/npm-test.js +50 -0
- package/dist/distillers/rg-results.d.ts +8 -0
- package/dist/distillers/rg-results.js +28 -0
- package/dist/distillers/tsc-errors.d.ts +8 -0
- package/dist/distillers/tsc-errors.js +52 -0
- package/dist/event-log.d.ts +56 -0
- package/dist/event-log.js +214 -0
- package/dist/llm.d.ts +29 -0
- package/dist/llm.js +155 -0
- package/dist/logger.d.ts +94 -0
- package/dist/logger.js +287 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +54 -0
- package/dist/memory/confidence.d.ts +7 -0
- package/dist/memory/confidence.js +37 -0
- package/dist/memory/consolidation.d.ts +26 -0
- package/dist/memory/consolidation.js +166 -0
- package/dist/memory/db.d.ts +40 -0
- package/dist/memory/db.js +283 -0
- package/dist/memory/dedup.d.ts +6 -0
- package/dist/memory/dedup.js +50 -0
- package/dist/memory/embedder-factory.d.ts +3 -0
- package/dist/memory/embedder-factory.js +81 -0
- package/dist/memory/embeddings.d.ts +15 -0
- package/dist/memory/embeddings.js +67 -0
- package/dist/memory/index.d.ts +9 -0
- package/dist/memory/index.js +11 -0
- package/dist/memory/ollama-summarizer.d.ts +19 -0
- package/dist/memory/ollama-summarizer.js +72 -0
- package/dist/memory/openai-summarizer.d.ts +21 -0
- package/dist/memory/openai-summarizer.js +51 -0
- package/dist/memory/store.d.ts +61 -0
- package/dist/memory/store.js +502 -0
- package/dist/memory/summarizer-factory.d.ts +3 -0
- package/dist/memory/summarizer-factory.js +69 -0
- package/dist/memory/summarizer.d.ts +4 -0
- package/dist/memory/summarizer.js +112 -0
- package/dist/memory/types.d.ts +87 -0
- package/dist/memory/types.js +17 -0
- package/dist/model-context.d.ts +15 -0
- package/dist/model-context.js +212 -0
- package/dist/project-detector.d.ts +37 -0
- package/dist/project-detector.js +604 -0
- package/dist/render.d.ts +15 -0
- package/dist/render.js +46 -0
- package/dist/session.d.ts +118 -0
- package/dist/session.js +809 -0
- package/dist/skills/index.d.ts +69 -0
- package/dist/skills/index.js +885 -0
- package/dist/skills/types.d.ts +93 -0
- package/dist/skills/types.js +8 -0
- package/dist/slash-commands.d.ts +14 -0
- package/dist/slash-commands.js +301 -0
- package/dist/state-graph.d.ts +38 -0
- package/dist/state-graph.js +255 -0
- package/dist/status-bar.d.ts +54 -0
- package/dist/status-bar.js +184 -0
- package/dist/thinking-display.d.ts +21 -0
- package/dist/thinking-display.js +37 -0
- package/dist/tool-summary.d.ts +4 -0
- package/dist/tool-summary.js +67 -0
- package/dist/tools/index.d.ts +925 -0
- package/dist/tools/index.js +86 -0
- package/dist/tools/knowledge.d.ts +140 -0
- package/dist/tools/knowledge.js +260 -0
- package/dist/tools/memory.d.ts +39 -0
- package/dist/tools/memory.js +300 -0
- package/dist/tools/search-code.d.ts +134 -0
- package/dist/tools/search-code.js +390 -0
- package/dist/tools/system.d.ts +16 -0
- package/dist/tools/system.js +499 -0
- package/dist/tools/tool-def.d.ts +6 -0
- package/dist/tools/tool-def.js +3 -0
- package/dist/turn-control.d.ts +51 -0
- package/dist/turn-control.js +210 -0
- package/dist/turn.d.ts +20 -0
- package/dist/turn.js +624 -0
- package/dist/types.d.ts +233 -0
- package/dist/types.js +4 -0
- package/dist/ui/readline-ui.d.ts +2 -0
- package/dist/ui/readline-ui.js +176 -0
- package/dist/ui/tui/app.d.ts +13 -0
- package/dist/ui/tui/app.js +270 -0
- package/dist/ui/tui/busy-indicator.d.ts +2 -0
- package/dist/ui/tui/busy-indicator.js +13 -0
- package/dist/ui/tui/components/gutter-rule.d.ts +5 -0
- package/dist/ui/tui/components/gutter-rule.js +9 -0
- package/dist/ui/tui/components/inline-tool-row.d.ts +10 -0
- package/dist/ui/tui/components/inline-tool-row.js +8 -0
- package/dist/ui/tui/components/prompt-input.d.ts +20 -0
- package/dist/ui/tui/components/prompt-input.js +120 -0
- package/dist/ui/tui/components/system-line.d.ts +5 -0
- package/dist/ui/tui/components/system-line.js +6 -0
- package/dist/ui/tui/components/thinking-block.d.ts +11 -0
- package/dist/ui/tui/components/thinking-block.js +31 -0
- package/dist/ui/tui/components/toast-line.d.ts +4 -0
- package/dist/ui/tui/components/toast-line.js +8 -0
- package/dist/ui/tui/components/tool-result-line.d.ts +5 -0
- package/dist/ui/tui/components/tool-result-line.js +6 -0
- package/dist/ui/tui/components/turn-footer.d.ts +5 -0
- package/dist/ui/tui/components/turn-footer.js +7 -0
- package/dist/ui/tui/components/user-block.d.ts +6 -0
- package/dist/ui/tui/components/user-block.js +6 -0
- package/dist/ui/tui/logo-banner.d.ts +5 -0
- package/dist/ui/tui/logo-banner.js +8 -0
- package/dist/ui/tui/markdown-render.d.ts +16 -0
- package/dist/ui/tui/markdown-render.js +218 -0
- package/dist/ui/tui/palette.d.ts +12 -0
- package/dist/ui/tui/palette.js +13 -0
- package/dist/ui/tui/reasoning-summary.d.ts +12 -0
- package/dist/ui/tui/reasoning-summary.js +27 -0
- package/dist/ui/tui/reducer.d.ts +92 -0
- package/dist/ui/tui/reducer.js +260 -0
- package/dist/ui/tui/run.d.ts +3 -0
- package/dist/ui/tui/run.js +40 -0
- package/dist/ui/tui/sink.d.ts +4 -0
- package/dist/ui/tui/sink.js +89 -0
- package/dist/ui/tui/status-bar-view.d.ts +5 -0
- package/dist/ui/tui/status-bar-view.js +44 -0
- package/dist/ui/tui/terminal-height.d.ts +12 -0
- package/dist/ui/tui/terminal-height.js +20 -0
- package/dist/ui/tui/terminal-width.d.ts +2 -0
- package/dist/ui/tui/terminal-width.js +5 -0
- package/dist/ui/tui/tool-display.d.ts +23 -0
- package/dist/ui/tui/tool-display.js +217 -0
- package/dist/ui/tui/transcript-line.d.ts +12 -0
- package/dist/ui/tui/transcript-line.js +43 -0
- package/dist/ui/tui/transcript-replay.d.ts +12 -0
- package/dist/ui/tui/transcript-replay.js +117 -0
- package/dist/ui-events.d.ts +39 -0
- package/dist/ui-events.js +33 -0
- package/dist/ui.d.ts +77 -0
- package/dist/ui.js +179 -0
- package/package.json +73 -0
- package/praana.config.example.toml +231 -0
package/dist/compiler.js
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { getAppLogger } from "./logger.js";
|
|
2
|
+
/** Estimate token count from character count. 1 token ≈ 4 chars is rough but consistent. */
|
|
3
|
+
function estTokens(text) {
|
|
4
|
+
return Math.ceil(text.length / 4);
|
|
5
|
+
}
|
|
6
|
+
/** Build a deterministic prompt from structured state. */
|
|
7
|
+
export function compile(input) {
|
|
8
|
+
const sections = [];
|
|
9
|
+
// Generate state summary for system prompt
|
|
10
|
+
const stateSummary = buildStateSummary(input.stateGraph);
|
|
11
|
+
// ---- 1. SYSTEM FRAME ----
|
|
12
|
+
const frame = buildSystemFrame(input.cwd, input.sessionId, input.toolSchemas, stateSummary, input.agentsContext);
|
|
13
|
+
sections.push(frame);
|
|
14
|
+
// ---- 1b. SKILLS ----
|
|
15
|
+
if (input.skillsPromptSection) {
|
|
16
|
+
sections.push(input.skillsPromptSection);
|
|
17
|
+
}
|
|
18
|
+
// ---- 2. CROSS-SESSION MEMORY ----
|
|
19
|
+
if (input.memoryDigest && input.memoryDigest.trim()) {
|
|
20
|
+
const cs = buildCrossSessionMemory(input.memoryDigest);
|
|
21
|
+
sections.push(cs);
|
|
22
|
+
}
|
|
23
|
+
// ---- 3. ACTIVE STATE ----
|
|
24
|
+
const active = buildActiveState(input.stateGraph);
|
|
25
|
+
sections.push(active);
|
|
26
|
+
// ---- 4. PERIPHERAL STUBS ----
|
|
27
|
+
const peripheral = buildPeripheralStubs(input.stateGraph);
|
|
28
|
+
if (peripheral) {
|
|
29
|
+
sections.push(peripheral);
|
|
30
|
+
}
|
|
31
|
+
// ---- 5. RECENT TURNS ----
|
|
32
|
+
// Use token-based limiting for Recent Turns if recentTurnsTokenBudget is provided
|
|
33
|
+
const recentTurnsBudget = input.recentTurnsTokenBudget ?? Math.floor(input.tokenBudget * 0.3); // Default: 30% of total budget
|
|
34
|
+
const recent = buildRecentTurns(input.recentEvents, recentTurnsBudget);
|
|
35
|
+
sections.push(recent);
|
|
36
|
+
// ---- 6. CURRENT INPUT ----
|
|
37
|
+
if (input.userInput) {
|
|
38
|
+
const current = `## Current Input\n\nUser: ${input.userInput}`;
|
|
39
|
+
sections.push(current);
|
|
40
|
+
}
|
|
41
|
+
const fullPrompt = sections.join("\n\n");
|
|
42
|
+
// Token budget check
|
|
43
|
+
const estimatedTokens = estTokens(fullPrompt);
|
|
44
|
+
if (estimatedTokens > input.tokenBudget) {
|
|
45
|
+
getAppLogger().child("compiler").warn(`Prompt estimated at ${estimatedTokens} tokens, exceeds budget of ${input.tokenBudget}. Consider trimming.`);
|
|
46
|
+
}
|
|
47
|
+
return fullPrompt;
|
|
48
|
+
}
|
|
49
|
+
/** Compile with detailed token metrics per section. */
|
|
50
|
+
export function compileWithMetrics(input) {
|
|
51
|
+
const sections = [];
|
|
52
|
+
const metrics = {};
|
|
53
|
+
const reservedOutput = input.reservedOutputTokens ?? 0;
|
|
54
|
+
const usable = Math.max(0, input.tokenBudget - reservedOutput);
|
|
55
|
+
const maxMemoryTokens = Math.floor(usable * (input.memoriesBudgetRatio ?? 0.2));
|
|
56
|
+
const maxAgentsTokens = Math.floor(usable * (input.agentsBudgetRatio ?? 0.3));
|
|
57
|
+
const maxSkillsSectionTokens = Math.floor(usable * (input.skillsSectionBudgetRatio ?? 0.2));
|
|
58
|
+
const stateSummary = buildStateSummary(input.stateGraph);
|
|
59
|
+
const { text: agentsContext, truncated: agentsTruncated } = trimAgentsContext(input.agentsContext, maxAgentsTokens);
|
|
60
|
+
metrics.agentsContextTruncated = agentsTruncated;
|
|
61
|
+
// 1. SYSTEM FRAME
|
|
62
|
+
const frame = buildSystemFrame(input.cwd, input.sessionId, input.toolSchemas, stateSummary, agentsContext);
|
|
63
|
+
sections.push(frame);
|
|
64
|
+
metrics.systemFrameTokens = estTokens(frame);
|
|
65
|
+
metrics.agentsContextTokens = agentsContext ? estTokens(agentsContext) : 0;
|
|
66
|
+
// 1b. SKILLS (progressive disclosure)
|
|
67
|
+
let skillsSection = "";
|
|
68
|
+
if (input.skillsPromptSection) {
|
|
69
|
+
const { text, truncated } = trimSectionToTokenBudget(input.skillsPromptSection, maxSkillsSectionTokens, "skills section truncated to token budget");
|
|
70
|
+
skillsSection = text;
|
|
71
|
+
metrics.skillsTruncated = truncated;
|
|
72
|
+
sections.push(skillsSection);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
metrics.skillsTruncated = false;
|
|
76
|
+
}
|
|
77
|
+
metrics.skillsCatalogTokens = skillsSection ? estTokens(skillsSection) : 0;
|
|
78
|
+
// 1c. SESSION CHECKPOINT
|
|
79
|
+
let checkpointSection = "";
|
|
80
|
+
if (input.checkpointSection?.trim()) {
|
|
81
|
+
checkpointSection = input.checkpointSection.trim();
|
|
82
|
+
sections.push(checkpointSection);
|
|
83
|
+
}
|
|
84
|
+
metrics.checkpointTokens = checkpointSection ? estTokens(checkpointSection) : 0;
|
|
85
|
+
// 2. CROSS-SESSION MEMORY
|
|
86
|
+
let crossSection = "";
|
|
87
|
+
if (input.memoryDigest && input.memoryDigest.trim()) {
|
|
88
|
+
const { text, truncated } = trimSectionToTokenBudget(buildCrossSessionMemory(input.memoryDigest), maxMemoryTokens);
|
|
89
|
+
crossSection = text;
|
|
90
|
+
metrics.memoryTruncated = truncated;
|
|
91
|
+
sections.push(crossSection);
|
|
92
|
+
metrics.crossSessionTokens = estTokens(crossSection);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
metrics.crossSessionTokens = 0;
|
|
96
|
+
metrics.memoryTruncated = false;
|
|
97
|
+
}
|
|
98
|
+
// 3. ACTIVE STATE
|
|
99
|
+
const active = buildActiveState(input.stateGraph);
|
|
100
|
+
sections.push(active);
|
|
101
|
+
metrics.activeStateTokens = estTokens(active);
|
|
102
|
+
metrics.activeObjectCount = input.stateGraph.getActive().length;
|
|
103
|
+
// 4. PERIPHERAL STUBS
|
|
104
|
+
const peripheral = buildPeripheralStubs(input.stateGraph);
|
|
105
|
+
if (peripheral) {
|
|
106
|
+
sections.push(peripheral);
|
|
107
|
+
metrics.peripheralStubsTokens = estTokens(peripheral);
|
|
108
|
+
metrics.peripheralObjectCount = input.stateGraph.getPeripheral().length;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
metrics.peripheralStubsTokens = 0;
|
|
112
|
+
metrics.peripheralObjectCount = 0;
|
|
113
|
+
}
|
|
114
|
+
// 5. RECENT TURNS
|
|
115
|
+
const recentTurnsBudget = input.recentTurnsTokenBudget ?? Math.floor(input.tokenBudget * 0.3);
|
|
116
|
+
const { text: recentText, truncated } = buildRecentTurnsWithTruncationFlag(input.recentEvents, recentTurnsBudget);
|
|
117
|
+
sections.push(recentText);
|
|
118
|
+
metrics.recentTurnsTokens = estTokens(recentText);
|
|
119
|
+
metrics.recentTurnsTruncated = truncated;
|
|
120
|
+
// 6. CURRENT INPUT
|
|
121
|
+
let currentSection = "";
|
|
122
|
+
if (input.userInput) {
|
|
123
|
+
currentSection = `## Current Input\n\nUser: ${input.userInput}`;
|
|
124
|
+
sections.push(currentSection);
|
|
125
|
+
}
|
|
126
|
+
metrics.currentInputTokens = estTokens(currentSection);
|
|
127
|
+
const fullPrompt = sections.join("\n\n");
|
|
128
|
+
metrics.totalTokens = estTokens(fullPrompt);
|
|
129
|
+
if (metrics.totalTokens > input.tokenBudget) {
|
|
130
|
+
getAppLogger().child("compiler").warn(`Prompt estimated at ${metrics.totalTokens} tokens, exceeds budget of ${input.tokenBudget}.`);
|
|
131
|
+
}
|
|
132
|
+
return { prompt: fullPrompt, metrics: metrics };
|
|
133
|
+
}
|
|
134
|
+
// ---- Section builders ----
|
|
135
|
+
export function buildSystemFrame(cwd, sessionId, toolSchemas, stateSummary, agentsContext) {
|
|
136
|
+
const lines = [
|
|
137
|
+
"# System",
|
|
138
|
+
"",
|
|
139
|
+
"You are PRAANA, a coding agent with persistent memory.",
|
|
140
|
+
`Working directory: ${cwd}`,
|
|
141
|
+
`Session ID: ${sessionId}`,
|
|
142
|
+
];
|
|
143
|
+
if (agentsContext && agentsContext.trim()) {
|
|
144
|
+
lines.push("", "## Project Context", "", agentsContext.trim());
|
|
145
|
+
}
|
|
146
|
+
if (stateSummary) {
|
|
147
|
+
lines.push("", "## Working Memory Status", "", stateSummary);
|
|
148
|
+
}
|
|
149
|
+
lines.push("", "## Available Tools", "", ...toolSchemas.map((t) => `- ${t}`), "", "Use tools when needed to accomplish the user's request. Respond concisely.", "", "## Evidence-First Assertions", "", "Before stating how the codebase works — especially negative claims like \"X is not implemented\" — follow this checklist:", "1. Scan Active State notes and constraints for relevant keywords.", "2. Scan Recent Turns for prior read_file/shell tool results about that topic.", "3. If evidence is missing or stale, call search_session_log or re-read the source.", "4. Only assert after you have explicit evidence from this repository context.", "", "## Implicit Knowledge Capture", "", "When the user mentions a preference, convention, or project fact without explicitly", "requesting a state object, capture it proactively. This is critical for long sessions", "where casual remarks get lost after a few turns. Examples:", "- User says \"let's use pnpm\" → call add_constraint(\"Use pnpm for package management\")", "- User says \"not npm, pnpm\" → call add_constraint(\"Use pnpm, not npm\")", "- User corrects you → call add_note(\"User corrected X to Y\")", "- User mentions a convention → call add_constraint", "- User says \"I prefer\" / \"let's do\" / \"how about\" / \"we always\" → call add_constraint", "- User says \"never\" / \"don't\" / \"make sure\" → call add_constraint", "Don't over-capture trivial remarks. Capture anything that would prevent a future mistake.", "", "## Memory Management", "", "You have working memory with three tiers: active (full content), soft (one-line stub), and hard (minimal anchor).", "Periodically call soft_unload on stale notes/tasks and complete_task when work is done to keep your working memory clean.", "To recover earlier content from this session (reviews, findings, tool output), use search_session_log — not recall (recall is cross-session SQLite only).", "After significant analysis, call add_note immediately so key findings survive when recent turns scroll out of the prompt.", "Notes must capture semantic findings (what you learned), not activity logs (which files you read).", "Good note: \"turn.ts uses piStream() for streaming — streaming IS implemented\". Bad note: \"read turn.ts, session.ts...\".", "Session event log file: ~/.praana/sessions/<session_id>/events.jsonl", "See the Active State and Peripheral Memory sections below for your current working memory.", "", "## Tool Safety", "", "RULE: Never call write_file, edit_file, or use shell commands with file write side-effects (e.g. `echo > file`, `sed -i`, `tee`) unless the user's message in THIS turn explicitly requests changes. Describing what you would change does not count. If unsure, ask first.");
|
|
150
|
+
return lines.join("\n");
|
|
151
|
+
}
|
|
152
|
+
export function buildCrossSessionMemory(digest) {
|
|
153
|
+
return [
|
|
154
|
+
"# Cross-Session Memory",
|
|
155
|
+
"",
|
|
156
|
+
digest,
|
|
157
|
+
"",
|
|
158
|
+
"Use recall('query') to search your knowledge base for more.",
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
161
|
+
export function buildActiveState(stateGraph) {
|
|
162
|
+
const active = stateGraph.getActive();
|
|
163
|
+
if (active.length === 0) {
|
|
164
|
+
return "# Active State\n\nNo active state.";
|
|
165
|
+
}
|
|
166
|
+
// Group by kind
|
|
167
|
+
const groups = groupByKind(active);
|
|
168
|
+
const lines = ["# Active State"];
|
|
169
|
+
for (const [kind, objects] of Object.entries(groups)) {
|
|
170
|
+
lines.push("", `## ${capitalizePlural(kind)}`);
|
|
171
|
+
for (const obj of objects) {
|
|
172
|
+
lines.push(renderActiveObject(obj));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
177
|
+
export function buildPeripheralStubs(stateGraph) {
|
|
178
|
+
const peripheral = stateGraph.getPeripheral();
|
|
179
|
+
if (peripheral.length === 0)
|
|
180
|
+
return null;
|
|
181
|
+
const lines = ["# Peripheral Memory"];
|
|
182
|
+
for (const obj of peripheral) {
|
|
183
|
+
if (obj.tier === "soft") {
|
|
184
|
+
lines.push(`- ${obj.id} [${obj.kind}]: ${summarizeForStub(obj)}`);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// hard
|
|
188
|
+
lines.push(`- ${obj.id} [${obj.kind}]`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
lines.push("", "Use hydrate('<id>') to bring any of these into active memory.");
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Calculate token savings: compare compact peripheral stubs vs.
|
|
196
|
+
* rendering the same objects in full active form.
|
|
197
|
+
* Returns { compactTokens, fullTokens, savedTokens, savingsRatio }.
|
|
198
|
+
*/
|
|
199
|
+
export function calculateTokenSavings(stateGraph) {
|
|
200
|
+
const peripheral = stateGraph.getPeripheral(); // soft + hard
|
|
201
|
+
if (peripheral.length === 0) {
|
|
202
|
+
return { compactTokens: 0, fullTokens: 0, savedTokens: 0, savingsRatio: 0 };
|
|
203
|
+
}
|
|
204
|
+
// Build how peripherals are actually rendered (stubs)
|
|
205
|
+
const compactLines = [];
|
|
206
|
+
for (const obj of peripheral) {
|
|
207
|
+
if (obj.tier === "soft") {
|
|
208
|
+
compactLines.push(`- ${obj.id} [${obj.kind}]: ${summarizeForStub(obj)}`);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
compactLines.push(`- ${obj.id} [${obj.kind}]`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const compact = compactLines.join("\n");
|
|
215
|
+
const compactTokens = estTokens(compact);
|
|
216
|
+
// Build what they would cost if all rendered in full active form
|
|
217
|
+
const fullLines = [];
|
|
218
|
+
for (const obj of peripheral) {
|
|
219
|
+
fullLines.push(renderActiveObject(obj));
|
|
220
|
+
}
|
|
221
|
+
const full = fullLines.join("\n");
|
|
222
|
+
const fullTokens = estTokens(full);
|
|
223
|
+
const savedTokens = Math.max(0, fullTokens - compactTokens);
|
|
224
|
+
const savingsRatio = fullTokens > 0 ? savedTokens / fullTokens : 0;
|
|
225
|
+
return { compactTokens, fullTokens, savedTokens, savingsRatio };
|
|
226
|
+
}
|
|
227
|
+
function buildRecentTurns(events, tokenBudget) {
|
|
228
|
+
const { text } = buildRecentTurnsWithTruncationFlag(events, tokenBudget);
|
|
229
|
+
return text;
|
|
230
|
+
}
|
|
231
|
+
function buildRecentTurnsWithTruncationFlag(events, tokenBudget) {
|
|
232
|
+
if (events.length === 0) {
|
|
233
|
+
return { text: "# Recent Turns\n\n(no recent events)", truncated: false };
|
|
234
|
+
}
|
|
235
|
+
const lines = ["# Recent Turns"];
|
|
236
|
+
const filtered = events.filter((e) => e.kind !== "context_action" && e.kind !== "system_note");
|
|
237
|
+
let lastToolName;
|
|
238
|
+
let estimatedTokens = Math.ceil("# Recent Turns".length / 4);
|
|
239
|
+
let truncated = false;
|
|
240
|
+
for (const ev of filtered) {
|
|
241
|
+
let line = "";
|
|
242
|
+
switch (ev.kind) {
|
|
243
|
+
case "user_message":
|
|
244
|
+
line = `User: ${ev.payload.text ?? ""}`;
|
|
245
|
+
break;
|
|
246
|
+
case "agent_message": {
|
|
247
|
+
const text = truncateText(ev.payload.text, 800);
|
|
248
|
+
line = `PRAANA: ${text}`;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case "tool_call":
|
|
252
|
+
lastToolName = ev.payload.tool;
|
|
253
|
+
line = `Tool call: ${lastToolName ?? "unknown"}(${JSON.stringify(ev.payload.args ?? {})})`;
|
|
254
|
+
break;
|
|
255
|
+
case "tool_result": {
|
|
256
|
+
const result = ev.payload.result;
|
|
257
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
258
|
+
const toolName = ev.payload.tool ?? lastToolName;
|
|
259
|
+
const maxLen = getToolResultTruncation(toolName);
|
|
260
|
+
line = `Result: ${truncateText(resultStr, maxLen)}`;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (line) {
|
|
265
|
+
if (tokenBudget) {
|
|
266
|
+
const lineTokens = Math.ceil(line.length / 4);
|
|
267
|
+
if (estimatedTokens + lineTokens > tokenBudget) {
|
|
268
|
+
lines.push("\n... (truncated due to token budget)");
|
|
269
|
+
truncated = true;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
estimatedTokens += lineTokens;
|
|
273
|
+
}
|
|
274
|
+
lines.push(line);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { text: lines.join("\n"), truncated };
|
|
278
|
+
}
|
|
279
|
+
/** Get truncation limit for tool results based on tool name */
|
|
280
|
+
function getToolResultTruncation(toolName) {
|
|
281
|
+
// Shell results can be longer (500 chars)
|
|
282
|
+
if (toolName === "shell")
|
|
283
|
+
return 500;
|
|
284
|
+
// write_file and read_file (show) results should be shorter (200 chars)
|
|
285
|
+
if (toolName === "write_file" || toolName === "read_file" || toolName === "show")
|
|
286
|
+
return 200;
|
|
287
|
+
// Default for other tools
|
|
288
|
+
return 500;
|
|
289
|
+
}
|
|
290
|
+
// ---- Helpers ----
|
|
291
|
+
/** Build a brief summary of working memory state for the system prompt */
|
|
292
|
+
export function buildStateSummary(stateGraph) {
|
|
293
|
+
const active = stateGraph.getActive();
|
|
294
|
+
const peripheral = stateGraph.getPeripheral();
|
|
295
|
+
const activeByKind = groupByKind(active);
|
|
296
|
+
const parts = [];
|
|
297
|
+
for (const [kind, objects] of Object.entries(activeByKind)) {
|
|
298
|
+
parts.push(`${objects.length} ${kind}(s)`);
|
|
299
|
+
}
|
|
300
|
+
if (peripheral.length > 0) {
|
|
301
|
+
parts.push(`${peripheral.length} in peripheral memory`);
|
|
302
|
+
}
|
|
303
|
+
if (parts.length === 0) {
|
|
304
|
+
return "No working memory objects yet.";
|
|
305
|
+
}
|
|
306
|
+
return `You have ${parts.join(", ")}. Use soft_unload/hard_unload to demote and hydrate to restore.`;
|
|
307
|
+
}
|
|
308
|
+
function groupByKind(objects) {
|
|
309
|
+
const groups = {};
|
|
310
|
+
for (const obj of objects) {
|
|
311
|
+
const k = obj.kind;
|
|
312
|
+
if (!groups[k])
|
|
313
|
+
groups[k] = [];
|
|
314
|
+
groups[k].push(obj);
|
|
315
|
+
}
|
|
316
|
+
return groups;
|
|
317
|
+
}
|
|
318
|
+
function capitalizePlural(kind) {
|
|
319
|
+
switch (kind) {
|
|
320
|
+
case "task":
|
|
321
|
+
return "Tasks";
|
|
322
|
+
case "decision":
|
|
323
|
+
return "Decisions";
|
|
324
|
+
case "constraint":
|
|
325
|
+
return "Constraints";
|
|
326
|
+
case "note":
|
|
327
|
+
return "Notes";
|
|
328
|
+
default:
|
|
329
|
+
return kind;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function renderActiveObject(obj) {
|
|
333
|
+
const focusTag = obj.focused ? "[FOCUS] " : "";
|
|
334
|
+
const prefix = `- ${obj.id} `;
|
|
335
|
+
switch (obj.kind) {
|
|
336
|
+
case "task": {
|
|
337
|
+
const p = obj.payload;
|
|
338
|
+
let s = `${prefix}${focusTag}[${p.status}] ${p.title}`;
|
|
339
|
+
if (p.description)
|
|
340
|
+
s += `\n ${p.description}`;
|
|
341
|
+
return s;
|
|
342
|
+
}
|
|
343
|
+
case "decision": {
|
|
344
|
+
const p = obj.payload;
|
|
345
|
+
return `${prefix}${focusTag}${p.summary}\n Rationale: ${p.rationale}`;
|
|
346
|
+
}
|
|
347
|
+
case "constraint":
|
|
348
|
+
case "note": {
|
|
349
|
+
const text = obj.payload.text;
|
|
350
|
+
return `${prefix}${focusTag}${text}`;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function summarizeForStub(obj) {
|
|
355
|
+
switch (obj.kind) {
|
|
356
|
+
case "task":
|
|
357
|
+
return obj.payload.title;
|
|
358
|
+
case "decision":
|
|
359
|
+
return obj.payload.summary;
|
|
360
|
+
case "constraint":
|
|
361
|
+
case "note": {
|
|
362
|
+
const text = obj.payload.text;
|
|
363
|
+
return text.length > 80 ? text.slice(0, 80) + "..." : text;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function truncateText(text, maxLen) {
|
|
368
|
+
const s = typeof text === "string" ? text : JSON.stringify(text);
|
|
369
|
+
if (s.length <= maxLen)
|
|
370
|
+
return s;
|
|
371
|
+
return s.slice(0, maxLen) + "...";
|
|
372
|
+
}
|
|
373
|
+
export function trimSectionToTokenBudget(text, maxTokens, truncationNote = "memory section truncated to token budget") {
|
|
374
|
+
if (maxTokens <= 0 || estTokens(text) <= maxTokens) {
|
|
375
|
+
return { text, truncated: false };
|
|
376
|
+
}
|
|
377
|
+
const lines = text.split("\n");
|
|
378
|
+
const kept = [];
|
|
379
|
+
let tokens = 0;
|
|
380
|
+
for (const line of lines) {
|
|
381
|
+
const lineTokens = estTokens(line + "\n");
|
|
382
|
+
if (kept.length > 0 && tokens + lineTokens > maxTokens) {
|
|
383
|
+
kept.push(`... (${truncationNote})`);
|
|
384
|
+
return { text: kept.join("\n"), truncated: true };
|
|
385
|
+
}
|
|
386
|
+
kept.push(line);
|
|
387
|
+
tokens += lineTokens;
|
|
388
|
+
}
|
|
389
|
+
return { text: kept.join("\n"), truncated: false };
|
|
390
|
+
}
|
|
391
|
+
export function trimAgentsContext(agentsContext, maxTokens) {
|
|
392
|
+
if (!agentsContext?.trim())
|
|
393
|
+
return { text: null, truncated: false };
|
|
394
|
+
const trimmed = agentsContext.trim();
|
|
395
|
+
if (maxTokens <= 0 || estTokens(trimmed) <= maxTokens) {
|
|
396
|
+
return { text: trimmed, truncated: false };
|
|
397
|
+
}
|
|
398
|
+
const lines = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
399
|
+
const headings = lines.filter((l) => l.startsWith("#") || l.startsWith("<!--"));
|
|
400
|
+
const summary = headings.slice(0, 5).join("; ") || lines[0]?.slice(0, 120) || "project context";
|
|
401
|
+
const degraded = `[Project context truncated to fit budget] ${summary}`;
|
|
402
|
+
if (estTokens(degraded) <= maxTokens) {
|
|
403
|
+
return { text: degraded, truncated: true };
|
|
404
|
+
}
|
|
405
|
+
return { text: degraded.slice(0, maxTokens * 4), truncated: true };
|
|
406
|
+
}
|
package/dist/config.d.ts
ADDED