generated from nhcarrigan/template
feat: massive overhaul to manage costs (#103)
### Explanation _No response_ ### Issue Closes #102 ### Attestations - [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [ ] I have pinned the dependencies to a specific patch version. ### Style - [ ] I have run the linter and resolved any errors. - [ ] My pull request uses an appropriate title, matching the conventional commit standards. - [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning _No response_ Reviewed-on: #103 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #103.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import type { ConversationSummary } from "$lib/stores/conversations";
|
||||
|
||||
/**
|
||||
* Sanitises a string for safe JSON serialization through Tauri IPC.
|
||||
* Removes control characters and lone surrogates that could cause issues
|
||||
* during JSON serialization/deserialization.
|
||||
*/
|
||||
export function sanitizeForJson(text: string): string {
|
||||
// Remove control characters except for common whitespace (tab, newline, carriage return)
|
||||
// These can cause JSON parsing issues and are rarely meaningful in conversation summaries.
|
||||
// eslint-disable-next-line no-control-regex -- regex uses control character codes
|
||||
let sanitized = text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
||||
|
||||
// Remove extended ASCII control chars (C1 control codes)
|
||||
sanitized = sanitized.replace(/[\x80-\x9F]/g, "");
|
||||
|
||||
// Remove lone surrogates (U+D800 to U+DFFF) which cause "unexpected end of hex escape"
|
||||
// errors in serde_json when they appear without proper pairing.
|
||||
// These are invalid in JSON and can cause parse failures.
|
||||
sanitized = sanitized.replace(/[\uD800-\uDFFF]/g, "");
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a prompt to ask Claude to summarise a conversation.
|
||||
* This can be sent as a user message to get a summary.
|
||||
*/
|
||||
export function generateSummaryPrompt(conversationContent: string): string {
|
||||
return `Please provide a concise summary of our conversation so far. Focus on:
|
||||
1. Key topics discussed
|
||||
2. Important decisions or conclusions made
|
||||
3. Any ongoing tasks or context that would be helpful to remember
|
||||
4. Code changes or files that were modified
|
||||
|
||||
Keep the summary brief but comprehensive enough to continue our work in a new session.
|
||||
|
||||
Here is our conversation:
|
||||
|
||||
${conversationContent}
|
||||
|
||||
Please provide the summary now:`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a context injection message to prepend to a new conversation.
|
||||
* This provides Claude with context from a previous session.
|
||||
*/
|
||||
export function generateContextInjection(summary: ConversationSummary): string {
|
||||
return `[Previous Session Context]
|
||||
The following is a summary from our previous conversation (${summary.messageCount} messages, approximately ${summary.tokenEstimate.toLocaleString()} tokens):
|
||||
|
||||
${summary.content}
|
||||
|
||||
[End of Previous Context]
|
||||
|
||||
Please continue from where we left off, or let me know if you need any clarification about the previous context.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the token count for a given string.
|
||||
* Uses a rough approximation of ~4 characters per token.
|
||||
*/
|
||||
export function estimateTokens(text: string): number {
|
||||
return Math.ceil(text.length / 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ConversationSummary object from summary content.
|
||||
*/
|
||||
export function createSummary(
|
||||
content: string,
|
||||
messageCount: number,
|
||||
originalTokenEstimate: number
|
||||
): ConversationSummary {
|
||||
return {
|
||||
generatedAt: new Date(),
|
||||
content,
|
||||
messageCount,
|
||||
tokenEstimate: originalTokenEstimate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a conversation should be compacted based on token usage.
|
||||
* Returns true if the conversation is using more than the threshold percentage
|
||||
* of the context window.
|
||||
*/
|
||||
export function shouldSuggestCompaction(
|
||||
contextTokensUsed: number,
|
||||
contextWindowLimit: number,
|
||||
thresholdPercent: number = 60
|
||||
): boolean {
|
||||
if (contextWindowLimit === 0) return false;
|
||||
const utilisationPercent = (contextTokensUsed / contextWindowLimit) * 100;
|
||||
return utilisationPercent >= thresholdPercent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a token count for display.
|
||||
*/
|
||||
export function formatTokenCount(tokens: number): string {
|
||||
if (tokens >= 1000000) {
|
||||
return `${(tokens / 1000000).toFixed(1)}M`;
|
||||
}
|
||||
if (tokens >= 1000) {
|
||||
return `${(tokens / 1000).toFixed(1)}K`;
|
||||
}
|
||||
return tokens.toString();
|
||||
}
|
||||
Reference in New Issue
Block a user