@tenex-chat/backend 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +5 -1
  2. package/dist/daemon-wrapper.cjs +34 -0
  3. package/dist/index.js +59268 -0
  4. package/dist/wrapper.js +171 -0
  5. package/package.json +19 -27
  6. package/src/agents/AgentRegistry.ts +9 -7
  7. package/src/agents/AgentStorage.ts +24 -1
  8. package/src/agents/agent-installer.ts +6 -0
  9. package/src/agents/agent-loader.ts +7 -2
  10. package/src/agents/constants.ts +10 -2
  11. package/src/agents/execution/AgentExecutor.ts +35 -6
  12. package/src/agents/execution/StreamCallbacks.ts +53 -13
  13. package/src/agents/execution/StreamExecutionHandler.ts +110 -16
  14. package/src/agents/execution/StreamSetup.ts +19 -9
  15. package/src/agents/execution/ToolEventHandlers.ts +112 -0
  16. package/src/agents/role-categories.ts +53 -0
  17. package/src/agents/types/runtime.ts +7 -0
  18. package/src/agents/types/storage.ts +7 -0
  19. package/src/commands/agent/import/openclaw-distiller.ts +63 -7
  20. package/src/commands/agent/import/openclaw-reader.ts +54 -0
  21. package/src/commands/agent/import/openclaw.ts +120 -29
  22. package/src/commands/agent/index.ts +83 -2
  23. package/src/commands/setup/display.ts +123 -0
  24. package/src/commands/setup/embed.ts +13 -13
  25. package/src/commands/setup/global-system-prompt.ts +15 -17
  26. package/src/commands/setup/image.ts +17 -20
  27. package/src/commands/setup/interactive.ts +37 -20
  28. package/src/commands/setup/llm.ts +12 -7
  29. package/src/commands/setup/onboarding.ts +1580 -248
  30. package/src/commands/setup/providers.ts +3 -3
  31. package/src/conversations/ConversationStore.ts +23 -2
  32. package/src/conversations/MessageBuilder.ts +51 -73
  33. package/src/conversations/formatters/utils/conversation-transcript-formatter.ts +425 -0
  34. package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +40 -98
  35. package/src/conversations/search/embeddings/ConversationIndexingJob.ts +40 -52
  36. package/src/conversations/services/ConversationSummarizer.ts +1 -2
  37. package/src/conversations/types.ts +11 -0
  38. package/src/daemon/Daemon.ts +78 -57
  39. package/src/daemon/ProjectRuntime.ts +6 -12
  40. package/src/daemon/SubscriptionManager.ts +13 -0
  41. package/src/daemon/index.ts +0 -1
  42. package/src/event-handler/index.ts +1 -0
  43. package/src/index.ts +20 -1
  44. package/src/llm/ChunkHandler.ts +1 -1
  45. package/src/llm/FinishHandler.ts +28 -4
  46. package/src/llm/LLMConfigEditor.ts +218 -106
  47. package/src/llm/index.ts +0 -4
  48. package/src/llm/meta/MetaModelResolver.ts +3 -18
  49. package/src/llm/middleware/message-sanitizer.ts +153 -0
  50. package/src/llm/providers/ollama-models.ts +0 -38
  51. package/src/llm/service.ts +50 -15
  52. package/src/llm/types.ts +0 -12
  53. package/src/llm/utils/ConfigurationManager.ts +88 -465
  54. package/src/llm/utils/ConfigurationTester.ts +42 -185
  55. package/src/llm/utils/ModelSelector.ts +156 -92
  56. package/src/llm/utils/ProviderConfigUI.ts +10 -141
  57. package/src/llm/utils/models-dev-cache.ts +102 -23
  58. package/src/llm/utils/provider-select-prompt.ts +284 -0
  59. package/src/llm/utils/provider-setup.ts +81 -34
  60. package/src/llm/utils/variant-list-prompt.ts +361 -0
  61. package/src/nostr/AgentEventDecoder.ts +1 -0
  62. package/src/nostr/AgentEventEncoder.ts +37 -0
  63. package/src/nostr/AgentProfilePublisher.ts +13 -0
  64. package/src/nostr/AgentPublisher.ts +26 -0
  65. package/src/nostr/kinds.ts +1 -0
  66. package/src/nostr/ndkClient.ts +4 -1
  67. package/src/nostr/types.ts +12 -0
  68. package/src/prompts/fragments/25-rag-instructions.ts +22 -21
  69. package/src/prompts/fragments/31-agents-md-guidance.ts +7 -21
  70. package/src/prompts/fragments/index.ts +2 -0
  71. package/src/prompts/utils/systemPromptBuilder.ts +18 -28
  72. package/src/services/AgentDefinitionMonitor.ts +8 -0
  73. package/src/services/ConfigService.ts +34 -0
  74. package/src/services/PubkeyService.ts +7 -1
  75. package/src/services/compression/CompressionService.ts +133 -74
  76. package/src/services/compression/compression-utils.ts +110 -19
  77. package/src/services/config/types.ts +0 -6
  78. package/src/services/dispatch/AgentDispatchService.ts +79 -0
  79. package/src/services/intervention/InterventionService.ts +78 -5
  80. package/src/services/nip46/Nip46SigningService.ts +30 -1
  81. package/src/services/projects/ProjectContext.ts +8 -6
  82. package/src/services/rag/RAGCollectionRegistry.ts +199 -0
  83. package/src/services/rag/RAGDatabaseService.ts +2 -7
  84. package/src/services/rag/RAGOperations.ts +25 -45
  85. package/src/services/rag/RAGService.ts +0 -31
  86. package/src/services/rag/RagSubscriptionService.ts +71 -122
  87. package/src/services/rag/rag-utils.ts +13 -0
  88. package/src/services/ral/RALRegistry.ts +25 -184
  89. package/src/services/reports/ReportEmbeddingService.ts +63 -113
  90. package/src/services/search/UnifiedSearchService.ts +115 -4
  91. package/src/services/search/index.ts +1 -0
  92. package/src/services/search/projectFilter.ts +20 -4
  93. package/src/services/search/providers/ConversationSearchProvider.ts +1 -0
  94. package/src/services/search/providers/GenericCollectionSearchProvider.ts +81 -0
  95. package/src/services/search/providers/LessonSearchProvider.ts +1 -8
  96. package/src/services/search/providers/ReportSearchProvider.ts +1 -0
  97. package/src/services/search/types.ts +24 -3
  98. package/src/services/trust-pubkeys/SystemPubkeyListService.ts +148 -0
  99. package/src/services/trust-pubkeys/TrustPubkeyService.ts +70 -9
  100. package/src/telemetry/setup.ts +2 -13
  101. package/src/tools/implementations/ask.ts +3 -3
  102. package/src/tools/implementations/conversation_get.ts +28 -268
  103. package/src/tools/implementations/fs_grep.ts +6 -6
  104. package/src/tools/implementations/fs_read.ts +2 -0
  105. package/src/tools/implementations/fs_write.ts +2 -0
  106. package/src/tools/implementations/learn.ts +38 -50
  107. package/src/tools/implementations/rag_add_documents.ts +6 -4
  108. package/src/tools/implementations/rag_create_collection.ts +37 -4
  109. package/src/tools/implementations/rag_delete_collection.ts +9 -0
  110. package/src/tools/implementations/{search.ts → rag_search.ts} +31 -25
  111. package/src/tools/registry.ts +7 -8
  112. package/src/tools/types.ts +11 -2
  113. package/src/tools/utils/transcript-args.ts +13 -0
  114. package/src/utils/cli-theme.ts +13 -0
  115. package/src/utils/logger.ts +55 -0
  116. package/src/utils/metadataKeys.ts +17 -0
  117. package/src/utils/sqlEscaping.ts +39 -0
  118. package/src/wrapper.ts +7 -3
  119. package/dist/src/index.js +0 -46790
  120. package/dist/tenex-backend-wrapper.cjs +0 -3
  121. package/src/agents/execution/constants.ts +0 -16
  122. package/src/agents/execution/index.ts +0 -3
  123. package/src/agents/index.ts +0 -4
  124. package/src/commands/agent.ts +0 -235
  125. package/src/conversations/formatters/DelegationXmlFormatter.ts +0 -64
  126. package/src/conversations/formatters/index.ts +0 -9
  127. package/src/conversations/index.ts +0 -2
  128. package/src/conversations/utils/content-utils.ts +0 -69
  129. package/src/daemon/UnixSocketTransport.ts +0 -318
  130. package/src/event-handler/newConversation.ts +0 -165
  131. package/src/events/NDKProjectStatus.ts +0 -384
  132. package/src/events/index.ts +0 -4
  133. package/src/lib/json-parser.ts +0 -30
  134. package/src/llm/RecordingState.ts +0 -37
  135. package/src/llm/StreamPublisher.ts +0 -40
  136. package/src/llm/middleware/flight-recorder.ts +0 -188
  137. package/src/llm/utils/claudeCodePromptCompiler.ts +0 -141
  138. package/src/nostr/constants.ts +0 -38
  139. package/src/prompts/core/index.ts +0 -3
  140. package/src/prompts/index.ts +0 -21
  141. package/src/services/image/index.ts +0 -12
  142. package/src/services/status/index.ts +0 -11
  143. package/src/telemetry/diagnostics.ts +0 -27
  144. package/src/tools/implementations/rag_query.ts +0 -107
  145. package/src/types/index.ts +0 -46
  146. package/src/utils/agentFetcher.ts +0 -107
  147. package/src/utils/conversation-utils.ts +0 -1
  148. package/src/utils/process.ts +0 -49
@@ -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
- conversationStore.addMessage({
130
- pubkey: context.triggeringEvent.pubkey,
131
- ral: ralNumber,
132
- content: injection.content,
133
- messageType: "text",
134
- targetedPubkeys: [context.agent.pubkey],
135
- senderPubkey: injection.senderPubkey,
136
- eventId: injection.eventId,
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
- llmConfig: LLMConfiguration
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 { object } = await service.generateObject(
52
- [{ role: "user", content: prompt }],
53
- DistilledIdentitySchema
54
- );
107
+ const result = await distillWithRetry(llmConfigs, async (service) => {
108
+ const { text } = await service.generateText(messages);
109
+ return text;
110
+ });
55
111
 
56
- return object;
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".