@tenex-chat/backend 0.9.5 → 0.9.6

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 +47 -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
@@ -1,13 +1,12 @@
1
1
  import type { ToolExecutionContext } from "@/tools/types";
2
2
  import { ConversationStore } from "@/conversations/ConversationStore";
3
+ import { renderConversationXml } from "@/conversations/formatters/utils/conversation-transcript-formatter";
3
4
  import { llmServiceFactory } from "@/llm";
4
5
  import { config } from "@/services/ConfigService";
5
- import { getPubkeyService } from "@/services/PubkeyService";
6
6
  import type { AISdkTool } from "@/tools/types";
7
7
  import { logger } from "@/utils/logger";
8
- import { isHexPrefix, resolvePrefixToId, PREFIX_LENGTH } from "@/utils/nostr-entity-parser";
8
+ import { isHexPrefix, resolvePrefixToId } from "@/utils/nostr-entity-parser";
9
9
  import { tool } from "ai";
10
- import type { ToolCallPart, ToolResultPart } from "ai";
11
10
  import { z } from "zod";
12
11
  import { nip19 } from "nostr-tools";
13
12
 
@@ -74,12 +73,12 @@ const conversationGetSchema = z.object({
74
73
  .describe(
75
74
  "Optional prompt to analyze the conversation. When provided, the conversation will be processed through an LLM which will provide an explanation based on this prompt. Useful for extracting specific information or getting a summary of the conversation."
76
75
  ),
77
- includeToolResults: z
76
+ includeToolCalls: z
78
77
  .boolean()
79
78
  .optional()
80
79
  .default(false)
81
80
  .describe(
82
- "Whether to include tool result content in the response. WARNING: This can significantly increase token usage (up to 50k tokens). Tool results are truncated at 10k chars each with a 50k total budget. Only enable if you specifically need to analyze tool outputs."
81
+ "Whether to include tool-call events in the XML transcript. Tool-result entries are intentionally omitted; tool-call attributes are summarized/truncated by the shared transcript formatter."
83
82
  ),
84
83
  });
85
84
 
@@ -161,110 +160,15 @@ function safeCopy<T>(data: T): T {
161
160
  }
162
161
  }
163
162
 
164
- /**
165
- * Safely stringify a value, handling BigInt, circular refs, and other edge cases
166
- * Returns a JSON string representation or "[Unserializable]" on failure
167
- */
168
- function safeStringify(value: unknown): string {
169
- if (value === undefined) return "";
170
- if (value === null) return "null";
171
- try {
172
- return JSON.stringify(value);
173
- } catch {
174
- // Handle BigInt, circular references, or other unserializable values
175
- return '"[Unserializable]"';
176
- }
177
- }
178
-
179
- const MAX_PARAM_LENGTH = 100;
180
-
181
- /**
182
- * Format tool input parameters with per-param truncation
183
- * Each param value is truncated at MAX_PARAM_LENGTH chars
184
- * Format: param1="value1" param2="value2..." (N chars truncated)
185
- */
186
- function formatToolInput(input: unknown): string {
187
- if (input === undefined || input === null) return "";
188
-
189
- // If input is not an object, just stringify and truncate the whole thing
190
- if (typeof input !== "object" || Array.isArray(input)) {
191
- const str = safeStringify(input);
192
- if (str.length > MAX_PARAM_LENGTH) {
193
- const truncated = str.length - MAX_PARAM_LENGTH;
194
- return `${str.slice(0, MAX_PARAM_LENGTH)}... (${truncated} chars truncated)`;
195
- }
196
- return str;
197
- }
198
-
199
- // For objects, format each param with truncation
200
- const parts: string[] = [];
201
- const obj = input as Record<string, unknown>;
202
-
203
- for (const [key, value] of Object.entries(obj)) {
204
- let valueStr: string;
205
- try {
206
- valueStr = JSON.stringify(value);
207
- } catch {
208
- valueStr = '"[Unserializable]"';
209
- }
210
-
211
- if (valueStr.length > MAX_PARAM_LENGTH) {
212
- const truncated = valueStr.length - MAX_PARAM_LENGTH;
213
- parts.push(`${key}=${valueStr.slice(0, MAX_PARAM_LENGTH)}... (${truncated} chars truncated)`);
214
- } else {
215
- parts.push(`${key}=${valueStr}`);
216
- }
217
- }
218
-
219
- return parts.join(" ");
220
- }
221
-
222
- const MAX_LINE_LENGTH = 1500;
223
-
224
- /**
225
- * Format a single line with timestamp, sender, target(s), and content
226
- * Truncates the full line INCLUDING suffix to maxLength chars
227
- */
228
- function formatLine(
229
- relativeSeconds: number,
230
- from: string,
231
- targets: string[] | undefined,
232
- content: string,
233
- maxLength: number = MAX_LINE_LENGTH
234
- ): string {
235
- // Build target string: no target = "", single = "-> @to", multiple = "-> @to1, @to2"
236
- let targetStr = "";
237
- if (targets && targets.length > 0) {
238
- targetStr = ` -> ${targets.map(t => `@${t}`).join(", ")}`;
239
- }
240
-
241
- // Escape newlines to preserve single-line format
242
- const escapedContent = content.replace(/\n/g, "\\n");
243
-
244
- const line = `[+${relativeSeconds}] [@${from}${targetStr}] ${escapedContent}`;
245
-
246
- if (line.length > maxLength) {
247
- // Calculate suffix first, then determine how much content to keep
248
- // We want: kept_content + suffix <= maxLength
249
- const truncatedChars = line.length - maxLength;
250
- const suffix = `... [truncated ${truncatedChars} chars]`;
251
- const keepLength = Math.max(0, maxLength - suffix.length);
252
- return line.slice(0, keepLength) + suffix;
253
- }
254
- return line;
255
- }
256
-
257
163
  /**
258
164
  * Serialize a Conversation object to a JSON-safe plain object
259
- * Formats messages as a single multi-line string with relative timestamps
260
- * Format: [+seconds] [@from -> @to] content
165
+ * Formats messages as XML with relative timestamps and root t0.
261
166
  */
262
167
  function serializeConversation(
263
168
  conversation: ConversationStore,
264
- options: { includeToolResults?: boolean; untilId?: string } = {}
169
+ options: { includeToolCalls?: boolean; untilId?: string } = {}
265
170
  ): Record<string, unknown> {
266
171
  let messages = conversation.getAllMessages();
267
- const pubkeyService = getPubkeyService();
268
172
 
269
173
  // Filter messages up to and including untilId if provided
270
174
  if (options.untilId) {
@@ -281,174 +185,18 @@ function serializeConversation(
281
185
  }
282
186
  }
283
187
 
284
- // Find the first DEFINED timestamp to use as baseline for relative times.
285
- // This handles edge cases where early messages (e.g., tool-calls synced via
286
- // MessageSyncer) may lack timestamps. Using the first defined timestamp
287
- // ensures later messages don't show huge epoch offsets.
288
- let baselineTimestamp = 0;
289
- for (const msg of messages) {
290
- if (msg.timestamp !== undefined) {
291
- baselineTimestamp = msg.timestamp;
292
- break;
293
- }
294
- }
295
-
296
- const formattedLines: string[] = [];
297
-
298
- // Track the last known timestamp for fallback on entries without timestamps.
299
- // This provides more accurate ordering than always falling back to baseline.
300
- let lastKnownTimestamp = baselineTimestamp;
301
-
302
- for (let i = 0; i < messages.length; i++) {
303
- const entry = messages[i];
304
- // Use lastKnownTimestamp as fallback when entry.timestamp is undefined.
305
- // This ensures entries without timestamps (e.g., tool-calls synced via MessageSyncer)
306
- // appear at their approximate position rather than showing [+0] or huge negative
307
- // numbers like [+-1771103685].
308
- const effectiveTimestamp = entry.timestamp ?? lastKnownTimestamp;
309
- const relativeSeconds = Math.floor(effectiveTimestamp - baselineTimestamp);
310
-
311
- // Update lastKnownTimestamp if this entry has a defined timestamp
312
- if (entry.timestamp !== undefined) {
313
- lastKnownTimestamp = entry.timestamp;
314
- }
315
- const from = pubkeyService.getNameSync(entry.pubkey);
316
- const targets = entry.targetedPubkeys?.map(pk => pubkeyService.getNameSync(pk));
317
-
318
- if (entry.messageType === "text") {
319
- // Text messages: straightforward format
320
- formattedLines.push(formatLine(relativeSeconds, from, targets, entry.content));
321
- } else if (entry.messageType === "tool-call") {
322
- // Only include tool calls if includeToolResults is true
323
- if (!options.includeToolResults) {
324
- // Skip tool call entries when not including tool results
325
- continue;
326
- }
327
-
328
- // Tool call: look for matching tool-results by toolCallId or adjacency
329
- const toolData = (entry.toolData ?? []) as ToolCallPart[];
330
-
331
- // Check if we have toolCallIds to match with
332
- const hasToolCallIds = toolData.some(tc => tc.toolCallId);
333
-
334
- // Build a map of toolCallId -> result for matching (when IDs are present)
335
- const toolResultsMap = new Map<string, ToolResultPart>();
336
- // Also keep an ordered array for fallback adjacency matching
337
- let adjacentResults: ToolResultPart[] = [];
338
- let shouldSkipNext = false;
339
-
340
- if (i + 1 < messages.length) {
341
- const nextMsg = messages[i + 1];
342
- if (nextMsg.messageType === "tool-result" && nextMsg.pubkey === entry.pubkey) {
343
- const resultData = (nextMsg.toolData ?? []) as ToolResultPart[];
344
- adjacentResults = resultData;
345
-
346
- // Build toolCallId map for ID-based matching
347
- for (const tr of resultData) {
348
- if (tr.toolCallId) {
349
- toolResultsMap.set(tr.toolCallId, tr);
350
- }
351
- }
352
- }
353
- }
354
-
355
- // Format tool calls with their matched results
356
- const toolCallParts: string[] = [];
357
- const matchedResultIds = new Set<string>();
358
- let adjacentResultIndex = 0;
359
-
360
- for (const tc of toolData) {
361
- const toolName = tc.toolName || "unknown";
362
- const input = tc.input !== undefined ? formatToolInput(tc.input) : "";
363
- let toolCallStr = `[tool-use ${toolName} ${input}]`;
364
-
365
- let matchingResult: ToolResultPart | undefined;
366
-
367
- // Try to find matching result by toolCallId first
368
- if (tc.toolCallId && toolResultsMap.has(tc.toolCallId)) {
369
- matchingResult = toolResultsMap.get(tc.toolCallId);
370
- matchedResultIds.add(tc.toolCallId);
371
- } else if (!hasToolCallIds && adjacentResultIndex < adjacentResults.length) {
372
- // Fallback: when no toolCallIds, match by position (adjacency)
373
- matchingResult = adjacentResults[adjacentResultIndex++];
374
- }
375
-
376
- if (matchingResult) {
377
- const resultContent =
378
- matchingResult.output !== undefined
379
- ? safeStringify(matchingResult.output)
380
- : "";
381
- toolCallStr += ` [tool-result ${resultContent}]`;
382
- shouldSkipNext = true;
383
- }
384
- toolCallParts.push(toolCallStr);
385
- }
386
-
387
- // Skip the next tool-result message if we merged all results
388
- if (shouldSkipNext && i + 1 < messages.length) {
389
- const nextMsg = messages[i + 1];
390
- if (nextMsg.messageType === "tool-result" && nextMsg.pubkey === entry.pubkey) {
391
- // For ID-based matching, verify all were matched
392
- // For adjacency-based, we already processed them all
393
- if (!hasToolCallIds || adjacentResults.every(tr => !tr.toolCallId || matchedResultIds.has(tr.toolCallId))) {
394
- i++;
395
- }
396
- }
397
- }
398
-
399
- const content = toolCallParts.join(" ");
400
- formattedLines.push(formatLine(relativeSeconds, from, targets, content));
401
- } else if (entry.messageType === "tool-result") {
402
- // Standalone tool-result (not merged with tool-call)
403
- // Only show if includeToolResults is true
404
- if (options.includeToolResults) {
405
- const resultData = (entry.toolData ?? []) as ToolResultPart[];
406
- const resultParts: string[] = [];
407
- for (const tr of resultData) {
408
- const resultContent =
409
- tr.output !== undefined ? safeStringify(tr.output) : "";
410
- resultParts.push(`[tool-result ${resultContent}]`);
411
- }
412
- formattedLines.push(formatLine(relativeSeconds, from, targets, resultParts.join(" ")));
413
- }
414
- } else if (entry.messageType === "delegation-marker") {
415
- // Delegation markers: always shown (regardless of includeToolResults)
416
- const marker = entry.delegationMarker;
417
-
418
- // Validate required fields - skip gracefully if missing
419
- if (!marker?.delegationConversationId || !marker?.recipientPubkey || !marker?.status) {
420
- // Skip malformed delegation marker - don't crash, just omit from output
421
- continue;
422
- }
423
-
424
- const shortConversationId = marker.delegationConversationId.slice(0, PREFIX_LENGTH);
425
- const recipientName = pubkeyService.getNameSync(marker.recipientPubkey);
426
-
427
- // Format based on status
428
- let emoji: string;
429
- let statusText: string;
430
- if (marker.status === "pending") {
431
- emoji = "⏳";
432
- statusText = "in progress";
433
- } else if (marker.status === "completed") {
434
- emoji = "✅";
435
- statusText = "completed";
436
- } else {
437
- emoji = "⚠️";
438
- statusText = "aborted";
439
- }
440
- const content = `${emoji} Delegation ${shortConversationId} → ${recipientName} ${statusText}`;
441
-
442
- formattedLines.push(formatLine(relativeSeconds, from, targets, content));
443
- }
444
- }
188
+ const includeToolCalls = options.includeToolCalls ?? false;
189
+ const { xml } = renderConversationXml(messages, {
190
+ conversationId: String(conversation.id),
191
+ includeToolCalls,
192
+ });
445
193
 
446
194
  return {
447
195
  id: String(conversation.id),
448
196
  title: conversation.title ? String(conversation.title) : undefined,
449
197
  executionTime: safeCopy(conversation.executionTime),
450
198
  messageCount: messages.length,
451
- messages: formattedLines.join("\n"),
199
+ messages: xml,
452
200
  };
453
201
  }
454
202
 
@@ -497,11 +245,23 @@ async function executeConversationGet(
497
245
  agent: context.agent.name,
498
246
  });
499
247
 
248
+ // Allow custom runtimes/tests to resolve arbitrary conversation IDs through context.
249
+ const resolveFromContext = context.getConversation as unknown as (
250
+ conversationId?: string
251
+ ) => ConversationStore | undefined;
252
+ const contextConversationCandidate = resolveFromContext(targetConversationId);
253
+ const contextConversation =
254
+ contextConversationCandidate &&
255
+ String(contextConversationCandidate.id).toLowerCase() === targetConversationId
256
+ ? contextConversationCandidate
257
+ : undefined;
258
+
500
259
  // Get conversation from ConversationStore
501
260
  const conversation =
502
- targetConversationId === context.conversationId
261
+ contextConversation ??
262
+ (targetConversationId === context.conversationId
503
263
  ? context.getConversation()
504
- : ConversationStore.get(targetConversationId);
264
+ : ConversationStore.get(targetConversationId));
505
265
 
506
266
  if (!conversation) {
507
267
  logger.info("📭 Conversation not found", {
@@ -524,7 +284,7 @@ async function executeConversationGet(
524
284
  });
525
285
 
526
286
  const serializedConversation = serializeConversation(conversation, {
527
- includeToolResults: input.includeToolResults,
287
+ includeToolCalls: input.includeToolCalls,
528
288
  untilId: targetUntilId,
529
289
  });
530
290
 
@@ -634,7 +394,7 @@ ${conversationText}`,
634
394
  export function createConversationGetTool(context: ToolExecutionContext): AISdkTool {
635
395
  const aiTool = tool({
636
396
  description:
637
- "Retrieve a conversation by its ID, including all messages/events in the conversation history. Returns conversation info (id, title, messageCount, executionTime) and a formatted messages string. Messages are formatted as: [+seconds] [@from -> @to] content, where seconds is relative to the first message. Tool calls and results can be merged into single lines when includeToolResults is true. Useful for reviewing conversation context, analyzing message history, or debugging agent interactions.",
397
+ "Retrieve a conversation by its ID, including all messages/events in the conversation history. Returns conversation info (id, title, messageCount, executionTime) and an XML transcript string. XML includes absolute t0 on the root and per-entry relative time indicators via time=\"+seconds\", plus author/recipient attribution and short event ids.",
638
398
 
639
399
  inputSchema: conversationGetSchema,
640
400
 
@@ -421,12 +421,12 @@ async function executeGrep(
421
421
  return `${relative(workingDirectory, filePath)}:${count}`;
422
422
  }
423
423
  } else {
424
- // Content mode: /path/to/file:line:content
425
- const firstColon = line.indexOf(":");
426
- if (firstColon > 0) {
427
- const filePath = line.substring(0, firstColon);
428
- const rest = line.substring(firstColon);
429
- return `${relative(workingDirectory, filePath)}${rest}`;
424
+ // Content mode: match lines use ":" separator (file:num:content),
425
+ // context lines use "-" separator (file-num-content).
426
+ // Strip the working directory prefix to handle both uniformly.
427
+ const prefix = workingDirectory + "/";
428
+ if (line.startsWith(prefix)) {
429
+ return line.substring(prefix.length);
430
430
  }
431
431
  }
432
432
  return line;
@@ -11,6 +11,7 @@ import {
11
11
  isExpectedFsError,
12
12
  isExpectedNotFoundError,
13
13
  } from "@/tools/utils";
14
+ import { attachTranscriptArgs } from "@/tools/utils/transcript-args";
14
15
  import { logger } from "@/utils/logger";
15
16
  import { tool } from "ai";
16
17
  import { z } from "zod";
@@ -328,5 +329,6 @@ export function createFsReadTool(context: ToolExecutionContext): AISdkTool {
328
329
  configurable: true,
329
330
  });
330
331
 
332
+ attachTranscriptArgs(toolInstance as AISdkTool, [{ key: "path", attribute: "file_path" }]);
331
333
  return toolInstance as AISdkTool;
332
334
  }
@@ -9,6 +9,7 @@ import {
9
9
  getFsErrorDescription,
10
10
  isExpectedFsError,
11
11
  } from "@/tools/utils";
12
+ import { attachTranscriptArgs } from "@/tools/utils/transcript-args";
12
13
  import { tool } from "ai";
13
14
  import { z } from "zod";
14
15
 
@@ -109,5 +110,6 @@ export function createFsWriteTool(context: ToolExecutionContext): AISdkTool {
109
110
  configurable: true,
110
111
  });
111
112
 
113
+ attachTranscriptArgs(toolInstance as AISdkTool, [{ key: "path", attribute: "file_path" }]);
112
114
  return toolInstance as AISdkTool;
113
115
  }
@@ -72,58 +72,46 @@ async function executeLessonLearn(
72
72
  const lessonEvent = await context.agentPublisher.lesson(intent, eventContext);
73
73
 
74
74
  // Add lesson to RAG collection for semantic search
75
- try {
76
- const ragService = RAGService.getInstance();
77
-
78
- // Ensure the lessons collection exists
79
- try {
80
- await ragService.createCollection("lessons");
81
- } catch (error) {
82
- // Collection might already exist, which is fine
83
- logger.debug("Lessons collection might already exist", { error });
84
- }
85
-
86
- // Add the lesson to the RAG collection
87
- const lessonContent = detailed || lesson;
88
-
89
- // Get projectId for project-scoped search isolation
90
- let projectId: string | undefined;
91
- if (isProjectContextInitialized()) {
92
- try {
93
- projectId = getProjectContext().project.tagId();
94
- } catch {
95
- // Project context not available - lesson will lack project scoping
96
- }
97
- }
98
-
99
- await ragService.addDocuments("lessons", [
100
- {
101
- id: lessonEvent.encode(),
102
- content: lessonContent,
103
- metadata: {
104
- title,
105
- category,
106
- hashtags: hashtags.length > 0 ? hashtags : undefined,
107
- agentPubkey: context.agent.pubkey,
108
- agentName: context.agent.name,
109
- timestamp: Date.now(),
110
- hasDetailed: !!detailed,
111
- type: "lesson",
112
- ...(projectId && { projectId }),
113
- },
114
- },
115
- ]);
116
-
117
- logger.info("Lesson added to RAG collection", {
118
- title,
119
- eventId: lessonEvent.encode(),
120
- agentName: context.agent.name,
121
- });
122
- } catch (error) {
123
- // Don't fail the tool if RAG integration fails
124
- logger.warn("Failed to add lesson to RAG collection", { error, title });
75
+ const ragService = RAGService.getInstance();
76
+
77
+ // Ensure the lessons collection exists
78
+ const collections = await ragService.listCollections();
79
+ if (!collections.includes("lessons")) {
80
+ await ragService.createCollection("lessons");
81
+ }
82
+
83
+ const lessonContent = detailed || lesson;
84
+
85
+ // Get projectId for project-scoped search isolation
86
+ let projectId: string | undefined;
87
+ if (isProjectContextInitialized()) {
88
+ projectId = getProjectContext().project.tagId();
125
89
  }
126
90
 
91
+ await ragService.addDocuments("lessons", [
92
+ {
93
+ id: lessonEvent.encode(),
94
+ content: lessonContent,
95
+ metadata: {
96
+ title,
97
+ category,
98
+ hashtags: hashtags.length > 0 ? hashtags : undefined,
99
+ agentPubkey: context.agent.pubkey,
100
+ agentName: context.agent.name,
101
+ timestamp: Date.now(),
102
+ hasDetailed: !!detailed,
103
+ type: "lesson",
104
+ ...(projectId && { projectId }),
105
+ },
106
+ },
107
+ ]);
108
+
109
+ logger.info("Lesson added to RAG collection", {
110
+ title,
111
+ eventId: lessonEvent.encode(),
112
+ agentName: context.agent.name,
113
+ });
114
+
127
115
  const message = `Lesson recorded: "${title}"${detailed ? " (with detailed version)" : ""}\n\nThis lesson will be available in future conversations to help avoid similar issues.`;
128
116
 
129
117
  return {
@@ -388,16 +388,18 @@ function computeBaseProvenance(context: ToolExecutionContext): DocumentMetadata
388
388
  provenance.agent_pubkey = context.agent.pubkey;
389
389
  }
390
390
 
391
- // Auto-inject project_id if available (uses NIP-33 address format)
391
+ // Auto-inject projectId if available (uses NIP-33 address format)
392
+ // NOTE: Must use camelCase "projectId" to match buildProjectFilter() and
393
+ // all specialized services (ReportEmbeddingService, ConversationEmbeddingService, learn.ts)
392
394
  if (isProjectContextInitialized()) {
393
395
  try {
394
396
  const projectCtx = getProjectContext();
395
397
  const projectId = projectCtx.project.tagId();
396
398
  if (projectId) {
397
- provenance.project_id = projectId;
399
+ provenance.projectId = projectId;
398
400
  }
399
401
  } catch {
400
- // Project context not available - skip project_id injection
402
+ // Project context not available - skip projectId injection
401
403
  }
402
404
  }
403
405
 
@@ -455,7 +457,7 @@ function mergeWithProvenance(
455
457
  // Coerce user-provided metadata to ensure JSON-serializable values
456
458
  const coercedMetadata = documentMetadata ? coerceToDocumentMetadata(documentMetadata) : {};
457
459
 
458
- // Base provenance (agent_pubkey, project_id) comes first, user metadata can override
460
+ // Base provenance (agent_pubkey, projectId) comes first, user metadata can override
459
461
  return {
460
462
  ...baseProvenance,
461
463
  ...coercedMetadata,
@@ -1,5 +1,7 @@
1
1
  import type { ToolExecutionContext } from "@/tools/types";
2
2
  import { RAGService } from "@/services/rag/RAGService";
3
+ import { RAGCollectionRegistry } from "@/services/rag/RAGCollectionRegistry";
4
+ import { getProjectContext, isProjectContextInitialized } from "@/services/projects";
3
5
  import type { AISdkTool } from "@/tools/types";
4
6
  import { type ToolResponse, executeToolWithErrorHandling } from "@/tools/utils";
5
7
  import { tool } from "ai";
@@ -20,6 +22,17 @@ const ragCreateCollectionSchema = z.object({
20
22
  .describe(
21
23
  "Optional custom schema for the collection (default includes id, content, vector, metadata, timestamp, source)"
22
24
  ),
25
+ scope: z
26
+ .enum(["global", "project", "personal"])
27
+ .optional()
28
+ .default("project")
29
+ .describe(
30
+ "Visibility scope for rag_search() auto-discovery. " +
31
+ "'global' = visible to all agents in all projects. " +
32
+ "'project' = visible to agents in this project (default). " +
33
+ "'personal' = primarily relevant to the creating agent. " +
34
+ "Note: This is NOT access control — agents can always query any collection explicitly."
35
+ ),
23
36
  });
24
37
 
25
38
  /**
@@ -27,13 +40,30 @@ const ragCreateCollectionSchema = z.object({
27
40
  */
28
41
  async function executeCreateCollection(
29
42
  input: z.infer<typeof ragCreateCollectionSchema>,
30
- _context: ToolExecutionContext
43
+ context: ToolExecutionContext
31
44
  ): Promise<ToolResponse> {
32
- const { name, schema } = input;
45
+ const { name, schema, scope } = input;
33
46
 
34
47
  const ragService = RAGService.getInstance();
35
48
  const collection = await ragService.createCollection(name, schema ?? undefined);
36
49
 
50
+ // Register in the collection registry with scope metadata
51
+ const registry = RAGCollectionRegistry.getInstance();
52
+ let projectId: string | undefined;
53
+ if (isProjectContextInitialized()) {
54
+ try {
55
+ projectId = getProjectContext().project.tagId();
56
+ } catch {
57
+ // Project context not available — no projectId
58
+ }
59
+ }
60
+
61
+ registry.register(name, {
62
+ scope,
63
+ projectId,
64
+ agentPubkey: context.agent.pubkey,
65
+ });
66
+
37
67
  return {
38
68
  success: true,
39
69
  message: `Collection '${name}' created successfully`,
@@ -41,6 +71,7 @@ async function executeCreateCollection(
41
71
  name: collection.name,
42
72
  created_at: new Date(collection.created_at).toISOString(),
43
73
  schema: collection.schema,
74
+ scope,
44
75
  },
45
76
  };
46
77
  }
@@ -51,7 +82,9 @@ async function executeCreateCollection(
51
82
  export function createRAGCreateCollectionTool(context: ToolExecutionContext): AISdkTool {
52
83
  return tool({
53
84
  description:
54
- "Create a new RAG collection (vector database) for storing documents with semantic search capabilities",
85
+ "Create a new RAG collection (vector database) for storing documents with semantic search capabilities. " +
86
+ "Set scope to control default visibility in rag_search(): 'global' (all projects), " +
87
+ "'project' (current project, default), or 'personal' (creating agent only).",
55
88
  inputSchema: ragCreateCollectionSchema,
56
89
  execute: async (input: unknown) => {
57
90
  return executeToolWithErrorHandling(
@@ -62,4 +95,4 @@ export function createRAGCreateCollectionTool(context: ToolExecutionContext): AI
62
95
  );
63
96
  },
64
97
  }) as AISdkTool;
65
- }
98
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ToolExecutionContext } from "@/tools/types";
2
2
  import { RAGService } from "@/services/rag/RAGService";
3
+ import { RAGCollectionRegistry } from "@/services/rag/RAGCollectionRegistry";
3
4
  import type { AISdkTool } from "@/tools/types";
4
5
  import { type ToolResponse, executeToolWithErrorHandling } from "@/tools/utils";
5
6
  import { tool } from "ai";
@@ -41,6 +42,14 @@ async function executeDeleteCollection(
41
42
  const ragService = RAGService.getInstance();
42
43
  await ragService.deleteCollection(name);
43
44
 
45
+ // Clean up registry entry
46
+ try {
47
+ const registry = RAGCollectionRegistry.getInstance();
48
+ registry.unregister(name);
49
+ } catch {
50
+ // Registry not available — non-critical, skip
51
+ }
52
+
44
53
  return {
45
54
  success: true,
46
55
  message: `Collection '${name}' has been permanently deleted`,