wave-agent-sdk 0.18.2 → 0.18.4
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/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +0 -2
- package/dist/managers/MemoryRuleManager.d.ts +9 -0
- package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
- package/dist/managers/MemoryRuleManager.js +18 -0
- package/dist/managers/aiManager.d.ts +1 -1
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +370 -345
- package/dist/managers/messageManager.d.ts +32 -13
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +82 -76
- package/dist/prompts/index.d.ts +10 -4
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +21 -20
- package/dist/services/aiService.d.ts +2 -1
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +37 -10
- package/dist/services/memory.d.ts +1 -0
- package/dist/services/memory.d.ts.map +1 -1
- package/dist/services/memory.js +35 -17
- package/dist/tools/agentTool.d.ts.map +1 -1
- package/dist/tools/agentTool.js +24 -2
- package/dist/tools/editTool.js +3 -3
- package/dist/tools/readTool.js +2 -2
- package/dist/tools/writeTool.js +2 -2
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +1 -0
- package/dist/utils/openaiClient.d.ts.map +1 -1
- package/dist/utils/openaiClient.js +28 -7
- package/dist/utils/taskReminder.d.ts +2 -3
- package/dist/utils/taskReminder.d.ts.map +1 -1
- package/dist/utils/taskReminder.js +7 -18
- package/package.json +1 -1
- package/scripts/install_ripgrep.js +21 -1
- package/src/agent.ts +0 -5
- package/src/managers/MemoryRuleManager.ts +23 -0
- package/src/managers/aiManager.ts +473 -434
- package/src/managers/messageManager.ts +99 -82
- package/src/prompts/index.ts +33 -24
- package/src/services/aiService.ts +39 -12
- package/src/services/memory.ts +34 -16
- package/src/tools/agentTool.ts +24 -2
- package/src/tools/editTool.ts +3 -3
- package/src/tools/readTool.ts +2 -2
- package/src/tools/writeTool.ts +2 -2
- package/src/utils/containerSetup.ts +1 -0
- package/src/utils/openaiClient.ts +35 -7
- package/src/utils/taskReminder.ts +7 -19
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
|
@@ -165,20 +165,20 @@ export class AIManager {
|
|
|
165
165
|
planMgr.consumePlanEntryReminder();
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
-
async
|
|
168
|
+
async maybeGetTaskReminderText(toolNames) {
|
|
169
169
|
// Guard: no task tools available
|
|
170
170
|
if (!toolNames.has("TaskUpdate"))
|
|
171
|
-
return;
|
|
171
|
+
return null;
|
|
172
172
|
const internalMessages = this.messageManager.getMessages();
|
|
173
173
|
const turnCounts = getTaskReminderTurnCounts(internalMessages);
|
|
174
174
|
if (turnCounts.turnsSinceLastTaskManagement <
|
|
175
175
|
TASK_REMINDER_CONFIG.TURNS_SINCE_WRITE ||
|
|
176
176
|
turnCounts.turnsSinceLastReminder <
|
|
177
177
|
TASK_REMINDER_CONFIG.TURNS_BETWEEN_REMINDERS) {
|
|
178
|
-
return;
|
|
178
|
+
return null;
|
|
179
179
|
}
|
|
180
180
|
const tasks = await this.taskManager.listTasks();
|
|
181
|
-
maybeInjectTaskReminder(
|
|
181
|
+
return maybeInjectTaskReminder(turnCounts, tasks);
|
|
182
182
|
}
|
|
183
183
|
setIsLoading(isLoading) {
|
|
184
184
|
this.isLoading = isLoading;
|
|
@@ -500,362 +500,397 @@ export class AIManager {
|
|
|
500
500
|
}
|
|
501
501
|
async sendAIMessage(options = {}) {
|
|
502
502
|
const { recursionDepth = 0, model, allowedRules, maxTokens } = options;
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
turnSequence =
|
|
508
|
-
|
|
509
|
-
.
|
|
510
|
-
.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
this.setIsLoading(true);
|
|
521
|
-
if (allowedRules && allowedRules.length > 0) {
|
|
522
|
-
this.permissionManager?.addTemporaryRules(allowedRules);
|
|
503
|
+
let turnOffset = recursionDepth;
|
|
504
|
+
outer: while (true) {
|
|
505
|
+
let shouldRestart = false;
|
|
506
|
+
// OpenTelemetry: start interaction span for this turn (initial call only)
|
|
507
|
+
let turnSequence = 0;
|
|
508
|
+
if (turnOffset === 0) {
|
|
509
|
+
const messages = this.messageManager.getMessages();
|
|
510
|
+
turnSequence = messages.filter((m) => m.role === "user").length;
|
|
511
|
+
const lastUserMessage = [...messages]
|
|
512
|
+
.reverse()
|
|
513
|
+
.find((m) => m.role === "user");
|
|
514
|
+
const userPromptText = lastUserMessage?.blocks.find((b) => b.type === "text")?.content || "";
|
|
515
|
+
startInteractionSpan(userPromptText, turnSequence);
|
|
516
|
+
// Log user_prompt event
|
|
517
|
+
logOTelEvent("user_prompt", {
|
|
518
|
+
prompt_length: String(userPromptText.length),
|
|
519
|
+
}).catch(() => { }); // Non-blocking
|
|
523
520
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
521
|
+
// Set loading state early for the initial call, before any async work
|
|
522
|
+
if (turnOffset === 0) {
|
|
523
|
+
this.setIsLoading(true);
|
|
524
|
+
if (allowedRules && allowedRules.length > 0) {
|
|
525
|
+
this.permissionManager?.addTemporaryRules(allowedRules);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Scan for file mentions in the last user message to trigger conditional rules
|
|
529
|
+
if (turnOffset === 0) {
|
|
530
|
+
const messages = this.messageManager.getMessages();
|
|
531
|
+
const lastMessage = messages[messages.length - 1];
|
|
532
|
+
if (lastMessage && lastMessage.role === "user") {
|
|
533
|
+
for (const block of lastMessage.blocks) {
|
|
534
|
+
if (block.type === "text") {
|
|
535
|
+
const content = block.content;
|
|
536
|
+
const fileMentionRegex = /(?:^|\s)@([\w.\-/]+)/g;
|
|
537
|
+
let match;
|
|
538
|
+
while ((match = fileMentionRegex.exec(content)) !== null) {
|
|
539
|
+
const filePath = match[1];
|
|
540
|
+
this.messageManager.triggerFileRead(filePath);
|
|
541
|
+
}
|
|
538
542
|
}
|
|
539
543
|
}
|
|
540
544
|
}
|
|
541
545
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
this.abortController = abortController;
|
|
553
|
-
toolAbortController = new AbortController();
|
|
554
|
-
this.toolAbortController = toolAbortController;
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
// Reuse existing controllers for recursive calls
|
|
558
|
-
abortController = this.abortController;
|
|
559
|
-
toolAbortController = this.toolAbortController;
|
|
560
|
-
}
|
|
561
|
-
// Get current permission mode
|
|
562
|
-
const currentMode = this.permissionManager?.getCurrentEffectiveMode(this.getModelConfig().permissionMode);
|
|
563
|
-
// Add plan mode reminder as persistent meta message before getting messages
|
|
564
|
-
this.maybeAddPlanModeMessage(currentMode);
|
|
565
|
-
// Get recent message history
|
|
566
|
-
const rawMessages = this.messageManager.getMessages();
|
|
567
|
-
const recentMessages = convertMessagesForAPI(rawMessages);
|
|
568
|
-
try {
|
|
569
|
-
// Get combined memory content
|
|
570
|
-
const combinedMemory = await this.messageManager.getCombinedMemory();
|
|
571
|
-
// Track if assistant message has been created
|
|
572
|
-
let assistantMessageCreated = false;
|
|
573
|
-
logger?.debug("modelConfig in sendAIMessage", this.getModelConfig());
|
|
574
|
-
const toolsConfig = this.getFilteredToolsConfig();
|
|
575
|
-
const toolNames = new Set(toolsConfig.map((t) => t.function.name));
|
|
576
|
-
const filteredToolPlugins = this.toolManager
|
|
577
|
-
.getTools()
|
|
578
|
-
.filter((t) => toolNames.has(t.name));
|
|
579
|
-
let autoMemoryOptions;
|
|
580
|
-
if (this.getAutoMemoryEnabled()) {
|
|
581
|
-
const directory = this.memoryService.getAutoMemoryDirectory(this.getWorkdir());
|
|
582
|
-
const content = await this.memoryService.getAutoMemoryContent(this.getWorkdir());
|
|
583
|
-
autoMemoryOptions = { directory, content };
|
|
546
|
+
// Only create new AbortControllers for the initial call (turnOffset === 0)
|
|
547
|
+
// For restarts, reuse existing controllers to maintain abort signal
|
|
548
|
+
let abortController;
|
|
549
|
+
let toolAbortController;
|
|
550
|
+
if (turnOffset === 0) {
|
|
551
|
+
// Create new AbortControllers for initial call
|
|
552
|
+
abortController = new AbortController();
|
|
553
|
+
this.abortController = abortController;
|
|
554
|
+
toolAbortController = new AbortController();
|
|
555
|
+
this.toolAbortController = toolAbortController;
|
|
584
556
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
};
|
|
607
|
-
// Inject task reminder if needed (not persisted, regenerated each turn)
|
|
608
|
-
await this.maybeInjectTaskReminderIntoMessages(callAgentOptions.messages, toolNames);
|
|
609
|
-
// Add streaming callbacks only if streaming is enabled
|
|
610
|
-
if (this.stream) {
|
|
611
|
-
callAgentOptions.onContentUpdate = (content) => {
|
|
612
|
-
// Create assistant message on first chunk if not already created
|
|
613
|
-
if (!assistantMessageCreated) {
|
|
614
|
-
this.messageManager.addAssistantMessage();
|
|
615
|
-
assistantMessageCreated = true;
|
|
557
|
+
else {
|
|
558
|
+
// Reuse existing controllers
|
|
559
|
+
abortController = this.abortController;
|
|
560
|
+
toolAbortController = this.toolAbortController;
|
|
561
|
+
}
|
|
562
|
+
let turnDepth = turnOffset;
|
|
563
|
+
inner: while (true) {
|
|
564
|
+
try {
|
|
565
|
+
// Save session in each iteration to ensure message persistence
|
|
566
|
+
await this.messageManager.saveSession();
|
|
567
|
+
// Get current permission mode
|
|
568
|
+
const currentMode = this.permissionManager?.getCurrentEffectiveMode(this.getModelConfig().permissionMode);
|
|
569
|
+
// Add plan mode reminder as persistent meta message before getting messages
|
|
570
|
+
this.maybeAddPlanModeMessage(currentMode);
|
|
571
|
+
// Process conditional rules triggered by file reads (persist as meta messages)
|
|
572
|
+
const triggeredRules = this.messageManager.processTriggeredRules();
|
|
573
|
+
for (const rule of triggeredRules) {
|
|
574
|
+
this.messageManager.addUserMessage({
|
|
575
|
+
content: `<!-- rule: ${rule.id} -->\n<system-reminder>\nThe following rules from .wave/rules apply to your work. Be sure to adhere to these instructions.\n\n${rule.content}\n</system-reminder>`,
|
|
576
|
+
isMeta: true,
|
|
577
|
+
});
|
|
616
578
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
579
|
+
// Get recent message history
|
|
580
|
+
const rawMessages = this.messageManager.getMessages();
|
|
581
|
+
const recentMessages = convertMessagesForAPI(rawMessages);
|
|
582
|
+
// Track if assistant message has been created
|
|
583
|
+
let assistantMessageCreated = false;
|
|
584
|
+
logger?.debug("modelConfig in sendAIMessage", this.getModelConfig());
|
|
585
|
+
const toolsConfig = this.getFilteredToolsConfig();
|
|
586
|
+
const toolNames = new Set(toolsConfig.map((t) => t.function.name));
|
|
587
|
+
const filteredToolPlugins = this.toolManager
|
|
588
|
+
.getTools()
|
|
589
|
+
.filter((t) => toolNames.has(t.name));
|
|
590
|
+
let autoMemoryOptions;
|
|
591
|
+
if (this.getAutoMemoryEnabled()) {
|
|
592
|
+
const directory = this.memoryService.getAutoMemoryDirectory(this.getWorkdir());
|
|
593
|
+
const content = await this.memoryService.getAutoMemoryContent(this.getWorkdir());
|
|
594
|
+
autoMemoryOptions = { directory, content };
|
|
624
595
|
}
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
//
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
596
|
+
// Get memory for message-array injection (not system prompt)
|
|
597
|
+
const { prependContent } = await this.messageManager.getMemoryForInjection();
|
|
598
|
+
// Call AI service with streaming callbacks if enabled
|
|
599
|
+
const callAgentOptions = {
|
|
600
|
+
gatewayConfig: this.getGatewayConfig(),
|
|
601
|
+
modelConfig: this.getModelConfig(),
|
|
602
|
+
messages: recentMessages,
|
|
603
|
+
sessionId: this.messageManager.getSessionId(),
|
|
604
|
+
abortSignal: abortController.signal,
|
|
605
|
+
workdir: this.getWorkdir(), // Pass working directory
|
|
606
|
+
tools: toolsConfig, // Pass filtered tool configuration
|
|
607
|
+
model: model, // Use passed model
|
|
608
|
+
systemPrompt: buildSystemPrompt(this.systemPrompt, filteredToolPlugins, {
|
|
609
|
+
workdir: this.getWorkdir(),
|
|
610
|
+
originalWorkdir: this.getOriginalWorkdir(),
|
|
611
|
+
language: this.getLanguage(),
|
|
612
|
+
isSubagent: !!this.subagentType,
|
|
613
|
+
autoMemory: autoMemoryOptions,
|
|
614
|
+
}), // Pass custom system prompt
|
|
615
|
+
maxTokens: maxTokens, // Pass max tokens override
|
|
616
|
+
toolChoice: this.toolChoiceOverride, // Pass tool_choice override
|
|
617
|
+
};
|
|
618
|
+
// Prepend: AGENTS.md + user memory + unconditional rules as system-reminder
|
|
619
|
+
if (prependContent.trim()) {
|
|
620
|
+
callAgentOptions.messages.unshift({
|
|
621
|
+
role: "user",
|
|
622
|
+
content: `<system-reminder>\n${prependContent}\n</system-reminder>`,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
// Task reminder: persist as meta message (conditional rules already persisted above)
|
|
626
|
+
const taskReminderText = await this.maybeGetTaskReminderText(toolNames);
|
|
627
|
+
if (taskReminderText) {
|
|
628
|
+
this.messageManager.addUserMessage({
|
|
629
|
+
content: taskReminderText,
|
|
630
|
+
isMeta: true,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
// Add streaming callbacks only if streaming is enabled
|
|
634
|
+
if (this.stream) {
|
|
635
|
+
callAgentOptions.onContentUpdate = (content) => {
|
|
636
|
+
// Create assistant message on first chunk if not already created
|
|
637
|
+
if (!assistantMessageCreated) {
|
|
638
|
+
this.messageManager.addAssistantMessage();
|
|
639
|
+
assistantMessageCreated = true;
|
|
640
|
+
}
|
|
641
|
+
this.messageManager.updateCurrentMessageContent(content);
|
|
642
|
+
};
|
|
643
|
+
callAgentOptions.onToolUpdate = (toolCall) => {
|
|
644
|
+
// Create assistant message on first tool update if not already created
|
|
645
|
+
if (!assistantMessageCreated) {
|
|
646
|
+
this.messageManager.addAssistantMessage();
|
|
647
|
+
assistantMessageCreated = true;
|
|
648
|
+
}
|
|
649
|
+
// Use parametersChunk as compact param for better performance
|
|
650
|
+
// No need to extract params or generate compact params during streaming
|
|
651
|
+
// Update tool block with streaming parameters using parametersChunk as compact param
|
|
652
|
+
this.messageManager.updateToolBlock({
|
|
653
|
+
id: toolCall.id,
|
|
654
|
+
name: toolCall.name,
|
|
655
|
+
parameters: toolCall.parameters,
|
|
656
|
+
parametersChunk: toolCall.parametersChunk,
|
|
657
|
+
stage: toolCall.stage || "streaming", // Default to streaming if stage not provided
|
|
658
|
+
});
|
|
659
|
+
};
|
|
660
|
+
callAgentOptions.onReasoningUpdate = (reasoning) => {
|
|
661
|
+
// Create assistant message on first reasoning update if not already created
|
|
662
|
+
if (!assistantMessageCreated) {
|
|
663
|
+
this.messageManager.addAssistantMessage();
|
|
664
|
+
assistantMessageCreated = true;
|
|
665
|
+
}
|
|
666
|
+
this.messageManager.updateCurrentMessageReasoning(reasoning);
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
startLLMRequestSpan(model || this.getModelConfig().model || "");
|
|
670
|
+
const result = await aiService.callAgent(callAgentOptions);
|
|
671
|
+
// End LLM span with usage data
|
|
672
|
+
endLLMRequestSpan({
|
|
673
|
+
model: model || this.getModelConfig().model || "",
|
|
674
|
+
success: true,
|
|
675
|
+
hasToolCall: !!(result.tool_calls && result.tool_calls.length > 0),
|
|
634
676
|
});
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
//
|
|
638
|
-
if (!
|
|
677
|
+
const createdByStreaming = assistantMessageCreated;
|
|
678
|
+
// For non-streaming mode, create assistant message after callAgent returns
|
|
679
|
+
// Also create if streaming mode but no streaming callbacks were called (e.g., when content comes directly in result)
|
|
680
|
+
if (!this.stream ||
|
|
681
|
+
(!assistantMessageCreated &&
|
|
682
|
+
(result.content || result.tool_calls || result.reasoning_content))) {
|
|
639
683
|
this.messageManager.addAssistantMessage();
|
|
640
684
|
assistantMessageCreated = true;
|
|
641
685
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
endLLMRequestSpan({
|
|
649
|
-
model: model || this.getModelConfig().model || "",
|
|
650
|
-
success: true,
|
|
651
|
-
hasToolCall: !!(result.tool_calls && result.tool_calls.length > 0),
|
|
652
|
-
});
|
|
653
|
-
const createdByStreaming = assistantMessageCreated;
|
|
654
|
-
// For non-streaming mode, create assistant message after callAgent returns
|
|
655
|
-
// Also create if streaming mode but no streaming callbacks were called (e.g., when content comes directly in result)
|
|
656
|
-
if (!this.stream ||
|
|
657
|
-
(!assistantMessageCreated &&
|
|
658
|
-
(result.content || result.tool_calls || result.reasoning_content))) {
|
|
659
|
-
this.messageManager.addAssistantMessage();
|
|
660
|
-
assistantMessageCreated = true;
|
|
661
|
-
}
|
|
662
|
-
// Log finish reason and response headers if available
|
|
663
|
-
if (result.finish_reason) {
|
|
664
|
-
// Log warning headers when finish reason is length
|
|
665
|
-
if (result.finish_reason === "length") {
|
|
666
|
-
logger?.warn("AI response truncated due to length limit. Response headers:", result.response_headers);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
if (result.additionalFields &&
|
|
670
|
-
Object.keys(result.additionalFields).length > 0) {
|
|
671
|
-
this.messageManager.mergeAssistantAdditionalFields(result.additionalFields);
|
|
672
|
-
}
|
|
673
|
-
// Handle result reasoning content from non-streaming mode
|
|
674
|
-
if (result.reasoning_content && !createdByStreaming) {
|
|
675
|
-
this.messageManager.updateCurrentMessageReasoning(result.reasoning_content);
|
|
676
|
-
}
|
|
677
|
-
// Handle result content from non-streaming mode
|
|
678
|
-
if (result.content && !createdByStreaming) {
|
|
679
|
-
this.messageManager.updateCurrentMessageContent(result.content);
|
|
680
|
-
}
|
|
681
|
-
// Handle usage tracking for agent operations
|
|
682
|
-
let usage;
|
|
683
|
-
if (result.usage) {
|
|
684
|
-
usage = {
|
|
685
|
-
prompt_tokens: result.usage.prompt_tokens,
|
|
686
|
-
completion_tokens: result.usage.completion_tokens,
|
|
687
|
-
total_tokens: result.usage.total_tokens,
|
|
688
|
-
model: model || this.getModelConfig().model,
|
|
689
|
-
operation_type: "agent",
|
|
690
|
-
// Preserve cache fields if present
|
|
691
|
-
...(result.usage.cache_read_input_tokens !== undefined && {
|
|
692
|
-
cache_read_input_tokens: result.usage.cache_read_input_tokens,
|
|
693
|
-
}),
|
|
694
|
-
...(result.usage.cache_creation_input_tokens !== undefined && {
|
|
695
|
-
cache_creation_input_tokens: result.usage.cache_creation_input_tokens,
|
|
696
|
-
}),
|
|
697
|
-
...(result.usage.cache_creation && {
|
|
698
|
-
cache_creation: result.usage.cache_creation,
|
|
699
|
-
}),
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
// Set usage on the assistant message if available
|
|
703
|
-
if (usage) {
|
|
704
|
-
const messages = this.messageManager.getMessages();
|
|
705
|
-
const lastMessage = messages[messages.length - 1];
|
|
706
|
-
if (lastMessage && lastMessage.role === "assistant") {
|
|
707
|
-
lastMessage.usage = usage;
|
|
708
|
-
this.messageManager.setMessages(messages);
|
|
709
|
-
}
|
|
710
|
-
// Notify Agent to add to usage tracking
|
|
711
|
-
if (this.callbacks?.onUsageAdded) {
|
|
712
|
-
this.callbacks.onUsageAdded(usage);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
// Collect tool calls for processing
|
|
716
|
-
const toolCalls = [];
|
|
717
|
-
if (result.tool_calls) {
|
|
718
|
-
for (const toolCall of result.tool_calls) {
|
|
719
|
-
if (toolCall.type === "function") {
|
|
720
|
-
toolCalls.push(toolCall);
|
|
686
|
+
// Log finish reason and response headers if available
|
|
687
|
+
if (result.finish_reason) {
|
|
688
|
+
// Log warning headers when finish reason is length
|
|
689
|
+
if (result.finish_reason === "length") {
|
|
690
|
+
logger?.warn("AI response truncated due to length limit. Response headers:", result.response_headers);
|
|
691
|
+
}
|
|
721
692
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
// Partition tool calls into batches: consecutive concurrency-safe
|
|
726
|
-
// tools run in parallel, non-safe tools (Edit, Write, MCP) run one
|
|
727
|
-
// at a time to prevent read-modify-write races on the same file.
|
|
728
|
-
const batches = [];
|
|
729
|
-
for (const call of toolCalls) {
|
|
730
|
-
const toolName = call.function?.name || "";
|
|
731
|
-
const safe = this.toolManager.isConcurrencySafe(toolName);
|
|
732
|
-
const lastBatch = batches[batches.length - 1];
|
|
733
|
-
if (lastBatch && lastBatch.safe && safe) {
|
|
734
|
-
lastBatch.calls.push(call);
|
|
693
|
+
if (result.additionalFields &&
|
|
694
|
+
Object.keys(result.additionalFields).length > 0) {
|
|
695
|
+
this.messageManager.mergeAssistantAdditionalFields(result.additionalFields);
|
|
735
696
|
}
|
|
736
|
-
|
|
737
|
-
|
|
697
|
+
// Handle result reasoning content from non-streaming mode
|
|
698
|
+
if (result.reasoning_content && !createdByStreaming) {
|
|
699
|
+
this.messageManager.updateCurrentMessageReasoning(result.reasoning_content);
|
|
738
700
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
if (abortController.signal.aborted ||
|
|
743
|
-
toolAbortController.signal.aborted) {
|
|
744
|
-
break;
|
|
701
|
+
// Handle result content from non-streaming mode
|
|
702
|
+
if (result.content && !createdByStreaming) {
|
|
703
|
+
this.messageManager.updateCurrentMessageContent(result.content);
|
|
745
704
|
}
|
|
746
|
-
|
|
747
|
-
|
|
705
|
+
// Handle usage tracking for agent operations
|
|
706
|
+
let usage;
|
|
707
|
+
if (result.usage) {
|
|
708
|
+
usage = {
|
|
709
|
+
prompt_tokens: result.usage.prompt_tokens,
|
|
710
|
+
completion_tokens: result.usage.completion_tokens,
|
|
711
|
+
total_tokens: result.usage.total_tokens,
|
|
712
|
+
model: model || this.getModelConfig().model,
|
|
713
|
+
operation_type: "agent",
|
|
714
|
+
// Preserve cache fields if present
|
|
715
|
+
...(result.usage.cache_read_input_tokens !== undefined && {
|
|
716
|
+
cache_read_input_tokens: result.usage.cache_read_input_tokens,
|
|
717
|
+
}),
|
|
718
|
+
...(result.usage.cache_creation_input_tokens !== undefined && {
|
|
719
|
+
cache_creation_input_tokens: result.usage.cache_creation_input_tokens,
|
|
720
|
+
}),
|
|
721
|
+
...(result.usage.cache_creation && {
|
|
722
|
+
cache_creation: result.usage.cache_creation,
|
|
723
|
+
}),
|
|
724
|
+
};
|
|
748
725
|
}
|
|
749
|
-
|
|
750
|
-
|
|
726
|
+
// Set usage on the assistant message if available
|
|
727
|
+
if (usage) {
|
|
728
|
+
const messages = this.messageManager.getMessages();
|
|
729
|
+
const lastMessage = messages[messages.length - 1];
|
|
730
|
+
if (lastMessage && lastMessage.role === "assistant") {
|
|
731
|
+
lastMessage.usage = usage;
|
|
732
|
+
this.messageManager.setMessages(messages);
|
|
733
|
+
}
|
|
734
|
+
// Notify Agent to add to usage tracking
|
|
735
|
+
if (this.callbacks?.onUsageAdded) {
|
|
736
|
+
this.callbacks.onUsageAdded(usage);
|
|
737
|
+
}
|
|
751
738
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if (toolCalls.length > 0 || result.finish_reason === "length") {
|
|
760
|
-
// Check maxTurns limit before recursing
|
|
761
|
-
if (this.maxTurns && recursionDepth + 1 >= this.maxTurns) {
|
|
762
|
-
logger?.debug(`Max turns (${this.maxTurns}) reached, stopping recursion.`);
|
|
763
|
-
}
|
|
764
|
-
else {
|
|
765
|
-
// Record committed snapshots to message history
|
|
766
|
-
if (this.reversionManager) {
|
|
767
|
-
const snapshots = this.reversionManager.getAndClearCommittedSnapshots();
|
|
768
|
-
if (snapshots.length > 0) {
|
|
769
|
-
this.messageManager.addFileHistoryBlock(snapshots);
|
|
739
|
+
// Collect tool calls for processing
|
|
740
|
+
const toolCalls = [];
|
|
741
|
+
if (result.tool_calls) {
|
|
742
|
+
for (const toolCall of result.tool_calls) {
|
|
743
|
+
if (toolCall.type === "function") {
|
|
744
|
+
toolCalls.push(toolCall);
|
|
745
|
+
}
|
|
770
746
|
}
|
|
771
747
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
748
|
+
if (toolCalls.length > 0) {
|
|
749
|
+
// Partition tool calls into batches: consecutive concurrency-safe
|
|
750
|
+
// tools run in parallel, non-safe tools (Edit, Write, MCP) run one
|
|
751
|
+
// at a time to prevent read-modify-write races on the same file.
|
|
752
|
+
const batches = [];
|
|
753
|
+
for (const call of toolCalls) {
|
|
754
|
+
const toolName = call.function?.name || "";
|
|
755
|
+
const safe = this.toolManager.isConcurrencySafe(toolName);
|
|
756
|
+
const lastBatch = batches[batches.length - 1];
|
|
757
|
+
if (lastBatch && lastBatch.safe && safe) {
|
|
758
|
+
lastBatch.calls.push(call);
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
batches.push({ calls: [call], safe });
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// Execute batches sequentially; within a safe batch, tools run in parallel
|
|
765
|
+
for (const batch of batches) {
|
|
766
|
+
if (abortController.signal.aborted ||
|
|
767
|
+
toolAbortController.signal.aborted) {
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
if (batch.calls.length === 1) {
|
|
771
|
+
await this.executeToolCall(batch.calls[0], abortController, toolAbortController, result.finish_reason);
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
await Promise.all(batch.calls.map((call) => this.executeToolCall(call, abortController, toolAbortController, result.finish_reason)));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
782
777
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
778
|
+
// Handle token statistics and message compaction
|
|
779
|
+
await this.handleTokenUsageAndCompaction(result.usage, abortController);
|
|
780
|
+
// Finalize text/reasoning blocks for the final response (no tools)
|
|
781
|
+
this.messageManager.finalizeStreamingBlocks();
|
|
782
|
+
// Check if there are tool operations or response was truncated, if so automatically initiate next AI service call
|
|
783
|
+
if (toolCalls.length > 0 || result.finish_reason === "length") {
|
|
784
|
+
// Check maxTurns limit before continuing
|
|
785
|
+
if (this.maxTurns && turnDepth + 1 >= this.maxTurns) {
|
|
786
|
+
logger?.debug(`Max turns (${this.maxTurns}) reached, stopping.`);
|
|
790
787
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
for (let i = messages.length - 2; i >= 0; i--) {
|
|
798
|
-
const msg = messages[i];
|
|
799
|
-
if (msg.role === "assistant" &&
|
|
800
|
-
msg.blocks.some((b) => b.type === "tool")) {
|
|
801
|
-
previousAssistantWithTools = msg;
|
|
802
|
-
break;
|
|
788
|
+
else {
|
|
789
|
+
// Record committed snapshots to message history
|
|
790
|
+
if (this.reversionManager) {
|
|
791
|
+
const snapshots = this.reversionManager.getAndClearCommittedSnapshots();
|
|
792
|
+
if (snapshots.length > 0) {
|
|
793
|
+
this.messageManager.addFileHistoryBlock(snapshots);
|
|
803
794
|
}
|
|
804
795
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
796
|
+
// Check interruption status
|
|
797
|
+
const isCurrentlyAborted = abortController.signal.aborted ||
|
|
798
|
+
toolAbortController.signal.aborted;
|
|
799
|
+
// Check if all tools were manually backgrounded
|
|
800
|
+
const lastMessage = this.messageManager.getMessages()[this.messageManager.getMessages().length - 1];
|
|
801
|
+
const toolBlocks = lastMessage?.blocks.filter((block) => block.type === "tool") || [];
|
|
802
|
+
const hasBackgrounded = toolBlocks.length > 0 &&
|
|
803
|
+
toolBlocks.some((block) => block.isManuallyBackgrounded);
|
|
804
|
+
if (hasBackgrounded) {
|
|
805
|
+
logger?.info("Some tools were manually backgrounded, stopping.");
|
|
806
|
+
}
|
|
807
|
+
else if (!isCurrentlyAborted) {
|
|
808
|
+
// If response was truncated, add a hidden continuation message
|
|
809
|
+
if (result.finish_reason === "length") {
|
|
810
|
+
this.messageManager.addUserMessage({
|
|
811
|
+
content: "Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.",
|
|
812
|
+
isMeta: true,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
// Duplicate Tool Call Detection
|
|
816
|
+
if (toolCalls.length > 0) {
|
|
817
|
+
const messages = this.messageManager.getMessages();
|
|
818
|
+
// Find the most recent assistant message BEFORE the current one that has tool blocks
|
|
819
|
+
// The current assistant message is messages[messages.length - 1]
|
|
820
|
+
let previousAssistantWithTools;
|
|
821
|
+
for (let i = messages.length - 2; i >= 0; i--) {
|
|
822
|
+
const msg = messages[i];
|
|
823
|
+
if (msg.role === "assistant" &&
|
|
824
|
+
msg.blocks.some((b) => b.type === "tool")) {
|
|
825
|
+
previousAssistantWithTools = msg;
|
|
826
|
+
break;
|
|
823
827
|
}
|
|
824
828
|
}
|
|
829
|
+
if (previousAssistantWithTools) {
|
|
830
|
+
const previousToolBlocks = previousAssistantWithTools.blocks.filter((b) => b.type === "tool");
|
|
831
|
+
for (const currentToolCall of toolCalls) {
|
|
832
|
+
const currentName = currentToolCall.function?.name;
|
|
833
|
+
const currentArgs = currentToolCall.function?.arguments;
|
|
834
|
+
const isDuplicate = previousToolBlocks.some((prevBlock) => prevBlock.name === currentName &&
|
|
835
|
+
prevBlock.parameters === currentArgs);
|
|
836
|
+
if (isDuplicate && currentName) {
|
|
837
|
+
const toolId = currentToolCall.id;
|
|
838
|
+
const lastMessage = messages[messages.length - 1];
|
|
839
|
+
const toolBlock = lastMessage.blocks.find((b) => b.type === "tool" && b.id === toolId);
|
|
840
|
+
if (toolBlock) {
|
|
841
|
+
const warning = `\n\nNote: You just called this tool with the same arguments in the previous turn. Please ensure you are not in a loop and consider if you need to change your approach.`;
|
|
842
|
+
this.messageManager.updateToolBlock({
|
|
843
|
+
id: toolId,
|
|
844
|
+
result: (toolBlock.result || "") + warning,
|
|
845
|
+
stage: "end",
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
// Yield to the event loop so macrotasks (abort timers,
|
|
853
|
+
// signals) can be processed between turns. Without this,
|
|
854
|
+
// mocked async operations (microtasks) would starve the
|
|
855
|
+
// event loop and abort timers would never fire.
|
|
856
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
857
|
+
// Re-check abort status after yielding — the signal may
|
|
858
|
+
// have fired during the setImmediate gap.
|
|
859
|
+
if (abortController.signal.aborted ||
|
|
860
|
+
toolAbortController.signal.aborted) {
|
|
861
|
+
break inner;
|
|
825
862
|
}
|
|
863
|
+
turnDepth++;
|
|
864
|
+
continue inner;
|
|
826
865
|
}
|
|
827
866
|
}
|
|
828
|
-
// Recursively call AI service, increment recursion depth, and pass same configuration
|
|
829
|
-
await this.sendAIMessage({
|
|
830
|
-
recursionDepth: recursionDepth + 1,
|
|
831
|
-
model,
|
|
832
|
-
allowedRules,
|
|
833
|
-
maxTokens,
|
|
834
|
-
});
|
|
835
867
|
}
|
|
868
|
+
// No tool calls (or stop conditions) → inner loop done
|
|
869
|
+
break inner;
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
// End LLM span with error
|
|
873
|
+
endLLMRequestSpan({
|
|
874
|
+
model: model || this.getModelConfig().model || "",
|
|
875
|
+
success: false,
|
|
876
|
+
error: error instanceof Error ? error.message : String(error),
|
|
877
|
+
});
|
|
878
|
+
// Log error event
|
|
879
|
+
logOTelEvent("error", {
|
|
880
|
+
error_type: error instanceof Error ? error.constructor.name : "Unknown",
|
|
881
|
+
message: error instanceof Error ? error.message : String(error),
|
|
882
|
+
}).catch(() => { }); // Non-blocking
|
|
883
|
+
this.messageManager.addErrorBlock(error instanceof Error ? error.message : "Unknown error occurred");
|
|
884
|
+
// Exit inner loop on error
|
|
885
|
+
break inner;
|
|
836
886
|
}
|
|
837
887
|
}
|
|
838
|
-
|
|
839
|
-
catch (error) {
|
|
840
|
-
// End LLM span with error
|
|
841
|
-
endLLMRequestSpan({
|
|
842
|
-
model: model || this.getModelConfig().model || "",
|
|
843
|
-
success: false,
|
|
844
|
-
error: error instanceof Error ? error.message : String(error),
|
|
845
|
-
});
|
|
846
|
-
// Log error event
|
|
847
|
-
logOTelEvent("error", {
|
|
848
|
-
error_type: error instanceof Error ? error.constructor.name : "Unknown",
|
|
849
|
-
message: error instanceof Error ? error.message : String(error),
|
|
850
|
-
}).catch(() => { }); // Non-blocking
|
|
851
|
-
this.messageManager.addErrorBlock(error instanceof Error ? error.message : "Unknown error occurred");
|
|
852
|
-
}
|
|
853
|
-
finally {
|
|
888
|
+
// Finally-equivalent (runs once per outer iteration):
|
|
854
889
|
// Only execute cleanup and hooks for the initial call
|
|
855
|
-
if (
|
|
890
|
+
if (turnOffset === 0) {
|
|
856
891
|
// OpenTelemetry: end interaction span
|
|
857
892
|
endInteractionSpan();
|
|
858
|
-
// Save session in each
|
|
893
|
+
// Save session in each iteration to ensure message persistence
|
|
859
894
|
await this.messageManager.saveSession();
|
|
860
895
|
// Set loading to false first
|
|
861
896
|
this.setIsLoading(false);
|
|
@@ -877,13 +912,9 @@ export class AIManager {
|
|
|
877
912
|
});
|
|
878
913
|
}
|
|
879
914
|
}
|
|
880
|
-
//
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
model,
|
|
884
|
-
allowedRules,
|
|
885
|
-
maxTokens,
|
|
886
|
-
});
|
|
915
|
+
// Restart outer loop to process the notifications
|
|
916
|
+
shouldRestart = true;
|
|
917
|
+
turnOffset = 0;
|
|
887
918
|
}
|
|
888
919
|
else {
|
|
889
920
|
// Clear temporary rules
|
|
@@ -943,12 +974,9 @@ export class AIManager {
|
|
|
943
974
|
// Keep loading state active to prevent UI flicker
|
|
944
975
|
this.setIsLoading(true);
|
|
945
976
|
goalContinuing = true;
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
allowedRules,
|
|
950
|
-
maxTokens,
|
|
951
|
-
});
|
|
977
|
+
// Restart outer loop to continue goal pursuit
|
|
978
|
+
shouldRestart = true;
|
|
979
|
+
turnOffset = 0;
|
|
952
980
|
}
|
|
953
981
|
}
|
|
954
982
|
}
|
|
@@ -963,18 +991,15 @@ export class AIManager {
|
|
|
963
991
|
if (shouldContinue) {
|
|
964
992
|
logger?.info(`${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`);
|
|
965
993
|
// Restart the conversation to let AI fix the issues
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
recursionDepth: 0,
|
|
969
|
-
model,
|
|
970
|
-
allowedRules,
|
|
971
|
-
maxTokens,
|
|
972
|
-
});
|
|
994
|
+
shouldRestart = true;
|
|
995
|
+
turnOffset = 0;
|
|
973
996
|
}
|
|
974
997
|
}
|
|
975
998
|
}
|
|
976
999
|
}
|
|
977
1000
|
}
|
|
1001
|
+
if (!shouldRestart)
|
|
1002
|
+
break outer;
|
|
978
1003
|
}
|
|
979
1004
|
}
|
|
980
1005
|
/**
|