@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
|
@@ -11,12 +11,10 @@ import {
|
|
|
11
11
|
type ChunkTypeChangeEvent,
|
|
12
12
|
type CompleteEvent,
|
|
13
13
|
type ContentEvent,
|
|
14
|
-
type RawChunkEvent,
|
|
15
14
|
type ReasoningEvent,
|
|
16
15
|
type SessionCapturedEvent,
|
|
17
16
|
type StreamErrorEvent,
|
|
18
17
|
} from "@/llm/types";
|
|
19
|
-
import { streamPublisher } from "@/llm";
|
|
20
18
|
import { PROVIDER_IDS } from "@/llm/providers/provider-ids";
|
|
21
19
|
import { shortenConversationId } from "@/utils/conversation-id";
|
|
22
20
|
import type { EventContext } from "@/nostr/types";
|
|
@@ -68,8 +66,15 @@ export interface StreamExecutionConfig {
|
|
|
68
66
|
* Handles all LLM stream event processing and coordination
|
|
69
67
|
*/
|
|
70
68
|
export class StreamExecutionHandler {
|
|
69
|
+
private static readonly STREAM_TEXT_DELTA_THROTTLE_MS = 1000;
|
|
70
|
+
|
|
71
71
|
private contentBuffer = "";
|
|
72
72
|
private reasoningBuffer = "";
|
|
73
|
+
private streamTextDeltaBuffer = "";
|
|
74
|
+
private streamTextDeltaSequence = 0;
|
|
75
|
+
private streamTextDeltaTimer: NodeJS.Timeout | undefined;
|
|
76
|
+
private streamTextDeltaFlushChain: Promise<void> = Promise.resolve();
|
|
77
|
+
private streamTextDeltaEventContext: EventContext | undefined;
|
|
73
78
|
private result: StreamExecutionResult | undefined;
|
|
74
79
|
private lastUsedVariant: string | undefined;
|
|
75
80
|
private currentModel: LanguageModel | undefined;
|
|
@@ -134,19 +139,6 @@ export class StreamExecutionHandler {
|
|
|
134
139
|
});
|
|
135
140
|
}
|
|
136
141
|
|
|
137
|
-
// Subscribe to raw chunks and forward to local streaming socket
|
|
138
|
-
llmService.on("raw-chunk", (event: RawChunkEvent) => {
|
|
139
|
-
logger.debug("[StreamExecutionHandler] raw-chunk received", {
|
|
140
|
-
chunkType: event.chunk.type,
|
|
141
|
-
agentPubkey: context.agent.pubkey.substring(0, 8),
|
|
142
|
-
});
|
|
143
|
-
streamPublisher.write({
|
|
144
|
-
agent_pubkey: context.agent.pubkey,
|
|
145
|
-
conversation_id: context.conversationId,
|
|
146
|
-
data: event.chunk,
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
142
|
// Create callbacks using extracted factory functions
|
|
151
143
|
const prepareStep = createPrepareStep({
|
|
152
144
|
context,
|
|
@@ -182,6 +174,11 @@ export class StreamExecutionHandler {
|
|
|
182
174
|
prepareStep,
|
|
183
175
|
});
|
|
184
176
|
|
|
177
|
+
await this.flushStreamTextDeltas({
|
|
178
|
+
force: true,
|
|
179
|
+
reason: "stream-return",
|
|
180
|
+
});
|
|
181
|
+
|
|
185
182
|
// DIAGNOSTIC: Track when stream() returns with process state comparison
|
|
186
183
|
const streamCallEndTime = Date.now();
|
|
187
184
|
const streamCallDuration = streamCallEndTime - streamCallStartTime;
|
|
@@ -284,11 +281,13 @@ export class StreamExecutionHandler {
|
|
|
284
281
|
const { context, llmService, toolTracker, toolsObject } = this.config;
|
|
285
282
|
const agentPublisher = context.agentPublisher;
|
|
286
283
|
const eventContext = this.createEventContext();
|
|
284
|
+
this.streamTextDeltaEventContext = eventContext;
|
|
287
285
|
const ralNumber = this.config.ralNumber;
|
|
288
286
|
|
|
289
287
|
llmService.on("content", (event: ContentEvent) => {
|
|
290
288
|
process.stdout.write(chalk.white(event.delta));
|
|
291
289
|
this.contentBuffer += event.delta;
|
|
290
|
+
this.enqueueStreamTextDelta(event.delta);
|
|
292
291
|
});
|
|
293
292
|
|
|
294
293
|
llmService.on("reasoning", (event: ReasoningEvent) => {
|
|
@@ -301,6 +300,10 @@ export class StreamExecutionHandler {
|
|
|
301
300
|
await this.flushReasoningBuffer();
|
|
302
301
|
}
|
|
303
302
|
if (event.from === "text-delta") {
|
|
303
|
+
await this.flushStreamTextDeltas({
|
|
304
|
+
force: true,
|
|
305
|
+
reason: "chunk-type-change",
|
|
306
|
+
});
|
|
304
307
|
await this.flushContentBuffer();
|
|
305
308
|
}
|
|
306
309
|
});
|
|
@@ -312,7 +315,12 @@ export class StreamExecutionHandler {
|
|
|
312
315
|
"ral.number": ralNumber,
|
|
313
316
|
});
|
|
314
317
|
|
|
315
|
-
llmService.on("complete", (event: CompleteEvent) => {
|
|
318
|
+
llmService.on("complete", async (event: CompleteEvent) => {
|
|
319
|
+
await this.flushStreamTextDeltas({
|
|
320
|
+
force: true,
|
|
321
|
+
reason: "complete",
|
|
322
|
+
});
|
|
323
|
+
|
|
316
324
|
const completeReceivedTime = Date.now();
|
|
317
325
|
const timeSinceRegistration = completeReceivedTime - completeListenerRegisteredAt;
|
|
318
326
|
this.executionSpan?.addEvent("executor.complete_received", {
|
|
@@ -349,6 +357,11 @@ export class StreamExecutionHandler {
|
|
|
349
357
|
});
|
|
350
358
|
|
|
351
359
|
llmService.on("stream-error", async (event: StreamErrorEvent) => {
|
|
360
|
+
await this.flushStreamTextDeltas({
|
|
361
|
+
force: true,
|
|
362
|
+
reason: "stream-error",
|
|
363
|
+
});
|
|
364
|
+
|
|
352
365
|
const errorReceivedTime = Date.now();
|
|
353
366
|
const timeSinceRegistration = errorReceivedTime - completeListenerRegisteredAt;
|
|
354
367
|
this.executionSpan?.addEvent("executor.stream_error_received", {
|
|
@@ -500,6 +513,11 @@ export class StreamExecutionHandler {
|
|
|
500
513
|
const { context } = this.config;
|
|
501
514
|
const ralNumber = this.config.ralNumber;
|
|
502
515
|
|
|
516
|
+
await this.flushStreamTextDeltas({
|
|
517
|
+
force: true,
|
|
518
|
+
reason: "handle-stream-error",
|
|
519
|
+
});
|
|
520
|
+
|
|
503
521
|
if (abortSignal.aborted) {
|
|
504
522
|
this.executionSpan?.addEvent("executor.aborted_by_stop_signal", {
|
|
505
523
|
"ral.number": ralNumber,
|
|
@@ -565,6 +583,8 @@ export class StreamExecutionHandler {
|
|
|
565
583
|
const ralNumber = this.config.ralNumber;
|
|
566
584
|
const ralRegistry = RALRegistry.getInstance();
|
|
567
585
|
|
|
586
|
+
this.clearStreamTextDeltaTimer();
|
|
587
|
+
|
|
568
588
|
ralRegistry.endLLMStream(context.agent.pubkey, context.conversationId, ralNumber);
|
|
569
589
|
ralRegistry.setStreaming(context.agent.pubkey, context.conversationId, ralNumber, false);
|
|
570
590
|
|
|
@@ -576,4 +596,78 @@ export class StreamExecutionHandler {
|
|
|
576
596
|
clearLLMSpanId(currentSpan.spanContext().traceId);
|
|
577
597
|
}
|
|
578
598
|
}
|
|
599
|
+
|
|
600
|
+
private enqueueStreamTextDelta(delta: string): void {
|
|
601
|
+
if (delta.length === 0) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
this.streamTextDeltaBuffer += delta;
|
|
606
|
+
if (this.streamTextDeltaTimer) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
this.streamTextDeltaTimer = setTimeout(() => {
|
|
611
|
+
this.streamTextDeltaTimer = undefined;
|
|
612
|
+
void this.flushStreamTextDeltas({
|
|
613
|
+
force: false,
|
|
614
|
+
reason: "throttle-window",
|
|
615
|
+
});
|
|
616
|
+
}, StreamExecutionHandler.STREAM_TEXT_DELTA_THROTTLE_MS);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private clearStreamTextDeltaTimer(): void {
|
|
620
|
+
if (this.streamTextDeltaTimer) {
|
|
621
|
+
clearTimeout(this.streamTextDeltaTimer);
|
|
622
|
+
this.streamTextDeltaTimer = undefined;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private flushStreamTextDeltas(options: { force: boolean; reason: string }): Promise<void> {
|
|
627
|
+
this.streamTextDeltaFlushChain = this.streamTextDeltaFlushChain
|
|
628
|
+
.then(async () => {
|
|
629
|
+
if (options.force) {
|
|
630
|
+
this.clearStreamTextDeltaTimer();
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (this.streamTextDeltaBuffer.length === 0) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const eventContext = this.streamTextDeltaEventContext;
|
|
638
|
+
if (!eventContext) {
|
|
639
|
+
this.streamTextDeltaBuffer = "";
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const deltaToPublish = this.streamTextDeltaBuffer;
|
|
644
|
+
this.streamTextDeltaBuffer = "";
|
|
645
|
+
this.streamTextDeltaSequence += 1;
|
|
646
|
+
|
|
647
|
+
this.executionSpan?.addEvent("executor.stream_delta_flush", {
|
|
648
|
+
"delta.sequence": this.streamTextDeltaSequence,
|
|
649
|
+
"delta.length": deltaToPublish.length,
|
|
650
|
+
"delta.reason": options.reason,
|
|
651
|
+
"ral.number": this.config.ralNumber,
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
await this.config.context.agentPublisher.streamTextDelta(
|
|
655
|
+
{
|
|
656
|
+
delta: deltaToPublish,
|
|
657
|
+
sequence: this.streamTextDeltaSequence,
|
|
658
|
+
},
|
|
659
|
+
eventContext
|
|
660
|
+
);
|
|
661
|
+
})
|
|
662
|
+
.catch((error) => {
|
|
663
|
+
logger.warn("[StreamExecutionHandler] Failed to flush stream text deltas", {
|
|
664
|
+
error: formatAnyError(error),
|
|
665
|
+
conversationId: this.config.context.conversationId.substring(0, 12),
|
|
666
|
+
agent: this.config.context.agent.slug,
|
|
667
|
+
ralNumber: this.config.ralNumber,
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
return this.streamTextDeltaFlushChain;
|
|
672
|
+
}
|
|
579
673
|
}
|
|
@@ -126,15 +126,25 @@ export async function setupStreamExecution(
|
|
|
126
126
|
content: injection.content,
|
|
127
127
|
});
|
|
128
128
|
} else {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
129
|
+
const relocated = injection.eventId
|
|
130
|
+
? conversationStore.relocateToEnd(injection.eventId, {
|
|
131
|
+
ral: ralNumber,
|
|
132
|
+
senderPubkey: injection.senderPubkey,
|
|
133
|
+
targetedPubkeys: [context.agent.pubkey],
|
|
134
|
+
})
|
|
135
|
+
: false;
|
|
136
|
+
|
|
137
|
+
if (!relocated) {
|
|
138
|
+
conversationStore.addMessage({
|
|
139
|
+
pubkey: context.triggeringEvent.pubkey,
|
|
140
|
+
ral: ralNumber,
|
|
141
|
+
content: injection.content,
|
|
142
|
+
messageType: "text",
|
|
143
|
+
targetedPubkeys: [context.agent.pubkey],
|
|
144
|
+
senderPubkey: injection.senderPubkey,
|
|
145
|
+
eventId: injection.eventId,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
138
148
|
}
|
|
139
149
|
}
|
|
140
150
|
|
|
@@ -20,6 +20,106 @@ import type { FullRuntimeContext } from "./types";
|
|
|
20
20
|
import { getHeuristicEngine } from "@/services/heuristics";
|
|
21
21
|
import { buildHeuristicContext } from "@/services/heuristics/ContextBuilder";
|
|
22
22
|
|
|
23
|
+
const TRANSCRIPT_RAW_ARGS_MAX_LENGTH = 200;
|
|
24
|
+
|
|
25
|
+
function truncateForTranscript(value: string, maxLength: number): string {
|
|
26
|
+
if (value.length <= maxLength) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
const truncatedChars = value.length - maxLength;
|
|
30
|
+
return `${value.slice(0, maxLength)}... [truncated ${truncatedChars} chars]`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeTranscriptAttrName(value: string): string {
|
|
34
|
+
const normalized = value.replaceAll(/[^a-zA-Z0-9_-]/g, "_");
|
|
35
|
+
if (/^[a-zA-Z_]/.test(normalized)) {
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
38
|
+
return `arg_${normalized}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function serializeTranscriptArg(value: unknown): string | undefined {
|
|
42
|
+
if (value === undefined || value === null) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === "string") {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
49
|
+
return String(value);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
} catch {
|
|
54
|
+
return "[Unserializable]";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildTranscriptToolAttributes(
|
|
59
|
+
toolName: string,
|
|
60
|
+
args: unknown,
|
|
61
|
+
toolDef: AISdkTool | undefined
|
|
62
|
+
): Record<string, string> | undefined {
|
|
63
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
64
|
+
if (toolName.startsWith("mcp_") && args !== undefined) {
|
|
65
|
+
const raw = truncateForTranscript(String(args), TRANSCRIPT_RAW_ARGS_MAX_LENGTH);
|
|
66
|
+
return { args: raw };
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const argsObject = args as Record<string, unknown>;
|
|
72
|
+
const attrs: Record<string, string> = {};
|
|
73
|
+
|
|
74
|
+
const description = argsObject.description;
|
|
75
|
+
if (typeof description === "string" && description.trim().length > 0) {
|
|
76
|
+
attrs.description = description.trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const specs = toolDef?.transcriptArgsToInclude;
|
|
80
|
+
if (specs && specs.length > 0) {
|
|
81
|
+
for (const spec of specs) {
|
|
82
|
+
const raw = argsObject[spec.key];
|
|
83
|
+
const serialized = serializeTranscriptArg(raw);
|
|
84
|
+
if (!serialized || serialized.length === 0) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const attrName = normalizeTranscriptAttrName(spec.attribute ?? spec.key);
|
|
88
|
+
attrs[attrName] = serialized;
|
|
89
|
+
}
|
|
90
|
+
} else if (toolName.startsWith("mcp_")) {
|
|
91
|
+
const rawArgs = truncateForTranscript(
|
|
92
|
+
JSON.stringify(argsObject),
|
|
93
|
+
TRANSCRIPT_RAW_ARGS_MAX_LENGTH
|
|
94
|
+
);
|
|
95
|
+
attrs.args = rawArgs;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return Object.keys(attrs).length > 0 ? attrs : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function setToolCallEventIdFromToolCallId(
|
|
102
|
+
conversationStore: FullRuntimeContext["conversationStore"],
|
|
103
|
+
toolCallId: string,
|
|
104
|
+
toolEventId: string
|
|
105
|
+
): void {
|
|
106
|
+
const messages = conversationStore.getAllMessages();
|
|
107
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
108
|
+
const message = messages[i];
|
|
109
|
+
if (message.messageType !== "tool-call" || !message.toolData) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const hasToolCallId = (message.toolData as ToolCallPart[]).some(
|
|
113
|
+
(part) => part.toolCallId === toolCallId
|
|
114
|
+
);
|
|
115
|
+
if (!hasToolCallId) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
conversationStore.setEventId(i, toolEventId);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
23
123
|
/**
|
|
24
124
|
* Configuration for setting up tool event handlers
|
|
25
125
|
*/
|
|
@@ -77,6 +177,15 @@ export function setupToolEventHandlers(config: ToolEventHandlersConfig): void {
|
|
|
77
177
|
event.args
|
|
78
178
|
);
|
|
79
179
|
|
|
180
|
+
// Generate human-readable summary from the tool's own formatter
|
|
181
|
+
const toolDef = toolsObject[event.toolName];
|
|
182
|
+
const humanReadable = toolDef?.getHumanReadableContent?.(event.args ?? {});
|
|
183
|
+
const transcriptToolAttributes = buildTranscriptToolAttributes(
|
|
184
|
+
event.toolName,
|
|
185
|
+
event.args,
|
|
186
|
+
toolDef
|
|
187
|
+
);
|
|
188
|
+
|
|
80
189
|
conversationStore.addMessage({
|
|
81
190
|
pubkey: context.agent.pubkey,
|
|
82
191
|
ral: ralNumber,
|
|
@@ -90,6 +199,8 @@ export function setupToolEventHandlers(config: ToolEventHandlersConfig): void {
|
|
|
90
199
|
input: event.args ?? {},
|
|
91
200
|
},
|
|
92
201
|
] as ToolCallPart[],
|
|
202
|
+
...(humanReadable ? { humanReadable } : {}),
|
|
203
|
+
...(transcriptToolAttributes ? { transcriptToolAttributes } : {}),
|
|
93
204
|
});
|
|
94
205
|
|
|
95
206
|
const toolEvent = await toolTracker.trackExecution({
|
|
@@ -225,6 +336,7 @@ export function setupToolEventHandlers(config: ToolEventHandlersConfig): void {
|
|
|
225
336
|
|
|
226
337
|
if (toolEventId) {
|
|
227
338
|
conversationStore.setEventId(toolResultMessageIndex, toolEventId);
|
|
339
|
+
setToolCallEventIdFromToolCallId(conversationStore, event.toolCallId, toolEventId);
|
|
228
340
|
}
|
|
229
341
|
|
|
230
342
|
ralRegistry.setToolActive(
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* role-categories - Semantic classification for agents
|
|
3
|
+
*
|
|
4
|
+
* Agents have an optional `category` field for semantic classification and organizational purposes.
|
|
5
|
+
* Categories do NOT restrict tool access — all agents have access to all tools.
|
|
6
|
+
*
|
|
7
|
+
* Categories represent operational roles:
|
|
8
|
+
* - `principal` — Human proxy (e.g., human-replica)
|
|
9
|
+
* - `orchestrator` — PMs, coordinators
|
|
10
|
+
* - `worker` — Developers, implementers
|
|
11
|
+
* - `advisor` — Experts, reviewers
|
|
12
|
+
* - `auditor` — Testers, code reviewers
|
|
13
|
+
*
|
|
14
|
+
* Unknown/missing category remains undefined. Only set a category when explicitly known.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Valid agent categories.
|
|
19
|
+
*/
|
|
20
|
+
export type AgentCategory = "principal" | "orchestrator" | "worker" | "advisor" | "auditor";
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* All recognized category values for validation.
|
|
25
|
+
*/
|
|
26
|
+
export const VALID_CATEGORIES: readonly AgentCategory[] = [
|
|
27
|
+
"principal",
|
|
28
|
+
"orchestrator",
|
|
29
|
+
"worker",
|
|
30
|
+
"advisor",
|
|
31
|
+
"auditor",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if a string is a valid agent category.
|
|
36
|
+
*/
|
|
37
|
+
export function isValidCategory(value: string): value is AgentCategory {
|
|
38
|
+
return VALID_CATEGORIES.includes(value as AgentCategory);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve an agent's effective category.
|
|
43
|
+
* Returns the category if valid and provided, otherwise undefined.
|
|
44
|
+
*
|
|
45
|
+
* Categories are for semantic classification and organizational purposes only.
|
|
46
|
+
* They do not restrict tool access — all agents have access to all tools.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveCategory(category: string | undefined): AgentCategory | undefined {
|
|
49
|
+
if (category && isValidCategory(category)) {
|
|
50
|
+
return category;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { AgentMetadataStore } from "@/services/agents";
|
|
|
2
2
|
import type { LLMService } from "@/llm/service";
|
|
3
3
|
import type { MCPConfig, MCPServerConfig } from "@/llm/providers/types";
|
|
4
4
|
import type { OnStreamStartCallback } from "@/llm/types";
|
|
5
|
+
import type { AgentCategory } from "@/agents/role-categories";
|
|
5
6
|
import type { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
|
6
7
|
import type { Tool as CoreTool } from "ai";
|
|
7
8
|
import type { AgentProjectConfig } from "./storage";
|
|
@@ -23,6 +24,12 @@ export interface AgentInstance {
|
|
|
23
24
|
pubkey: string;
|
|
24
25
|
signer: NDKPrivateKeySigner;
|
|
25
26
|
role: string;
|
|
27
|
+
/**
|
|
28
|
+
* Agent category for semantic classification and organizational purposes.
|
|
29
|
+
* Resolved from the agent definition's category tag.
|
|
30
|
+
* No restrictions are applied based on category — all agents have access to all tools.
|
|
31
|
+
*/
|
|
32
|
+
category?: AgentCategory;
|
|
26
33
|
description?: string;
|
|
27
34
|
instructions?: string;
|
|
28
35
|
customInstructions?: string; // Custom system prompt instructions
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MCPServerConfig } from "@/llm/providers/types";
|
|
2
|
+
import type { AgentCategory } from "@/agents/role-categories";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Default agent configuration block.
|
|
@@ -61,6 +62,12 @@ export interface ProjectScopedConfig {
|
|
|
61
62
|
export interface StoredAgentData {
|
|
62
63
|
name: string;
|
|
63
64
|
role: string;
|
|
65
|
+
/**
|
|
66
|
+
* Agent category for role-based tool restrictions (TIP-01).
|
|
67
|
+
* Valid values: "principal", "orchestrator", "worker", "advisor", "auditor".
|
|
68
|
+
* When missing or unrecognized, defaults to "advisor" (most restrictive).
|
|
69
|
+
*/
|
|
70
|
+
category?: AgentCategory;
|
|
64
71
|
description?: string;
|
|
65
72
|
instructions?: string;
|
|
66
73
|
useCriteria?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { llmServiceFactory } from "@/llm/LLMServiceFactory";
|
|
3
|
+
import { logger } from "@/utils/logger";
|
|
3
4
|
import type { LLMConfiguration } from "@/services/config/types";
|
|
4
5
|
import type { OpenClawWorkspaceFiles } from "./openclaw-reader";
|
|
5
6
|
|
|
@@ -41,17 +42,72 @@ Given these workspace files, return a JSON object with exactly these fields:
|
|
|
41
42
|
${sections.join("\n\n")}`;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
async function distillWithRetry<T>(
|
|
46
|
+
llmConfigs: LLMConfiguration[],
|
|
47
|
+
run: (service: ReturnType<typeof llmServiceFactory.createService>) => Promise<T>,
|
|
48
|
+
): Promise<T> {
|
|
49
|
+
if (llmConfigs.length === 0) {
|
|
50
|
+
throw new Error("No LLM configurations available for distillation");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let lastError: unknown;
|
|
54
|
+
for (const config of llmConfigs) {
|
|
55
|
+
try {
|
|
56
|
+
const service = llmServiceFactory.createService(config);
|
|
57
|
+
return await run(service);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
lastError = error;
|
|
60
|
+
if (llmConfigs.length > 1) {
|
|
61
|
+
logger.warn(
|
|
62
|
+
`[distiller] call failed with ${config.provider}:${config.model}, trying next model...`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
throw lastError;
|
|
69
|
+
}
|
|
70
|
+
|
|
44
71
|
export async function distillAgentIdentity(
|
|
45
72
|
files: OpenClawWorkspaceFiles,
|
|
46
|
-
|
|
73
|
+
llmConfigs: LLMConfiguration[]
|
|
47
74
|
): Promise<DistilledAgentIdentity> {
|
|
48
|
-
const service = llmServiceFactory.createService(llmConfig);
|
|
49
75
|
const prompt = buildDistillationPrompt(files);
|
|
76
|
+
const messages = [{ role: "user" as const, content: prompt }];
|
|
77
|
+
|
|
78
|
+
return distillWithRetry(llmConfigs, async (service) => {
|
|
79
|
+
const { object } = await service.generateObject(messages, DistilledIdentitySchema);
|
|
80
|
+
return object;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildUserContextPrompt(rawUserMd: string): string {
|
|
85
|
+
return `You are cleaning up a user profile document for use as context in an AI assistant's system prompt.
|
|
86
|
+
|
|
87
|
+
Given the raw USER.md content below, produce a clean, concise summary of everything that would be useful for an AI assistant to know about this user. Write it as a brief markdown section.
|
|
88
|
+
|
|
89
|
+
Keep anything that helps the assistant interact better: name, preferences, timezone, communication style, interests, projects, technical background, etc.
|
|
90
|
+
|
|
91
|
+
Drop anything that is noise: unknown/empty fields, platform-specific metadata (IDs, timestamps of first conversations), internal bookkeeping, and formatting artifacts.
|
|
92
|
+
|
|
93
|
+
If the document contains almost nothing useful, return an empty string.
|
|
94
|
+
|
|
95
|
+
<USER.md>
|
|
96
|
+
${rawUserMd}
|
|
97
|
+
</USER.md>`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function distillUserContext(
|
|
101
|
+
rawUserMd: string,
|
|
102
|
+
llmConfigs: LLMConfiguration[],
|
|
103
|
+
): Promise<string> {
|
|
104
|
+
const prompt = buildUserContextPrompt(rawUserMd);
|
|
105
|
+
const messages = [{ role: "user" as const, content: prompt }];
|
|
50
106
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
107
|
+
const result = await distillWithRetry(llmConfigs, async (service) => {
|
|
108
|
+
const { text } = await service.generateText(messages);
|
|
109
|
+
return text;
|
|
110
|
+
});
|
|
55
111
|
|
|
56
|
-
return
|
|
112
|
+
return result.trim();
|
|
57
113
|
}
|
|
@@ -130,6 +130,60 @@ export async function readOpenClawAgents(stateDir: string): Promise<OpenClawAgen
|
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
export interface OpenClawCredential {
|
|
134
|
+
provider: string;
|
|
135
|
+
apiKey: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Read provider credentials from OpenClaw's auth-profiles.json.
|
|
140
|
+
* Supports token, api_key, and oauth profile types.
|
|
141
|
+
* Returns deduplicated credentials (first occurrence per provider wins).
|
|
142
|
+
*/
|
|
143
|
+
export async function readOpenClawCredentials(stateDir: string): Promise<OpenClawCredential[]> {
|
|
144
|
+
const profilePath = path.join(stateDir, "agents", "main", "agent", "auth-profiles.json");
|
|
145
|
+
const content = await readFileOrNull(profilePath);
|
|
146
|
+
if (!content) return [];
|
|
147
|
+
|
|
148
|
+
let parsed: { profiles?: Record<string, Record<string, string>> };
|
|
149
|
+
try {
|
|
150
|
+
parsed = JSON.parse(content);
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!parsed.profiles) return [];
|
|
156
|
+
|
|
157
|
+
const credentials: OpenClawCredential[] = [];
|
|
158
|
+
const seenProviders = new Set<string>();
|
|
159
|
+
|
|
160
|
+
// Sort keys so `:default` profiles come first
|
|
161
|
+
const sortedKeys = Object.keys(parsed.profiles).sort((a, b) => {
|
|
162
|
+
const aDefault = a.includes(":default") ? 0 : 1;
|
|
163
|
+
const bDefault = b.includes(":default") ? 0 : 1;
|
|
164
|
+
return aDefault - bDefault || a.localeCompare(b);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
for (const key of sortedKeys) {
|
|
168
|
+
const profile = parsed.profiles[key];
|
|
169
|
+
const provider = profile.provider;
|
|
170
|
+
if (!provider || seenProviders.has(provider)) continue;
|
|
171
|
+
|
|
172
|
+
const apiKey =
|
|
173
|
+
profile.type === "token" ? profile.token :
|
|
174
|
+
profile.type === "api_key" ? profile.key :
|
|
175
|
+
profile.type === "oauth" ? profile.access :
|
|
176
|
+
undefined;
|
|
177
|
+
|
|
178
|
+
if (apiKey) {
|
|
179
|
+
seenProviders.add(provider);
|
|
180
|
+
credentials.push({ provider, apiKey });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return credentials;
|
|
185
|
+
}
|
|
186
|
+
|
|
133
187
|
/**
|
|
134
188
|
* Convert OpenClaw model format to TENEX format.
|
|
135
189
|
* OpenClaw uses "provider/model", TENEX uses "provider:model".
|