@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.
- package/README.md +5 -1
- package/dist/daemon-wrapper.cjs +34 -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,7 +1,7 @@
|
|
|
1
1
|
import * as fileSystem from "@/lib/fs";
|
|
2
2
|
import { runProviderSetup } from "@/llm/utils/provider-setup";
|
|
3
3
|
import { config } from "@/services/ConfigService";
|
|
4
|
-
import
|
|
4
|
+
import chalk from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
export const providersCommand = new Command("providers")
|
|
@@ -15,13 +15,13 @@ export const providersCommand = new Command("providers")
|
|
|
15
15
|
const updatedProviders = await runProviderSetup(existingProviders);
|
|
16
16
|
|
|
17
17
|
await config.saveGlobalProviders(updatedProviders);
|
|
18
|
-
|
|
18
|
+
console.log(chalk.green("✓") + chalk.bold(` Provider credentials saved to ${globalPath}/providers.json`));
|
|
19
19
|
} catch (error: unknown) {
|
|
20
20
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
21
21
|
if (errorMessage?.includes("SIGINT") || errorMessage?.includes("force closed")) {
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
console.log(chalk.red(`❌ Failed to configure providers: ${error}`));
|
|
25
25
|
process.exitCode = 1;
|
|
26
26
|
}
|
|
27
27
|
});
|
|
@@ -409,6 +409,16 @@ export class ConversationStore {
|
|
|
409
409
|
return index;
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
relocateToEnd(eventId: string, updates: Partial<ConversationEntry>): boolean {
|
|
413
|
+
const index = this.state.messages.findIndex(m => m.eventId === eventId);
|
|
414
|
+
if (index === -1) return false;
|
|
415
|
+
|
|
416
|
+
const [entry] = this.state.messages.splice(index, 1);
|
|
417
|
+
Object.assign(entry, updates);
|
|
418
|
+
this.state.messages.push(entry);
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
|
|
412
422
|
getAllMessages(): ConversationEntry[] {
|
|
413
423
|
return this.state.messages;
|
|
414
424
|
}
|
|
@@ -547,8 +557,19 @@ export class ConversationStore {
|
|
|
547
557
|
if (updates.abortReason) {
|
|
548
558
|
dm.abortReason = updates.abortReason;
|
|
549
559
|
}
|
|
550
|
-
|
|
551
|
-
//
|
|
560
|
+
|
|
561
|
+
// Move the completed marker to the end of the messages array.
|
|
562
|
+
// The pending marker was inserted during the delegate tool execution,
|
|
563
|
+
// which places it BEFORE the agent's post-delegation response text.
|
|
564
|
+
// When the marker is expanded during message building, it becomes a
|
|
565
|
+
// user message — if it stays at its original position, the conversation
|
|
566
|
+
// ends with the agent's trailing assistant message and Anthropic rejects
|
|
567
|
+
// it with "conversation must end with a user message".
|
|
568
|
+
const markerIndex = this.state.messages.indexOf(markerEntry);
|
|
569
|
+
if (markerIndex >= 0 && markerIndex < this.state.messages.length - 1) {
|
|
570
|
+
this.state.messages.splice(markerIndex, 1);
|
|
571
|
+
this.state.messages.push(markerEntry);
|
|
572
|
+
}
|
|
552
573
|
|
|
553
574
|
return true;
|
|
554
575
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { ModelMessage, ToolCallPart, ToolResultPart } from "ai";
|
|
14
14
|
import { trace } from "@opentelemetry/api";
|
|
15
15
|
import type { ConversationEntry, DelegationMarker } from "./types";
|
|
16
|
+
import { renderConversationXml } from "@/conversations/formatters/utils/conversation-transcript-formatter";
|
|
16
17
|
import { getPubkeyService } from "@/services/PubkeyService";
|
|
17
18
|
import { convertToMultimodalContent, hasImageUrls } from "./utils/multimodal-content";
|
|
18
19
|
import { processToolResult, shouldTruncateToolResult, type TruncationContext } from "./utils/tool-result-truncator";
|
|
@@ -106,9 +107,8 @@ interface AgentsMdContext {
|
|
|
106
107
|
* Rules:
|
|
107
108
|
* - Explicit role override: If entry.role is set (for synthetic entries like compressed summaries), use it
|
|
108
109
|
* - assistant: Only for the viewing agent's own messages
|
|
109
|
-
* - user: All other messages (regardless of targeting)
|
|
110
|
+
* - user: All other messages (regardless of targeting), including compressed summaries
|
|
110
111
|
* - tool: Tool results (fixed)
|
|
111
|
-
* - system: Synthetic system messages (compressed summaries, etc.)
|
|
112
112
|
*
|
|
113
113
|
* Note: Attribution context is not added to LLM input. Role simply distinguishes
|
|
114
114
|
* between the agent's own messages and messages from others.
|
|
@@ -136,6 +136,19 @@ function deriveRole(
|
|
|
136
136
|
return "user"; // All non-self messages
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Resolve a display name through PubkeyService, falling back to its sync path.
|
|
141
|
+
* This keeps all name formatting centralized in PubkeyService.
|
|
142
|
+
*/
|
|
143
|
+
async function resolveDisplayName(pubkey: string): Promise<string> {
|
|
144
|
+
const pubkeyService = getPubkeyService();
|
|
145
|
+
try {
|
|
146
|
+
return await pubkeyService.getName(pubkey);
|
|
147
|
+
} catch {
|
|
148
|
+
return pubkeyService.getNameSync(pubkey);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
139
152
|
/**
|
|
140
153
|
* Compute an attribution prefix for a conversation entry.
|
|
141
154
|
*
|
|
@@ -166,14 +179,7 @@ export function computeAttributionPrefix(
|
|
|
166
179
|
agentPubkeys: Set<string>,
|
|
167
180
|
resolveDisplayName?: (pubkey: string) => string
|
|
168
181
|
): string {
|
|
169
|
-
const resolve = resolveDisplayName ?? ((pk: string) =>
|
|
170
|
-
try {
|
|
171
|
-
const name = getPubkeyService().getNameSync(pk);
|
|
172
|
-
return name || pk.substring(0, 8);
|
|
173
|
-
} catch {
|
|
174
|
-
return pk.substring(0, 8);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
182
|
+
const resolve = resolveDisplayName ?? ((pk: string) => getPubkeyService().getNameSync(pk));
|
|
177
183
|
|
|
178
184
|
// Determine the actual sender (injected messages track original sender via senderPubkey)
|
|
179
185
|
const senderPubkey = entry.senderPubkey ?? entry.pubkey;
|
|
@@ -357,41 +363,25 @@ async function expandDelegationMarker(
|
|
|
357
363
|
marker: DelegationMarker,
|
|
358
364
|
delegationMessages: ConversationEntry[] | undefined
|
|
359
365
|
): Promise<ModelMessage> {
|
|
360
|
-
const pubkeyService = getPubkeyService();
|
|
361
|
-
|
|
362
366
|
// Handle pending delegations - show that work is in progress
|
|
363
367
|
if (marker.status === "pending") {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
};
|
|
370
|
-
} catch {
|
|
371
|
-
return {
|
|
372
|
-
role: "user",
|
|
373
|
-
content: `# DELEGATION IN PROGRESS\n\nAgent ${marker.recipientPubkey.substring(0, 12)} is currently working on this task.`,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
368
|
+
const recipientName = await resolveDisplayName(marker.recipientPubkey);
|
|
369
|
+
return {
|
|
370
|
+
role: "user",
|
|
371
|
+
content: `# DELEGATION IN PROGRESS\n\n@${recipientName} is currently working on this task.`,
|
|
372
|
+
};
|
|
376
373
|
}
|
|
377
374
|
|
|
378
375
|
if (!delegationMessages) {
|
|
379
376
|
// Delegation conversation not found - return placeholder
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
};
|
|
389
|
-
} catch {
|
|
390
|
-
return {
|
|
391
|
-
role: "user",
|
|
392
|
-
content: `# DELEGATION ${marker.status.toUpperCase()}\n\nAgent ${marker.recipientPubkey.substring(0, 12)} ${marker.status === "aborted" ? "was aborted" : "completed"} (transcript unavailable)`,
|
|
393
|
-
};
|
|
394
|
-
}
|
|
377
|
+
const recipientName = await resolveDisplayName(marker.recipientPubkey);
|
|
378
|
+
const statusText = marker.status === "aborted"
|
|
379
|
+
? `was aborted: ${marker.abortReason || "unknown reason"}`
|
|
380
|
+
: "completed (transcript unavailable)";
|
|
381
|
+
return {
|
|
382
|
+
role: "user",
|
|
383
|
+
content: `# DELEGATION ${marker.status.toUpperCase()}\n\n@${recipientName} ${statusText}`,
|
|
384
|
+
};
|
|
395
385
|
}
|
|
396
386
|
|
|
397
387
|
// Build flat transcript from delegation conversation
|
|
@@ -410,32 +400,15 @@ async function expandDelegationMarker(
|
|
|
410
400
|
lines.push("");
|
|
411
401
|
}
|
|
412
402
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
);
|
|
403
|
+
const { xml: transcriptXml } = renderConversationXml(delegationMessages, {
|
|
404
|
+
conversationId: marker.delegationConversationId,
|
|
405
|
+
includeMessageTypes: ["text"],
|
|
406
|
+
requireTargetedPubkeys: true,
|
|
407
|
+
includeToolCalls: false,
|
|
408
|
+
});
|
|
419
409
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
} else {
|
|
423
|
-
lines.push("### Transcript:");
|
|
424
|
-
for (const msg of transcriptMessages) {
|
|
425
|
-
try {
|
|
426
|
-
const senderName = await pubkeyService.getName(msg.pubkey);
|
|
427
|
-
const recipientName = msg.targetedPubkeys?.[0]
|
|
428
|
-
? await pubkeyService.getName(msg.targetedPubkeys[0])
|
|
429
|
-
: "unknown";
|
|
430
|
-
lines.push(`[@${senderName} -> @${recipientName}]: ${msg.content}`);
|
|
431
|
-
} catch {
|
|
432
|
-
// Fallback to shortened pubkeys
|
|
433
|
-
const senderFallback = msg.pubkey.substring(0, 12);
|
|
434
|
-
const recipientFallback = msg.targetedPubkeys?.[0]?.substring(0, 12) || "unknown";
|
|
435
|
-
lines.push(`[@${senderFallback} -> @${recipientFallback}]: ${msg.content}`);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
410
|
+
lines.push("### Transcript:");
|
|
411
|
+
lines.push(transcriptXml);
|
|
439
412
|
|
|
440
413
|
return {
|
|
441
414
|
role: "user",
|
|
@@ -462,14 +435,7 @@ async function expandDelegationMarker(
|
|
|
462
435
|
async function formatNestedDelegationMarker(
|
|
463
436
|
marker: DelegationMarker
|
|
464
437
|
): Promise<ModelMessage> {
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
let recipientName: string;
|
|
468
|
-
try {
|
|
469
|
-
recipientName = await pubkeyService.getName(marker.recipientPubkey);
|
|
470
|
-
} catch {
|
|
471
|
-
recipientName = marker.recipientPubkey.substring(0, 12);
|
|
472
|
-
}
|
|
438
|
+
const recipientName = await resolveDisplayName(marker.recipientPubkey);
|
|
473
439
|
|
|
474
440
|
const shortConversationId = marker.delegationConversationId.substring(0, 12);
|
|
475
441
|
|
|
@@ -668,9 +634,21 @@ export async function buildMessagesFromEntries(
|
|
|
668
634
|
|
|
669
635
|
// TOOL-RESULT: Add to result and mark tool-call as resolved
|
|
670
636
|
if (entry.messageType === "tool-result" && entry.toolData) {
|
|
637
|
+
const parts = entry.toolData as ToolResultPart[];
|
|
638
|
+
|
|
639
|
+
// Skip orphaned tool-results whose tool-calls were removed by compression/truncation.
|
|
640
|
+
// If the corresponding tool-call is not in pendingToolCalls, sending this result
|
|
641
|
+
// to the LLM would produce an API error (tool_result without matching tool_use).
|
|
642
|
+
if (parts.some((part) => !pendingToolCalls.has(part.toolCallId))) {
|
|
643
|
+
trace.getActiveSpan?.()?.addEvent("conversation.orphaned_tool_result_skipped", {
|
|
644
|
+
"orphan.tool_call_ids": parts.map((p) => p.toolCallId).join(","),
|
|
645
|
+
});
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
|
|
671
649
|
result.push(await entryToMessage(entry, viewingAgentPubkey, truncationContext, agentPubkeys, imageTracker, agentsMdContext));
|
|
672
650
|
|
|
673
|
-
for (const part of
|
|
651
|
+
for (const part of parts) {
|
|
674
652
|
pendingToolCalls.delete(part.toolCallId);
|
|
675
653
|
}
|
|
676
654
|
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import type { ConversationEntry } from "@/conversations/types";
|
|
2
|
+
import { getPubkeyService } from "@/services/PubkeyService";
|
|
3
|
+
import { PREFIX_LENGTH } from "@/utils/nostr-entity-parser";
|
|
4
|
+
import type { ToolCallPart } from "ai";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SHORT_ID_LENGTH = 12;
|
|
7
|
+
const DEFAULT_MAX_TOOL_DESCRIPTION_LENGTH = 150;
|
|
8
|
+
const DEFAULT_MAX_TOOL_INPUT_JSON_LENGTH = 200;
|
|
9
|
+
|
|
10
|
+
const FULL_HEX_EVENT_ID_REGEX = /^[0-9a-f]{64}$/i;
|
|
11
|
+
|
|
12
|
+
export interface ConversationTimelineEntry {
|
|
13
|
+
entry: ConversationEntry;
|
|
14
|
+
relativeSeconds: number;
|
|
15
|
+
author: string;
|
|
16
|
+
recipients: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ConversationTimeline {
|
|
20
|
+
t0: number;
|
|
21
|
+
entries: ConversationTimelineEntry[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ConversationTimelineOptions {
|
|
25
|
+
includeMessageTypes?: Array<ConversationEntry["messageType"]>;
|
|
26
|
+
requireTargetedPubkeys?: boolean;
|
|
27
|
+
includeToolCalls?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ConversationXmlRenderOptions {
|
|
31
|
+
conversationId?: string;
|
|
32
|
+
includeMessageTypes?: Array<ConversationEntry["messageType"]>;
|
|
33
|
+
requireTargetedPubkeys?: boolean;
|
|
34
|
+
includeToolCalls?: boolean;
|
|
35
|
+
shortIdLength?: number;
|
|
36
|
+
maxToolDescriptionLength?: number;
|
|
37
|
+
maxToolInputJsonLength?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ConversationXmlRenderResult {
|
|
41
|
+
xml: string;
|
|
42
|
+
shortIdToEventId: Map<string, string>;
|
|
43
|
+
firstShortId: string | null;
|
|
44
|
+
lastShortId: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function escapeXml(value: string): string {
|
|
48
|
+
return value
|
|
49
|
+
.replaceAll("&", "&")
|
|
50
|
+
.replaceAll("<", "<")
|
|
51
|
+
.replaceAll(">", ">")
|
|
52
|
+
.replaceAll('"', """)
|
|
53
|
+
.replaceAll("'", "'");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function computeBaselineTimestamp(entries: ConversationEntry[]): number {
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (entry.timestamp !== undefined) {
|
|
59
|
+
return entry.timestamp;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function shouldIncludeEntry(
|
|
66
|
+
entry: ConversationEntry,
|
|
67
|
+
options: ConversationTimelineOptions
|
|
68
|
+
): boolean {
|
|
69
|
+
const includeToolCalls = options.includeToolCalls ?? true;
|
|
70
|
+
|
|
71
|
+
if (entry.messageType === "tool-result") {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (entry.messageType === "tool-call" && !includeToolCalls) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options.includeMessageTypes && !options.includeMessageTypes.includes(entry.messageType)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (options.requireTargetedPubkeys && (!entry.targetedPubkeys || entry.targetedPubkeys.length === 0)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getShortEventId(
|
|
91
|
+
eventId: string,
|
|
92
|
+
shortIdLength: number,
|
|
93
|
+
usedShortIds: Set<string>
|
|
94
|
+
): string {
|
|
95
|
+
const base = eventId.substring(0, shortIdLength) || "event";
|
|
96
|
+
let candidate = base;
|
|
97
|
+
let suffix = 2;
|
|
98
|
+
|
|
99
|
+
while (usedShortIds.has(candidate)) {
|
|
100
|
+
candidate = `${base}-${suffix}`;
|
|
101
|
+
suffix++;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
usedShortIds.add(candidate);
|
|
105
|
+
return candidate;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatDelegationMarkerContent(entry: ConversationEntry): string | null {
|
|
109
|
+
const marker = entry.delegationMarker;
|
|
110
|
+
if (!marker?.delegationConversationId || !marker?.recipientPubkey || !marker?.status) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const pubkeyService = getPubkeyService();
|
|
115
|
+
const shortConversationId = marker.delegationConversationId.slice(0, PREFIX_LENGTH);
|
|
116
|
+
const recipientName = pubkeyService.getNameSync(marker.recipientPubkey);
|
|
117
|
+
|
|
118
|
+
if (marker.status === "pending") {
|
|
119
|
+
return `⏳ Delegation ${shortConversationId} → ${recipientName} in progress`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (marker.status === "completed") {
|
|
123
|
+
return `✅ Delegation ${shortConversationId} → ${recipientName} completed`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return `⚠️ Delegation ${shortConversationId} → ${recipientName} aborted`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function safeStringify(value: unknown): string {
|
|
130
|
+
const seen = new WeakSet<object>();
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const serialized = JSON.stringify(value, (_key, currentValue) => {
|
|
134
|
+
if (typeof currentValue === "bigint") {
|
|
135
|
+
return currentValue.toString();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (typeof currentValue === "object" && currentValue !== null) {
|
|
139
|
+
if (seen.has(currentValue)) {
|
|
140
|
+
return "[Circular]";
|
|
141
|
+
}
|
|
142
|
+
seen.add(currentValue);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return currentValue;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (serialized === undefined) {
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return serialized;
|
|
153
|
+
} catch {
|
|
154
|
+
return "[Unserializable]";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function truncateWithSuffix(value: string, maxLength: number): string {
|
|
159
|
+
if (value.length <= maxLength) {
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const truncatedChars = value.length - maxLength;
|
|
164
|
+
return `${value.slice(0, maxLength)}... [truncated ${truncatedChars} chars]`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function extractToolCallParts(entry: ConversationEntry): ToolCallPart[] {
|
|
168
|
+
if (!entry.toolData || entry.toolData.length === 0) {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return entry.toolData
|
|
173
|
+
.map((tool) => tool as unknown as Record<string, unknown>)
|
|
174
|
+
.filter((tool) => (tool.type ?? entry.messageType) === "tool-call")
|
|
175
|
+
.map((tool) => ({
|
|
176
|
+
type: "tool-call",
|
|
177
|
+
toolCallId: typeof tool.toolCallId === "string" ? tool.toolCallId : "",
|
|
178
|
+
toolName: typeof tool.toolName === "string" ? tool.toolName : "unknown",
|
|
179
|
+
input: tool.input,
|
|
180
|
+
})) as ToolCallPart[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function resolveToolCallEventIdMap(entries: ConversationEntry[]): Map<string, string> {
|
|
184
|
+
const result = new Map<string, string>();
|
|
185
|
+
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
if (entry.messageType !== "tool-result" || !entry.eventId || !entry.toolData) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const part of entry.toolData) {
|
|
192
|
+
const raw = part as unknown as Record<string, unknown>;
|
|
193
|
+
if (typeof raw.toolCallId !== "string" || raw.toolCallId.length === 0) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!result.has(raw.toolCallId)) {
|
|
198
|
+
result.set(raw.toolCallId, entry.eventId);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function resolveConversationRootEventId(
|
|
207
|
+
entries: ConversationEntry[],
|
|
208
|
+
conversationId?: string
|
|
209
|
+
): string | null {
|
|
210
|
+
if (conversationId && conversationId.length > 0) {
|
|
211
|
+
return conversationId;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const entry of entries) {
|
|
215
|
+
if (entry.eventId) {
|
|
216
|
+
return entry.eventId;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildToolXmlAttributes(
|
|
224
|
+
entry: ConversationEntry,
|
|
225
|
+
maxToolDescriptionLength: number,
|
|
226
|
+
maxToolInputJsonLength: number
|
|
227
|
+
): Record<string, string> {
|
|
228
|
+
const attrs: Record<string, string> = {};
|
|
229
|
+
|
|
230
|
+
if (entry.transcriptToolAttributes) {
|
|
231
|
+
Object.assign(attrs, entry.transcriptToolAttributes);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const toolCallParts = extractToolCallParts(entry);
|
|
235
|
+
const firstPart = toolCallParts[0];
|
|
236
|
+
const firstInput = firstPart?.input as Record<string, unknown> | undefined;
|
|
237
|
+
|
|
238
|
+
if (!attrs.description && firstInput && typeof firstInput.description === "string") {
|
|
239
|
+
attrs.description = truncateWithSuffix(firstInput.description, maxToolDescriptionLength);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const fallbackArgMappings: Array<{ key: string; attribute: string }> = [
|
|
243
|
+
{ key: "path", attribute: "file_path" },
|
|
244
|
+
{ key: "pattern", attribute: "pattern" },
|
|
245
|
+
{ key: "query", attribute: "query" },
|
|
246
|
+
{ key: "glob", attribute: "glob" },
|
|
247
|
+
{ key: "file_path", attribute: "file_path" },
|
|
248
|
+
];
|
|
249
|
+
for (const mapping of fallbackArgMappings) {
|
|
250
|
+
if (attrs[mapping.attribute]) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const candidate = firstInput?.[mapping.key];
|
|
254
|
+
if (typeof candidate === "string" && candidate.length > 0) {
|
|
255
|
+
attrs[mapping.attribute] = candidate;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const toolName = firstPart?.toolName ?? "unknown";
|
|
260
|
+
if (toolName.startsWith("mcp_") && !attrs.args) {
|
|
261
|
+
attrs.args = truncateWithSuffix(safeStringify(firstInput ?? {}), maxToolInputJsonLength);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return attrs;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function buildConversationTimeline(
|
|
268
|
+
entries: ConversationEntry[],
|
|
269
|
+
options: ConversationTimelineOptions = {}
|
|
270
|
+
): ConversationTimeline {
|
|
271
|
+
const pubkeyService = getPubkeyService();
|
|
272
|
+
const t0 = computeBaselineTimestamp(entries);
|
|
273
|
+
let lastKnownTimestamp = t0;
|
|
274
|
+
const timelineEntries: ConversationTimelineEntry[] = [];
|
|
275
|
+
|
|
276
|
+
for (const entry of entries) {
|
|
277
|
+
const effectiveTimestamp = entry.timestamp ?? lastKnownTimestamp;
|
|
278
|
+
const relativeSeconds = Math.floor(effectiveTimestamp - t0);
|
|
279
|
+
|
|
280
|
+
if (entry.timestamp !== undefined) {
|
|
281
|
+
lastKnownTimestamp = entry.timestamp;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!shouldIncludeEntry(entry, options)) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const authorPubkey = entry.senderPubkey ?? entry.pubkey;
|
|
289
|
+
const author = pubkeyService.getNameSync(authorPubkey);
|
|
290
|
+
const recipients = (entry.targetedPubkeys ?? []).map((pubkey) =>
|
|
291
|
+
pubkeyService.getNameSync(pubkey)
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
timelineEntries.push({
|
|
295
|
+
entry,
|
|
296
|
+
relativeSeconds,
|
|
297
|
+
author,
|
|
298
|
+
recipients,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
t0,
|
|
304
|
+
entries: timelineEntries,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function renderConversationXml(
|
|
309
|
+
entries: ConversationEntry[],
|
|
310
|
+
options: ConversationXmlRenderOptions = {}
|
|
311
|
+
): ConversationXmlRenderResult {
|
|
312
|
+
const shortIdLength = options.shortIdLength ?? DEFAULT_SHORT_ID_LENGTH;
|
|
313
|
+
const maxToolDescriptionLength =
|
|
314
|
+
options.maxToolDescriptionLength ?? DEFAULT_MAX_TOOL_DESCRIPTION_LENGTH;
|
|
315
|
+
const maxToolInputJsonLength =
|
|
316
|
+
options.maxToolInputJsonLength ?? DEFAULT_MAX_TOOL_INPUT_JSON_LENGTH;
|
|
317
|
+
|
|
318
|
+
const timeline = buildConversationTimeline(entries, {
|
|
319
|
+
includeMessageTypes: options.includeMessageTypes,
|
|
320
|
+
requireTargetedPubkeys: options.requireTargetedPubkeys,
|
|
321
|
+
includeToolCalls: options.includeToolCalls,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const toolCallEventIdMap = resolveToolCallEventIdMap(entries);
|
|
325
|
+
const usedShortIds = new Set<string>();
|
|
326
|
+
const shortIdToEventId = new Map<string, string>();
|
|
327
|
+
const renderedIds: string[] = [];
|
|
328
|
+
|
|
329
|
+
const getOrCreateShortId = (eventId: string): string => {
|
|
330
|
+
for (const [shortId, fullId] of shortIdToEventId.entries()) {
|
|
331
|
+
if (fullId === eventId) {
|
|
332
|
+
return shortId;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const shortId = getShortEventId(eventId, shortIdLength, usedShortIds);
|
|
337
|
+
shortIdToEventId.set(shortId, eventId);
|
|
338
|
+
return shortId;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const rootEventId = resolveConversationRootEventId(entries, options.conversationId);
|
|
342
|
+
const rootId = rootEventId
|
|
343
|
+
? getOrCreateShortId(rootEventId)
|
|
344
|
+
: "unknown";
|
|
345
|
+
|
|
346
|
+
const lines: string[] = [`<conversation id="${escapeXml(rootId)}" t0="${timeline.t0}">`];
|
|
347
|
+
|
|
348
|
+
for (const timelineEntry of timeline.entries) {
|
|
349
|
+
const { entry, relativeSeconds, author, recipients } = timelineEntry;
|
|
350
|
+
|
|
351
|
+
if (entry.messageType === "tool-result") {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const timeIndicator = `+${relativeSeconds}`;
|
|
356
|
+
|
|
357
|
+
if (entry.messageType === "tool-call") {
|
|
358
|
+
const toolParts = extractToolCallParts(entry);
|
|
359
|
+
if (toolParts.length === 0) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const primaryPart = toolParts[0];
|
|
364
|
+
const toolCallId = primaryPart.toolCallId;
|
|
365
|
+
const candidateToolEventId = entry.eventId || toolCallEventIdMap.get(toolCallId);
|
|
366
|
+
const toolId = candidateToolEventId
|
|
367
|
+
? (FULL_HEX_EVENT_ID_REGEX.test(candidateToolEventId)
|
|
368
|
+
? candidateToolEventId
|
|
369
|
+
: getOrCreateShortId(candidateToolEventId))
|
|
370
|
+
: null;
|
|
371
|
+
if (toolId) {
|
|
372
|
+
renderedIds.push(toolId);
|
|
373
|
+
if (candidateToolEventId && FULL_HEX_EVENT_ID_REGEX.test(candidateToolEventId)) {
|
|
374
|
+
shortIdToEventId.set(toolId, candidateToolEventId);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const idAttr = toolId ? ` id="${escapeXml(toolId)}"` : "";
|
|
379
|
+
const nameAttr = ` name="${escapeXml(primaryPart.toolName || "unknown")}"`;
|
|
380
|
+
const transcriptAttrs = buildToolXmlAttributes(
|
|
381
|
+
entry,
|
|
382
|
+
maxToolDescriptionLength,
|
|
383
|
+
maxToolInputJsonLength
|
|
384
|
+
);
|
|
385
|
+
const extraAttrs = Object.entries(transcriptAttrs)
|
|
386
|
+
.map(([key, value]) => ` ${key}="${escapeXml(value)}"`)
|
|
387
|
+
.join("");
|
|
388
|
+
|
|
389
|
+
lines.push(
|
|
390
|
+
` <tool${idAttr} user="${escapeXml(author)}"${nameAttr}${extraAttrs} time="${timeIndicator}" />`
|
|
391
|
+
);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const messageText = entry.messageType === "delegation-marker"
|
|
396
|
+
? formatDelegationMarkerContent(entry)
|
|
397
|
+
: (entry.content || "(empty)");
|
|
398
|
+
|
|
399
|
+
if (messageText === null) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const shortEventId = entry.eventId ? getOrCreateShortId(entry.eventId) : null;
|
|
404
|
+
if (shortEventId) {
|
|
405
|
+
renderedIds.push(shortEventId);
|
|
406
|
+
}
|
|
407
|
+
const idAttr = shortEventId ? ` id="${escapeXml(shortEventId)}"` : "";
|
|
408
|
+
const recipientAttr = recipients.length > 0
|
|
409
|
+
? ` recipient="${escapeXml(recipients.join(", "))}"`
|
|
410
|
+
: "";
|
|
411
|
+
|
|
412
|
+
lines.push(
|
|
413
|
+
` <message${idAttr} author="${escapeXml(author)}"${recipientAttr} time="${timeIndicator}">${escapeXml(messageText)}</message>`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
lines.push("</conversation>");
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
xml: lines.join("\n"),
|
|
421
|
+
shortIdToEventId,
|
|
422
|
+
firstShortId: renderedIds[0] ?? null,
|
|
423
|
+
lastShortId: renderedIds[renderedIds.length - 1] ?? null,
|
|
424
|
+
};
|
|
425
|
+
}
|