@tenex-chat/backend 0.9.5 → 0.9.7

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.
Files changed (148) hide show
  1. package/README.md +5 -1
  2. package/dist/daemon-wrapper.cjs +34 -0
  3. package/dist/index.js +59268 -0
  4. package/dist/wrapper.js +171 -0
  5. package/package.json +19 -27
  6. package/src/agents/AgentRegistry.ts +9 -7
  7. package/src/agents/AgentStorage.ts +24 -1
  8. package/src/agents/agent-installer.ts +6 -0
  9. package/src/agents/agent-loader.ts +7 -2
  10. package/src/agents/constants.ts +10 -2
  11. package/src/agents/execution/AgentExecutor.ts +35 -6
  12. package/src/agents/execution/StreamCallbacks.ts +53 -13
  13. package/src/agents/execution/StreamExecutionHandler.ts +110 -16
  14. package/src/agents/execution/StreamSetup.ts +19 -9
  15. package/src/agents/execution/ToolEventHandlers.ts +112 -0
  16. package/src/agents/role-categories.ts +53 -0
  17. package/src/agents/types/runtime.ts +7 -0
  18. package/src/agents/types/storage.ts +7 -0
  19. package/src/commands/agent/import/openclaw-distiller.ts +63 -7
  20. package/src/commands/agent/import/openclaw-reader.ts +54 -0
  21. package/src/commands/agent/import/openclaw.ts +120 -29
  22. package/src/commands/agent/index.ts +83 -2
  23. package/src/commands/setup/display.ts +123 -0
  24. package/src/commands/setup/embed.ts +13 -13
  25. package/src/commands/setup/global-system-prompt.ts +15 -17
  26. package/src/commands/setup/image.ts +17 -20
  27. package/src/commands/setup/interactive.ts +37 -20
  28. package/src/commands/setup/llm.ts +12 -7
  29. package/src/commands/setup/onboarding.ts +1580 -248
  30. package/src/commands/setup/providers.ts +3 -3
  31. package/src/conversations/ConversationStore.ts +23 -2
  32. package/src/conversations/MessageBuilder.ts +51 -73
  33. package/src/conversations/formatters/utils/conversation-transcript-formatter.ts +425 -0
  34. package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +40 -98
  35. package/src/conversations/search/embeddings/ConversationIndexingJob.ts +40 -52
  36. package/src/conversations/services/ConversationSummarizer.ts +1 -2
  37. package/src/conversations/types.ts +11 -0
  38. package/src/daemon/Daemon.ts +78 -57
  39. package/src/daemon/ProjectRuntime.ts +6 -12
  40. package/src/daemon/SubscriptionManager.ts +13 -0
  41. package/src/daemon/index.ts +0 -1
  42. package/src/event-handler/index.ts +1 -0
  43. package/src/index.ts +20 -1
  44. package/src/llm/ChunkHandler.ts +1 -1
  45. package/src/llm/FinishHandler.ts +28 -4
  46. package/src/llm/LLMConfigEditor.ts +218 -106
  47. package/src/llm/index.ts +0 -4
  48. package/src/llm/meta/MetaModelResolver.ts +3 -18
  49. package/src/llm/middleware/message-sanitizer.ts +153 -0
  50. package/src/llm/providers/ollama-models.ts +0 -38
  51. package/src/llm/service.ts +50 -15
  52. package/src/llm/types.ts +0 -12
  53. package/src/llm/utils/ConfigurationManager.ts +88 -465
  54. package/src/llm/utils/ConfigurationTester.ts +42 -185
  55. package/src/llm/utils/ModelSelector.ts +156 -92
  56. package/src/llm/utils/ProviderConfigUI.ts +10 -141
  57. package/src/llm/utils/models-dev-cache.ts +102 -23
  58. package/src/llm/utils/provider-select-prompt.ts +284 -0
  59. package/src/llm/utils/provider-setup.ts +81 -34
  60. package/src/llm/utils/variant-list-prompt.ts +361 -0
  61. package/src/nostr/AgentEventDecoder.ts +1 -0
  62. package/src/nostr/AgentEventEncoder.ts +37 -0
  63. package/src/nostr/AgentProfilePublisher.ts +13 -0
  64. package/src/nostr/AgentPublisher.ts +26 -0
  65. package/src/nostr/kinds.ts +1 -0
  66. package/src/nostr/ndkClient.ts +4 -1
  67. package/src/nostr/types.ts +12 -0
  68. package/src/prompts/fragments/25-rag-instructions.ts +22 -21
  69. package/src/prompts/fragments/31-agents-md-guidance.ts +7 -21
  70. package/src/prompts/fragments/index.ts +2 -0
  71. package/src/prompts/utils/systemPromptBuilder.ts +18 -28
  72. package/src/services/AgentDefinitionMonitor.ts +8 -0
  73. package/src/services/ConfigService.ts +34 -0
  74. package/src/services/PubkeyService.ts +7 -1
  75. package/src/services/compression/CompressionService.ts +133 -74
  76. package/src/services/compression/compression-utils.ts +110 -19
  77. package/src/services/config/types.ts +0 -6
  78. package/src/services/dispatch/AgentDispatchService.ts +79 -0
  79. package/src/services/intervention/InterventionService.ts +78 -5
  80. package/src/services/nip46/Nip46SigningService.ts +30 -1
  81. package/src/services/projects/ProjectContext.ts +8 -6
  82. package/src/services/rag/RAGCollectionRegistry.ts +199 -0
  83. package/src/services/rag/RAGDatabaseService.ts +2 -7
  84. package/src/services/rag/RAGOperations.ts +25 -45
  85. package/src/services/rag/RAGService.ts +0 -31
  86. package/src/services/rag/RagSubscriptionService.ts +71 -122
  87. package/src/services/rag/rag-utils.ts +13 -0
  88. package/src/services/ral/RALRegistry.ts +25 -184
  89. package/src/services/reports/ReportEmbeddingService.ts +63 -113
  90. package/src/services/search/UnifiedSearchService.ts +115 -4
  91. package/src/services/search/index.ts +1 -0
  92. package/src/services/search/projectFilter.ts +20 -4
  93. package/src/services/search/providers/ConversationSearchProvider.ts +1 -0
  94. package/src/services/search/providers/GenericCollectionSearchProvider.ts +81 -0
  95. package/src/services/search/providers/LessonSearchProvider.ts +1 -8
  96. package/src/services/search/providers/ReportSearchProvider.ts +1 -0
  97. package/src/services/search/types.ts +24 -3
  98. package/src/services/trust-pubkeys/SystemPubkeyListService.ts +148 -0
  99. package/src/services/trust-pubkeys/TrustPubkeyService.ts +70 -9
  100. package/src/telemetry/setup.ts +2 -13
  101. package/src/tools/implementations/ask.ts +3 -3
  102. package/src/tools/implementations/conversation_get.ts +28 -268
  103. package/src/tools/implementations/fs_grep.ts +6 -6
  104. package/src/tools/implementations/fs_read.ts +2 -0
  105. package/src/tools/implementations/fs_write.ts +2 -0
  106. package/src/tools/implementations/learn.ts +38 -50
  107. package/src/tools/implementations/rag_add_documents.ts +6 -4
  108. package/src/tools/implementations/rag_create_collection.ts +37 -4
  109. package/src/tools/implementations/rag_delete_collection.ts +9 -0
  110. package/src/tools/implementations/{search.ts → rag_search.ts} +31 -25
  111. package/src/tools/registry.ts +7 -8
  112. package/src/tools/types.ts +11 -2
  113. package/src/tools/utils/transcript-args.ts +13 -0
  114. package/src/utils/cli-theme.ts +13 -0
  115. package/src/utils/logger.ts +55 -0
  116. package/src/utils/metadataKeys.ts +17 -0
  117. package/src/utils/sqlEscaping.ts +39 -0
  118. package/src/wrapper.ts +7 -3
  119. package/dist/src/index.js +0 -46790
  120. package/dist/tenex-backend-wrapper.cjs +0 -3
  121. package/src/agents/execution/constants.ts +0 -16
  122. package/src/agents/execution/index.ts +0 -3
  123. package/src/agents/index.ts +0 -4
  124. package/src/commands/agent.ts +0 -235
  125. package/src/conversations/formatters/DelegationXmlFormatter.ts +0 -64
  126. package/src/conversations/formatters/index.ts +0 -9
  127. package/src/conversations/index.ts +0 -2
  128. package/src/conversations/utils/content-utils.ts +0 -69
  129. package/src/daemon/UnixSocketTransport.ts +0 -318
  130. package/src/event-handler/newConversation.ts +0 -165
  131. package/src/events/NDKProjectStatus.ts +0 -384
  132. package/src/events/index.ts +0 -4
  133. package/src/lib/json-parser.ts +0 -30
  134. package/src/llm/RecordingState.ts +0 -37
  135. package/src/llm/StreamPublisher.ts +0 -40
  136. package/src/llm/middleware/flight-recorder.ts +0 -188
  137. package/src/llm/utils/claudeCodePromptCompiler.ts +0 -141
  138. package/src/nostr/constants.ts +0 -38
  139. package/src/prompts/core/index.ts +0 -3
  140. package/src/prompts/index.ts +0 -21
  141. package/src/services/image/index.ts +0 -12
  142. package/src/services/status/index.ts +0 -11
  143. package/src/telemetry/diagnostics.ts +0 -27
  144. package/src/tools/implementations/rag_query.ts +0 -107
  145. package/src/types/index.ts +0 -46
  146. package/src/utils/agentFetcher.ts +0 -107
  147. package/src/utils/conversation-utils.ts +0 -1
  148. package/src/utils/process.ts +0 -49
@@ -77,15 +77,16 @@ rag_add_documents({
77
77
  })
78
78
  \`\`\`
79
79
 
80
- ### 3. rag_query
81
- Perform semantic search on a collection.
80
+ ### 3. rag_search
81
+ Search across ALL project knowledge — reports, conversations, lessons, and any
82
+ additional RAG collections — using natural language semantic search.
82
83
 
83
84
  \`\`\`typescript
84
- rag_query({
85
- collection: "project_knowledge",
86
- query_text: "How does authentication work?",
87
- top_k: 5, // Number of results (1-100)
88
- include_metadata: true // Include document metadata
85
+ rag_search({
86
+ query: "How does authentication work?",
87
+ limit: 10, // Maximum results (default: 10)
88
+ collections: ["reports", "lessons"], // Optional: filter to specific collections
89
+ prompt: "Summarize the auth approach" // Optional: LLM extraction
89
90
  })
90
91
  \`\`\`
91
92
 
@@ -143,7 +144,7 @@ All tools use standardized error responses:
143
144
  {
144
145
  "success": false,
145
146
  "error": "Descriptive error message",
146
- "toolName": "rag_query"
147
+ "toolName": "rag_search"
147
148
  }
148
149
  \`\`\`
149
150
 
@@ -169,9 +170,9 @@ rag_add_documents({
169
170
  })
170
171
 
171
172
  // Later, retrieve relevant context
172
- rag_query({
173
- collection: "agent_insights",
174
- query_text: "What are the user's programming language preferences?"
173
+ rag_search({
174
+ query: "What are the user's programming language preferences?",
175
+ collections: ["agent_insights"]
175
176
  })
176
177
  \`\`\`
177
178
 
@@ -192,9 +193,9 @@ rag_add_documents({
192
193
  })
193
194
 
194
195
  // Query for specific information
195
- rag_query({
196
- collection: "project_docs",
197
- query_text: "API authentication methods"
196
+ rag_search({
197
+ query: "API authentication methods",
198
+ collections: ["project_docs"]
198
199
  })
199
200
  \`\`\`
200
201
 
@@ -222,9 +223,9 @@ rag_add_documents({
222
223
  })
223
224
 
224
225
  // Find related lessons semantically
225
- rag_query({
226
- collection: "lessons",
227
- query_text: "How to handle promise rejections"
226
+ rag_search({
227
+ query: "How to handle promise rejections",
228
+ collections: ["lessons"]
228
229
  })
229
230
  \`\`\`
230
231
 
@@ -247,9 +248,9 @@ rag_add_documents({
247
248
  }]
248
249
  })
249
250
 
250
- rag_query({
251
- collection: "code_patterns",
252
- query_text: "authentication hook implementation"
251
+ rag_search({
252
+ query: "authentication hook implementation",
253
+ collections: ["code_patterns"]
253
254
  })
254
255
  \`\`\`
255
256
 
@@ -273,7 +274,7 @@ Share collections between agents:
273
274
  \`\`\`typescript
274
275
  delegate({
275
276
  task: "Analyze the project documentation",
276
- tools: ["rag_query"],
277
+ tools: ["rag_search"],
277
278
  context: "Use collection 'project_docs' for analysis"
278
279
  })
279
280
  \`\`\`
@@ -57,27 +57,13 @@ export const agentsMdGuidanceFragment: PromptFragment<AgentsMdGuidanceArgs> = {
57
57
  - Deeper, more specific AGENTS.md files override general root instructions.
58
58
 
59
59
  ### Writing AGENTS.md Files
60
- When working in a directory that needs specific agent guidance:
61
- 1. Create an AGENTS.md file in that directory.
62
- 2. Focus on **executable commands** (test/build) and **strict conventions**.
63
- 3. Do not duplicate generic info; focus on what is unique to this directory.
64
-
65
- ### AGENTS.md Format
66
- \`\`\`markdown
67
- # Directory Context
68
- Specific architectural details or business logic for this directory.
69
-
70
- ## Commands
71
- - Test: \`npm test path/to/dir\`
72
- - Lint: \`npm run lint:specific\`
73
-
74
- ## Conventions
75
- - Code Style: Functional patterns preferred
76
- - Naming: CamelCase for files, PascalCase for classes
77
-
78
- ## Related
79
- - [API Docs](./docs/api.md)
80
- \`\`\``);
60
+ When creating or updating an AGENTS.md file:
61
+ 1. **Maximum 50 lines.** If it's longer, you're including too much. Cut aggressively.
62
+ 2. **No code examples.** Don't include correct/incorrect patterns, usage snippets, or testing templates. Just state the rule in plain English.
63
+ 3. **No boilerplate sections.** Don't add Anti-Patterns, Testing, Dependencies, or Related sections. Only include what's genuinely unique to this directory.
64
+ 4. **Don't repeat parent info.** Import patterns, naming conventions, layer rules — if it's in a parent directory's AGENTS.md, don't restate it.
65
+ 5. **List actual files.** Name the real files that exist, not idealized directory trees. Keep it to key files only, not every file.
66
+ 6. **State rules, not ideology.** "Tools delegate to services" is a rule. A 10-line example showing correct vs incorrect delegation is ideology.`);
81
67
 
82
68
  // If root AGENTS.md content is available and short, include it
83
69
  if (rootAgentsMdContent && rootAgentsMdContent.length < MAX_ROOT_CONTENT_LENGTH_FOR_SYSTEM_PROMPT) {
@@ -37,6 +37,7 @@ import { agentDirectedMonitoringFragment } from "./28-agent-directed-monitoring"
37
37
  import { ragCollectionsFragment } from "./29-rag-collections";
38
38
  import { worktreeContextFragment } from "./30-worktree-context";
39
39
  import { agentsMdGuidanceFragment } from "./31-agents-md-guidance";
40
+ import { processMetricsFragment } from "./32-process-metrics";
40
41
  import { debugModeFragment } from "./debug-mode";
41
42
  import { delegationCompletionFragment } from "./delegation-completion";
42
43
 
@@ -85,6 +86,7 @@ export function registerAllFragments(): void {
85
86
  fragmentRegistry.register(ragCollectionsFragment);
86
87
  fragmentRegistry.register(worktreeContextFragment);
87
88
  fragmentRegistry.register(agentsMdGuidanceFragment);
89
+ fragmentRegistry.register(processMetricsFragment);
88
90
  }
89
91
 
90
92
  // Auto-register all fragments on import
@@ -250,37 +250,24 @@ async function addCoreAgentFragments(
250
250
 
251
251
  // Add RAG collection attribution - shows agents their contributions to RAG collections
252
252
  // This uses the provenance tracking metadata (agent_pubkey) from document ingestion
253
- //
254
- // OPTIMIZATION: First check if any collections exist using lightweight check
255
- // to avoid initializing embedding provider when RAG isn't used.
256
253
  try {
257
- const { hasRagCollections, RAGService } = await import("@/services/rag/RAGService");
258
-
259
- // Fast path: skip full initialization if no collections exist
260
- // Note: hasRagCollections() returns false on errors and logs them internally
261
- if (!(await hasRagCollections())) {
262
- logger.debug("📊 Skipping RAG collection stats - no collections available");
263
- } else {
264
- // Collections exist - now we need full service for stats
265
- const ragService = RAGService.getInstance();
266
- const collections = await ragService.getAllCollectionStats(agent.pubkey);
267
-
268
- // Only add the fragment if we have any collection data
269
- if (collections.length > 0) {
270
- builder.add("rag-collections", {
271
- agentPubkey: agent.pubkey,
272
- collections,
273
- });
274
- logger.debug("📊 Added RAG collection stats to system prompt", {
275
- agent: agent.name,
276
- collectionsWithContributions: collections.filter(c => c.agentDocCount > 0).length,
277
- totalCollections: collections.length,
278
- });
279
- }
254
+ const { RAGService } = await import("@/services/rag/RAGService");
255
+ const ragService = RAGService.getInstance();
256
+ const collections = await ragService.getAllCollectionStats(agent.pubkey);
257
+
258
+ if (collections.length > 0) {
259
+ builder.add("rag-collections", {
260
+ agentPubkey: agent.pubkey,
261
+ collections,
262
+ });
263
+ logger.debug("📊 Added RAG collection stats to system prompt", {
264
+ agent: agent.name,
265
+ collectionsWithContributions: collections.filter(c => c.agentDocCount > 0).length,
266
+ totalCollections: collections.length,
267
+ });
280
268
  }
281
269
  } catch (error) {
282
- // RAG service might not be available - skip gracefully
283
- logger.debug("Could not fetch RAG collection stats for prompt:", error);
270
+ logger.debug("Could not get RAG collection stats:", error);
284
271
  }
285
272
  }
286
273
 
@@ -654,6 +641,9 @@ async function buildMainSystemPrompt(options: BuildSystemPromptOptions): Promise
654
641
  // Add relay configuration context
655
642
  systemPromptBuilder.add("relay-configuration", {});
656
643
 
644
+ // Add process metrics (PID, uptime, CPU/memory usage)
645
+ systemPromptBuilder.add("process-metrics", {});
646
+
657
647
  // Add meta-project context (other projects this agent belongs to)
658
648
  // This gives agents cross-project awareness without overwhelming them
659
649
  systemPromptBuilder.add("meta-project-context", {
@@ -1,6 +1,7 @@
1
1
  import type { StoredAgent } from "@/agents/AgentStorage";
2
2
  import { agentStorage } from "@/agents/AgentStorage";
3
3
  import { NDKAgentDefinition } from "@/events/NDKAgentDefinition";
4
+ import { isValidCategory } from "@/agents/role-categories";
4
5
  import { logger } from "@/utils/logger";
5
6
  import type NDK from "@nostr-dev-kit/ndk";
6
7
  import type { NDKEvent, NDKFilter, NDKSubscription } from "@nostr-dev-kit/ndk";
@@ -545,6 +546,13 @@ export class AgentDefinitionMonitor {
545
546
  changedFields.push("role");
546
547
  }
547
548
 
549
+ const rawCategory = agentDef.category || undefined;
550
+ const newCategory = rawCategory && isValidCategory(rawCategory) ? rawCategory : undefined;
551
+ if (newCategory !== storedAgent.category) {
552
+ storedAgent.category = newCategory;
553
+ changedFields.push("category");
554
+ }
555
+
548
556
  const newDescription = agentDef.description || undefined;
549
557
  if (newDescription !== storedAgent.description) {
550
558
  storedAgent.description = newDescription;
@@ -367,6 +367,40 @@ export class ConfigService {
367
367
  return loadedConfig.llms.configurations[name];
368
368
  }
369
369
 
370
+ /**
371
+ * Get all resolved LLM configurations, skipping meta models.
372
+ * Returns the default config first, followed by the rest in declaration order.
373
+ */
374
+ getAllLLMConfigs(): LLMConfiguration[] {
375
+ if (!this.loadedConfig) {
376
+ throw new Error("Config not loaded. Call loadConfig() first.");
377
+ }
378
+
379
+ const llms = this.loadedConfig.llms;
380
+ const configs: LLMConfiguration[] = [];
381
+ const seen = new Set<string>();
382
+
383
+ // Default first
384
+ if (llms.default && llms.configurations[llms.default]) {
385
+ const config = llms.configurations[llms.default];
386
+ if (!isMetaModelConfiguration(config)) {
387
+ configs.push(config);
388
+ seen.add(`${config.provider}:${config.model}`);
389
+ }
390
+ }
391
+
392
+ // Then the rest, deduplicating by provider:model
393
+ for (const config of Object.values(llms.configurations)) {
394
+ if (isMetaModelConfiguration(config)) continue;
395
+ const key = `${config.provider}:${config.model}`;
396
+ if (seen.has(key)) continue;
397
+ seen.add(key);
398
+ configs.push(config);
399
+ }
400
+
401
+ return configs;
402
+ }
403
+
370
404
  /**
371
405
  * Check if a configuration name refers to a meta model.
372
406
  */
@@ -85,7 +85,13 @@ export class PubkeyService {
85
85
  * Uses AgentRegistry's getAgentByPubkey for efficient O(1) lookup.
86
86
  */
87
87
  private getAgentSlug(pubkey: Hexpubkey): string | undefined {
88
- const projectCtx = getProjectContext();
88
+ let projectCtx;
89
+ try {
90
+ projectCtx = getProjectContext();
91
+ } catch {
92
+ // Name resolution must remain available even outside a project-scoped context
93
+ return undefined;
94
+ }
89
95
 
90
96
  // Use direct pubkey lookup from AgentRegistry (O(1) instead of O(n))
91
97
  const agent = projectCtx.getAgentByPubkey(pubkey);
@@ -2,6 +2,7 @@ import type { LLMService } from "@/llm/service";
2
2
  import { shortenConversationId } from "@/utils/conversation-id";
3
3
  import type { ConversationStore } from "@/conversations/ConversationStore";
4
4
  import type { ConversationEntry } from "@/conversations/types";
5
+ import { renderConversationXml } from "@/conversations/formatters/utils/conversation-transcript-formatter";
5
6
  import { trace, SpanStatusCode, type Span } from "@opentelemetry/api";
6
7
  import { logger } from "@/utils/logger";
7
8
  import { config } from "@/services/ConfigService";
@@ -18,9 +19,52 @@ import {
18
19
  validateSegmentsForEntries,
19
20
  applySegmentsToEntries,
20
21
  createFallbackSegmentForEntries,
22
+ computeTokenAwareWindowSize,
21
23
  } from "./compression-utils.js";
22
24
 
23
25
  const tracer = trace.getTracer("tenex.compression");
26
+ const CONTEXT_SEGMENT_LIMIT = 3;
27
+
28
+ function buildContextPreamble(existingSegments: CompressionSegment[]): string {
29
+ if (existingSegments.length === 0) {
30
+ return "";
31
+ }
32
+
33
+ const recentSegments = existingSegments.slice(-CONTEXT_SEGMENT_LIMIT);
34
+ const contextLines = recentSegments.map(
35
+ (segment, index) => `[Previous context ${index + 1}]: ${segment.compressed}`
36
+ );
37
+
38
+ return `Previous conversation context (already compressed):\n${contextLines.join("\n")}\n\n`;
39
+ }
40
+
41
+ function buildCompressionPrompt(
42
+ transcriptXml: string,
43
+ firstShortId: string,
44
+ lastShortId: string,
45
+ existingSegments: CompressionSegment[]
46
+ ): string {
47
+ const contextPreamble = buildContextPreamble(existingSegments);
48
+
49
+ return `You are compressing conversation history represented as XML. Analyze the conversation and create 1-3 compressed segments that preserve key information while being concise.
50
+
51
+ For each segment, provide:
52
+ - fromEventId: starting message id from XML (the id attribute)
53
+ - toEventId: ending message id from XML (the id attribute)
54
+ - compressed: a concise summary (2-4 sentences) of the key points
55
+
56
+ Rules:
57
+ - Preserve attribution: who said or did what.
58
+ - Preserve recipient targeting when present.
59
+ - Preserve temporal flow using the time="+seconds" indicators and conversation t0.
60
+ - Use IDs exactly as shown in XML id attributes (do not invent IDs).
61
+ - The first segment must start at id "${firstShortId}" and the last segment must end at id "${lastShortId}".
62
+
63
+ ${contextPreamble}Messages to compress:
64
+ ${transcriptXml}
65
+
66
+ Create segments that group related topics together. Preserve important decisions, errors, and outcomes.`;
67
+ }
24
68
 
25
69
  /**
26
70
  * CompressionService - Orchestrates conversation history compression.
@@ -153,17 +197,57 @@ export class CompressionService {
153
197
  conversationId,
154
198
  entries,
155
199
  compressionConfig.slidingWindowSize,
156
- span
200
+ span,
201
+ effectiveBudget
157
202
  );
158
203
  }
159
204
  span.setStatus({ code: SpanStatusCode.OK });
160
205
  return;
161
206
  }
162
207
 
208
+ // Compute range impact before attempting LLM call
209
+ const rangeEntries = entries.slice(range.startIndex, range.endIndex);
210
+ const rangeTokens = estimateTokensFromEntries(rangeEntries);
211
+
212
+ // Skip LLM when range can't meaningfully help
213
+ if (blocking) {
214
+ const tokenOverage = currentTokens - effectiveBudget;
215
+ if (rangeTokens < tokenOverage) {
216
+ // Even compressing the entire range to zero can't close the gap
217
+ span.addEvent("compression.skip_to_fallback", {
218
+ "reason": "range_too_small_for_overage",
219
+ "range.tokens": rangeTokens,
220
+ "token.overage": tokenOverage,
221
+ });
222
+ await this.useFallback(
223
+ conversationId,
224
+ entries,
225
+ compressionConfig.slidingWindowSize,
226
+ span,
227
+ effectiveBudget
228
+ );
229
+ span.setStatus({ code: SpanStatusCode.OK });
230
+ return;
231
+ }
232
+ } else {
233
+ // Proactive mode: skip if range is tiny (not worth an LLM call)
234
+ if (rangeTokens < 500) {
235
+ span.addEvent("compression.skip_proactive", {
236
+ "reason": "range_too_small",
237
+ "range.tokens": rangeTokens,
238
+ });
239
+ span.setStatus({ code: SpanStatusCode.OK });
240
+ return;
241
+ }
242
+ }
243
+
163
244
  // Attempt LLM compression
164
245
  try {
165
- const rangeEntries = entries.slice(range.startIndex, range.endIndex);
166
- const newSegments = await this.compressEntries(rangeEntries);
246
+ const newSegments = await this.compressEntries(
247
+ conversationId,
248
+ rangeEntries,
249
+ existingSegments
250
+ );
167
251
 
168
252
  // Emit telemetry for successful summary generation
169
253
  span.addEvent("compression.summary_generated", {
@@ -195,7 +279,8 @@ export class CompressionService {
195
279
  conversationId,
196
280
  entries,
197
281
  compressionConfig.slidingWindowSize,
198
- span
282
+ span,
283
+ effectiveBudget
199
284
  );
200
285
  }
201
286
  return;
@@ -239,7 +324,8 @@ export class CompressionService {
239
324
  conversationId,
240
325
  entries,
241
326
  compressionConfig.slidingWindowSize,
242
- span
327
+ span,
328
+ effectiveBudget
243
329
  );
244
330
  } else {
245
331
  // Proactive mode: fail silently, don't throw
@@ -263,7 +349,9 @@ export class CompressionService {
263
349
  * Compress a range of entries using LLM.
264
350
  */
265
351
  private async compressEntries(
266
- entries: ConversationEntry[]
352
+ conversationId: string,
353
+ entries: ConversationEntry[],
354
+ existingSegments: CompressionSegment[]
267
355
  ): Promise<CompressionSegment[]> {
268
356
  return tracer.startActiveSpan(
269
357
  "compression.llm_compress",
@@ -271,71 +359,30 @@ export class CompressionService {
271
359
  try {
272
360
  span.setAttribute("entries.count", entries.length);
273
361
 
274
- // Format entries for LLM, including tool payloads
275
- const formattedEntries = entries
276
- .map((e) => {
277
- let formatted = `[${e.messageType}]`;
278
-
279
- // Add text content if present
280
- if (e.content) {
281
- formatted += ` ${e.content}`;
282
- }
283
-
284
- // Add tool payload summary for tool-call/tool-result entries
285
- if (e.toolData && e.toolData.length > 0) {
286
- const toolSummary = e.toolData
287
- .map((tool) => {
288
- if ('toolName' in tool) {
289
- // ToolCallPart
290
- return `Tool: ${tool.toolName}`;
291
- } else if ('toolCallId' in tool) {
292
- // ToolResultPart - cast to any to avoid type narrowing issues
293
- const toolResult = tool as any;
294
- const resultPreview = typeof toolResult.result === 'string'
295
- ? toolResult.result.substring(0, 100)
296
- : JSON.stringify(toolResult.result).substring(0, 100);
297
- return `Result: ${resultPreview}${resultPreview.length >= 100 ? '...' : ''}`;
298
- }
299
- return '';
300
- })
301
- .filter(Boolean)
302
- .join(', ');
303
-
304
- if (toolSummary) {
305
- formatted += ` [${toolSummary}]`;
306
- }
307
- }
308
-
309
- return formatted;
310
- })
311
- .join("\n\n");
312
-
313
- const eventIds = entries
314
- .filter((e) => e.eventId)
315
- .map((e) => e.eventId!);
316
-
317
- if (eventIds.length === 0) {
362
+ const {
363
+ xml: transcriptXml,
364
+ shortIdToEventId,
365
+ firstShortId,
366
+ lastShortId,
367
+ } = renderConversationXml(entries, { conversationId });
368
+
369
+ if (shortIdToEventId.size === 0 || !firstShortId || !lastShortId) {
318
370
  throw new Error("No eventIds found in entries to compress");
319
371
  }
320
372
 
373
+ const prompt = buildCompressionPrompt(
374
+ transcriptXml,
375
+ firstShortId,
376
+ lastShortId,
377
+ existingSegments
378
+ );
379
+
321
380
  // Call LLM to compress
322
381
  const result = await this.effectiveLlmService.generateObject(
323
382
  [
324
383
  {
325
384
  role: "user",
326
- content: `You are compressing conversation history. Analyze the following messages and create 1-3 compressed segments that preserve key information while being concise.
327
-
328
- For each segment, provide:
329
- - fromEventId: starting message event ID
330
- - toEventId: ending message event ID
331
- - compressed: a concise summary (2-4 sentences) of the key points
332
-
333
- Messages to compress:
334
- ${formattedEntries}
335
-
336
- Event IDs in order: ${eventIds.join(", ")}
337
-
338
- Create segments that group related topics together. Preserve important decisions, errors, and outcomes.`,
385
+ content: prompt,
339
386
  },
340
387
  ],
341
388
  CompressionSegmentsSchema
@@ -343,13 +390,17 @@ Create segments that group related topics together. Preserve important decisions
343
390
 
344
391
  // Convert LLM output to CompressionSegment format
345
392
  const segments: CompressionSegment[] = result.object.map(
346
- (seg: CompressionSegmentInput) => ({
347
- fromEventId: seg.fromEventId,
348
- toEventId: seg.toEventId,
349
- compressed: seg.compressed,
350
- createdAt: Date.now(),
351
- model: this.effectiveLlmService.model,
352
- })
393
+ (seg: CompressionSegmentInput) => {
394
+ const fromId = seg.fromEventId.trim();
395
+ const toId = seg.toEventId.trim();
396
+ return {
397
+ fromEventId: shortIdToEventId.get(fromId) ?? fromId,
398
+ toEventId: shortIdToEventId.get(toId) ?? toId,
399
+ compressed: seg.compressed,
400
+ createdAt: Date.now(),
401
+ model: this.effectiveLlmService.model,
402
+ };
403
+ }
353
404
  );
354
405
 
355
406
  span.setAttribute("segments.count", segments.length);
@@ -375,19 +426,27 @@ Create segments that group related topics together. Preserve important decisions
375
426
  conversationId: string,
376
427
  entries: ConversationEntry[],
377
428
  windowSize: number,
378
- span: Span
429
+ span: Span,
430
+ tokenBudget: number
379
431
  ): Promise<void> {
432
+ const tokenAwareWindowSize = computeTokenAwareWindowSize(entries, tokenBudget);
433
+ const effectiveWindowSize = Math.min(windowSize, tokenAwareWindowSize);
434
+
380
435
  span.setAttribute("fallback.used", true);
381
- span.setAttribute("fallback.window_size", windowSize);
436
+ span.setAttribute("fallback.configured_window", windowSize);
437
+ span.setAttribute("fallback.token_aware_window", tokenAwareWindowSize);
438
+ span.setAttribute("fallback.effective_window", effectiveWindowSize);
382
439
 
383
440
  logger.warn("Compression fallback triggered - using sliding window truncation", {
384
441
  conversationId,
385
442
  entriesCount: entries.length,
386
- windowSize,
443
+ configuredWindow: windowSize,
444
+ tokenAwareWindow: tokenAwareWindowSize,
445
+ effectiveWindow: effectiveWindowSize,
387
446
  });
388
447
 
389
448
  // Delegate to pure utility function
390
- const fallbackSegment = createFallbackSegmentForEntries(entries, windowSize);
449
+ const fallbackSegment = createFallbackSegmentForEntries(entries, effectiveWindowSize);
391
450
 
392
451
  if (!fallbackSegment) {
393
452
  // Can't create a valid segment (too few entries or insufficient event IDs)