@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.
- package/README.md +5 -1
- package/dist/daemon-wrapper.cjs +47 -0
- package/dist/index.js +59268 -0
- package/dist/wrapper.js +171 -0
- package/package.json +19 -27
- package/src/agents/AgentRegistry.ts +9 -7
- package/src/agents/AgentStorage.ts +24 -1
- package/src/agents/agent-installer.ts +6 -0
- package/src/agents/agent-loader.ts +7 -2
- package/src/agents/constants.ts +10 -2
- package/src/agents/execution/AgentExecutor.ts +35 -6
- package/src/agents/execution/StreamCallbacks.ts +53 -13
- package/src/agents/execution/StreamExecutionHandler.ts +110 -16
- package/src/agents/execution/StreamSetup.ts +19 -9
- package/src/agents/execution/ToolEventHandlers.ts +112 -0
- package/src/agents/role-categories.ts +53 -0
- package/src/agents/types/runtime.ts +7 -0
- package/src/agents/types/storage.ts +7 -0
- package/src/commands/agent/import/openclaw-distiller.ts +63 -7
- package/src/commands/agent/import/openclaw-reader.ts +54 -0
- package/src/commands/agent/import/openclaw.ts +120 -29
- package/src/commands/agent/index.ts +83 -2
- package/src/commands/setup/display.ts +123 -0
- package/src/commands/setup/embed.ts +13 -13
- package/src/commands/setup/global-system-prompt.ts +15 -17
- package/src/commands/setup/image.ts +17 -20
- package/src/commands/setup/interactive.ts +37 -20
- package/src/commands/setup/llm.ts +12 -7
- package/src/commands/setup/onboarding.ts +1580 -248
- package/src/commands/setup/providers.ts +3 -3
- package/src/conversations/ConversationStore.ts +23 -2
- package/src/conversations/MessageBuilder.ts +51 -73
- package/src/conversations/formatters/utils/conversation-transcript-formatter.ts +425 -0
- package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +40 -98
- package/src/conversations/search/embeddings/ConversationIndexingJob.ts +40 -52
- package/src/conversations/services/ConversationSummarizer.ts +1 -2
- package/src/conversations/types.ts +11 -0
- package/src/daemon/Daemon.ts +78 -57
- package/src/daemon/ProjectRuntime.ts +6 -12
- package/src/daemon/SubscriptionManager.ts +13 -0
- package/src/daemon/index.ts +0 -1
- package/src/event-handler/index.ts +1 -0
- package/src/index.ts +20 -1
- package/src/llm/ChunkHandler.ts +1 -1
- package/src/llm/FinishHandler.ts +28 -4
- package/src/llm/LLMConfigEditor.ts +218 -106
- package/src/llm/index.ts +0 -4
- package/src/llm/meta/MetaModelResolver.ts +3 -18
- package/src/llm/middleware/message-sanitizer.ts +153 -0
- package/src/llm/providers/ollama-models.ts +0 -38
- package/src/llm/service.ts +50 -15
- package/src/llm/types.ts +0 -12
- package/src/llm/utils/ConfigurationManager.ts +88 -465
- package/src/llm/utils/ConfigurationTester.ts +42 -185
- package/src/llm/utils/ModelSelector.ts +156 -92
- package/src/llm/utils/ProviderConfigUI.ts +10 -141
- package/src/llm/utils/models-dev-cache.ts +102 -23
- package/src/llm/utils/provider-select-prompt.ts +284 -0
- package/src/llm/utils/provider-setup.ts +81 -34
- package/src/llm/utils/variant-list-prompt.ts +361 -0
- package/src/nostr/AgentEventDecoder.ts +1 -0
- package/src/nostr/AgentEventEncoder.ts +37 -0
- package/src/nostr/AgentProfilePublisher.ts +13 -0
- package/src/nostr/AgentPublisher.ts +26 -0
- package/src/nostr/kinds.ts +1 -0
- package/src/nostr/ndkClient.ts +4 -1
- package/src/nostr/types.ts +12 -0
- package/src/prompts/fragments/25-rag-instructions.ts +22 -21
- package/src/prompts/fragments/31-agents-md-guidance.ts +7 -21
- package/src/prompts/fragments/index.ts +2 -0
- package/src/prompts/utils/systemPromptBuilder.ts +18 -28
- package/src/services/AgentDefinitionMonitor.ts +8 -0
- package/src/services/ConfigService.ts +34 -0
- package/src/services/PubkeyService.ts +7 -1
- package/src/services/compression/CompressionService.ts +133 -74
- package/src/services/compression/compression-utils.ts +110 -19
- package/src/services/config/types.ts +0 -6
- package/src/services/dispatch/AgentDispatchService.ts +79 -0
- package/src/services/intervention/InterventionService.ts +78 -5
- package/src/services/nip46/Nip46SigningService.ts +30 -1
- package/src/services/projects/ProjectContext.ts +8 -6
- package/src/services/rag/RAGCollectionRegistry.ts +199 -0
- package/src/services/rag/RAGDatabaseService.ts +2 -7
- package/src/services/rag/RAGOperations.ts +25 -45
- package/src/services/rag/RAGService.ts +0 -31
- package/src/services/rag/RagSubscriptionService.ts +71 -122
- package/src/services/rag/rag-utils.ts +13 -0
- package/src/services/ral/RALRegistry.ts +25 -184
- package/src/services/reports/ReportEmbeddingService.ts +63 -113
- package/src/services/search/UnifiedSearchService.ts +115 -4
- package/src/services/search/index.ts +1 -0
- package/src/services/search/projectFilter.ts +20 -4
- package/src/services/search/providers/ConversationSearchProvider.ts +1 -0
- package/src/services/search/providers/GenericCollectionSearchProvider.ts +81 -0
- package/src/services/search/providers/LessonSearchProvider.ts +1 -8
- package/src/services/search/providers/ReportSearchProvider.ts +1 -0
- package/src/services/search/types.ts +24 -3
- package/src/services/trust-pubkeys/SystemPubkeyListService.ts +148 -0
- package/src/services/trust-pubkeys/TrustPubkeyService.ts +70 -9
- package/src/telemetry/setup.ts +2 -13
- package/src/tools/implementations/ask.ts +3 -3
- package/src/tools/implementations/conversation_get.ts +28 -268
- package/src/tools/implementations/fs_grep.ts +6 -6
- package/src/tools/implementations/fs_read.ts +2 -0
- package/src/tools/implementations/fs_write.ts +2 -0
- package/src/tools/implementations/learn.ts +38 -50
- package/src/tools/implementations/rag_add_documents.ts +6 -4
- package/src/tools/implementations/rag_create_collection.ts +37 -4
- package/src/tools/implementations/rag_delete_collection.ts +9 -0
- package/src/tools/implementations/{search.ts → rag_search.ts} +31 -25
- package/src/tools/registry.ts +7 -8
- package/src/tools/types.ts +11 -2
- package/src/tools/utils/transcript-args.ts +13 -0
- package/src/utils/cli-theme.ts +13 -0
- package/src/utils/logger.ts +55 -0
- package/src/utils/metadataKeys.ts +17 -0
- package/src/utils/sqlEscaping.ts +39 -0
- package/src/wrapper.ts +7 -3
- package/dist/src/index.js +0 -46790
- package/dist/tenex-backend-wrapper.cjs +0 -3
- package/src/agents/execution/constants.ts +0 -16
- package/src/agents/execution/index.ts +0 -3
- package/src/agents/index.ts +0 -4
- package/src/commands/agent.ts +0 -235
- package/src/conversations/formatters/DelegationXmlFormatter.ts +0 -64
- package/src/conversations/formatters/index.ts +0 -9
- package/src/conversations/index.ts +0 -2
- package/src/conversations/utils/content-utils.ts +0 -69
- package/src/daemon/UnixSocketTransport.ts +0 -318
- package/src/event-handler/newConversation.ts +0 -165
- package/src/events/NDKProjectStatus.ts +0 -384
- package/src/events/index.ts +0 -4
- package/src/lib/json-parser.ts +0 -30
- package/src/llm/RecordingState.ts +0 -37
- package/src/llm/StreamPublisher.ts +0 -40
- package/src/llm/middleware/flight-recorder.ts +0 -188
- package/src/llm/utils/claudeCodePromptCompiler.ts +0 -141
- package/src/nostr/constants.ts +0 -38
- package/src/prompts/core/index.ts +0 -3
- package/src/prompts/index.ts +0 -21
- package/src/services/image/index.ts +0 -12
- package/src/services/status/index.ts +0 -11
- package/src/telemetry/diagnostics.ts +0 -27
- package/src/tools/implementations/rag_query.ts +0 -107
- package/src/types/index.ts +0 -46
- package/src/utils/agentFetcher.ts +0 -107
- package/src/utils/conversation-utils.ts +0 -1
- 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
|
|
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
|
-
|
|
76
|
+
includeToolCalls: z
|
|
78
77
|
.boolean()
|
|
79
78
|
.optional()
|
|
80
79
|
.default(false)
|
|
81
80
|
.describe(
|
|
82
|
-
"Whether to include tool
|
|
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
|
|
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: {
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
return
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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.
|
|
399
|
+
provenance.projectId = projectId;
|
|
398
400
|
}
|
|
399
401
|
} catch {
|
|
400
|
-
// Project context not available - skip
|
|
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,
|
|
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
|
-
|
|
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`,
|