@4onstudios/iris-agent 0.1.0
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/LICENSE +21 -0
- package/README.md +690 -0
- package/dist/api/acp/acpServer.d.ts +23 -0
- package/dist/api/acp/acpServer.js +814 -0
- package/dist/api/acp/index.d.ts +2 -0
- package/dist/api/acp/index.js +2 -0
- package/dist/api/acp/irisClient.d.ts +58 -0
- package/dist/api/acp/irisClient.js +214 -0
- package/dist/api/agent.d.ts +14 -0
- package/dist/api/agent.js +4171 -0
- package/dist/api/backendService.d.ts +4 -0
- package/dist/api/backendService.js +14 -0
- package/dist/api/core/agent/host/AgentContract.d.ts +82 -0
- package/dist/api/core/agent/host/AgentContract.js +1 -0
- package/dist/api/core/agent/host/AgentRegistry.d.ts +24 -0
- package/dist/api/core/agent/host/AgentRegistry.js +125 -0
- package/dist/api/core/agent/host/defaultRegistry.d.ts +15 -0
- package/dist/api/core/agent/host/defaultRegistry.js +46 -0
- package/dist/api/core/agent/host/externalAgentLifecycle.d.ts +65 -0
- package/dist/api/core/agent/host/externalAgentLifecycle.js +117 -0
- package/dist/api/core/agent/host/hostSessionManager.d.ts +111 -0
- package/dist/api/core/agent/host/hostSessionManager.js +352 -0
- package/dist/api/core/agent/host/index.d.ts +5 -0
- package/dist/api/core/agent/host/index.js +5 -0
- package/dist/api/core/agent/index.d.ts +87 -0
- package/dist/api/core/agent/index.js +1024 -0
- package/dist/api/core/agent/publicContracts.d.ts +35 -0
- package/dist/api/core/agent/publicContracts.js +103 -0
- package/dist/api/core/agent/tools/applyDiff.d.ts +51 -0
- package/dist/api/core/agent/tools/applyDiff.js +131 -0
- package/dist/api/core/agent/tools/backgroundTasks.d.ts +240 -0
- package/dist/api/core/agent/tools/backgroundTasks.js +313 -0
- package/dist/api/core/agent/tools/createDirectory.d.ts +43 -0
- package/dist/api/core/agent/tools/createDirectory.js +77 -0
- package/dist/api/core/agent/tools/deleteFile.d.ts +37 -0
- package/dist/api/core/agent/tools/deleteFile.js +66 -0
- package/dist/api/core/agent/tools/editFile.d.ts +73 -0
- package/dist/api/core/agent/tools/editFile.js +173 -0
- package/dist/api/core/agent/tools/executeCommand.d.ts +59 -0
- package/dist/api/core/agent/tools/executeCommand.js +250 -0
- package/dist/api/core/agent/tools/fileContent.d.ts +54 -0
- package/dist/api/core/agent/tools/fileContent.js +227 -0
- package/dist/api/core/agent/tools/findDefinition.d.ts +72 -0
- package/dist/api/core/agent/tools/findDefinition.js +109 -0
- package/dist/api/core/agent/tools/findReferences.d.ts +86 -0
- package/dist/api/core/agent/tools/findReferences.js +135 -0
- package/dist/api/core/agent/tools/formatDocument.d.ts +67 -0
- package/dist/api/core/agent/tools/formatDocument.js +90 -0
- package/dist/api/core/agent/tools/getCodeActions.d.ts +77 -0
- package/dist/api/core/agent/tools/getCodeActions.js +97 -0
- package/dist/api/core/agent/tools/getCodeCompletion.d.ts +77 -0
- package/dist/api/core/agent/tools/getCodeCompletion.js +174 -0
- package/dist/api/core/agent/tools/getCodeContext.d.ts +14 -0
- package/dist/api/core/agent/tools/getCodeContext.js +282 -0
- package/dist/api/core/agent/tools/getSignatureHelp.d.ts +61 -0
- package/dist/api/core/agent/tools/getSignatureHelp.js +85 -0
- package/dist/api/core/agent/tools/getSymbols.d.ts +50 -0
- package/dist/api/core/agent/tools/getSymbols.js +164 -0
- package/dist/api/core/agent/tools/getSymbolsLSP.d.ts +58 -0
- package/dist/api/core/agent/tools/getSymbolsLSP.js +147 -0
- package/dist/api/core/agent/tools/getTypeInfo.d.ts +70 -0
- package/dist/api/core/agent/tools/getTypeInfo.js +84 -0
- package/dist/api/core/agent/tools/getWorkspaceInfo.d.ts +80 -0
- package/dist/api/core/agent/tools/getWorkspaceInfo.js +281 -0
- package/dist/api/core/agent/tools/getWorkspaceSymbols.d.ts +53 -0
- package/dist/api/core/agent/tools/getWorkspaceSymbols.js +97 -0
- package/dist/api/core/agent/tools/grepSearch.d.ts +100 -0
- package/dist/api/core/agent/tools/grepSearch.js +211 -0
- package/dist/api/core/agent/tools/languageModelToolsIntegration.d.ts +13 -0
- package/dist/api/core/agent/tools/languageModelToolsIntegration.js +22 -0
- package/dist/api/core/agent/tools/listDirectory.d.ts +66 -0
- package/dist/api/core/agent/tools/listDirectory.js +161 -0
- package/dist/api/core/agent/tools/mcpTools.d.ts +48 -0
- package/dist/api/core/agent/tools/mcpTools.js +442 -0
- package/dist/api/core/agent/tools/queryKnowledgeGraph.d.ts +120 -0
- package/dist/api/core/agent/tools/queryKnowledgeGraph.js +306 -0
- package/dist/api/core/agent/tools/readFile.d.ts +92 -0
- package/dist/api/core/agent/tools/readFile.js +337 -0
- package/dist/api/core/agent/tools/renameFile.d.ts +49 -0
- package/dist/api/core/agent/tools/renameFile.js +86 -0
- package/dist/api/core/agent/tools/renameSymbol.d.ts +70 -0
- package/dist/api/core/agent/tools/renameSymbol.js +106 -0
- package/dist/api/core/agent/tools/runTerminalCommand.d.ts +72 -0
- package/dist/api/core/agent/tools/runTerminalCommand.js +69 -0
- package/dist/api/core/agent/tools/searchFiles.d.ts +82 -0
- package/dist/api/core/agent/tools/searchFiles.js +280 -0
- package/dist/api/core/agent/tools/terminalAutoApprove.d.ts +3 -0
- package/dist/api/core/agent/tools/terminalAutoApprove.js +676 -0
- package/dist/api/core/agent/tools/treeTraversal.d.ts +60 -0
- package/dist/api/core/agent/tools/treeTraversal.js +217 -0
- package/dist/api/core/agent/tools/webSearch.d.ts +37 -0
- package/dist/api/core/agent/tools/webSearch.js +80 -0
- package/dist/api/core/agent/tools/writeFile.d.ts +59 -0
- package/dist/api/core/agent/tools/writeFile.js +97 -0
- package/dist/api/core/agent/utils/capturedWorkspaceMutationBridge.d.ts +12 -0
- package/dist/api/core/agent/utils/capturedWorkspaceMutationBridge.js +67 -0
- package/dist/api/core/agent/utils/diffUtils.d.ts +14 -0
- package/dist/api/core/agent/utils/diffUtils.js +39 -0
- package/dist/api/core/agent/utils/environmentSnapshot.d.ts +42 -0
- package/dist/api/core/agent/utils/environmentSnapshot.js +213 -0
- package/dist/api/core/agent/utils/errorRecovery.d.ts +49 -0
- package/dist/api/core/agent/utils/errorRecovery.js +229 -0
- package/dist/api/core/agent/utils/multimodalTokenLimiter.d.ts +8 -0
- package/dist/api/core/agent/utils/multimodalTokenLimiter.js +238 -0
- package/dist/api/core/agent/utils/openRouterModelSettings.d.ts +12 -0
- package/dist/api/core/agent/utils/openRouterModelSettings.js +34 -0
- package/dist/api/core/agent/utils/pathRecovery.d.ts +1 -0
- package/dist/api/core/agent/utils/pathRecovery.js +29 -0
- package/dist/api/core/agent/utils/repoMapIndex.d.ts +3 -0
- package/dist/api/core/agent/utils/repoMapIndex.js +197 -0
- package/dist/api/core/agent/utils/skillsDiscovery.d.ts +10 -0
- package/dist/api/core/agent/utils/skillsDiscovery.js +50 -0
- package/dist/api/core/agent/utils/toolCallBudget.d.ts +9 -0
- package/dist/api/core/agent/utils/toolCallBudget.js +55 -0
- package/dist/api/core/agent/utils/toolLifecycle.d.ts +30 -0
- package/dist/api/core/agent/utils/toolLifecycle.js +361 -0
- package/dist/api/core/agent/utils/toolResultSafetyProcessor.d.ts +12 -0
- package/dist/api/core/agent/utils/toolResultSafetyProcessor.js +57 -0
- package/dist/api/core/agent/utils/workspaceMutationBridge.d.ts +13 -0
- package/dist/api/core/agent/utils/workspaceMutationBridge.js +149 -0
- package/dist/api/core/agent/utils/workspacePathGuard.d.ts +7 -0
- package/dist/api/core/agent/utils/workspacePathGuard.js +79 -0
- package/dist/api/core/containers/chat/toolResultSerialization.d.ts +8 -0
- package/dist/api/core/containers/chat/toolResultSerialization.js +71 -0
- package/dist/api/core/library/BrowserManager.d.ts +11 -0
- package/dist/api/core/library/BrowserManager.js +66 -0
- package/dist/api/core/library/desktopWorkspace.d.ts +1 -0
- package/dist/api/core/library/desktopWorkspace.js +3 -0
- package/dist/api/core/library/extensionManager.d.ts +10 -0
- package/dist/api/core/library/extensionManager.js +27 -0
- package/dist/api/core/library/knowledgeGraph.d.ts +268 -0
- package/dist/api/core/library/knowledgeGraph.js +989 -0
- package/dist/api/core/library/languageModelTools.d.ts +141 -0
- package/dist/api/core/library/languageModelTools.js +207 -0
- package/dist/api/core/library/localRuntime.d.ts +23 -0
- package/dist/api/core/library/localRuntime.js +144 -0
- package/dist/api/core/library/lsp/coreLsp.d.ts +367 -0
- package/dist/api/core/library/lsp/coreLsp.js +2076 -0
- package/dist/api/core/library/lsp/protocol.d.ts +25 -0
- package/dist/api/core/library/lsp/protocol.js +1 -0
- package/dist/api/core/library/lsp/serverManager.d.ts +37 -0
- package/dist/api/core/library/lsp/serverManager.js +427 -0
- package/dist/api/core/library/mcpServerProvider.d.ts +69 -0
- package/dist/api/core/library/mcpServerProvider.js +152 -0
- package/dist/api/core/library/mcpSettings.d.ts +20 -0
- package/dist/api/core/library/mcpSettings.js +131 -0
- package/dist/api/core/library/patternMatcher.d.ts +66 -0
- package/dist/api/core/library/patternMatcher.js +284 -0
- package/dist/api/core/library/regexEscape.d.ts +4 -0
- package/dist/api/core/library/regexEscape.js +4 -0
- package/dist/api/core/library/runtimeEventBus.d.ts +27 -0
- package/dist/api/core/library/runtimeEventBus.js +78 -0
- package/dist/api/core/library/safetyMiddleware.d.ts +98 -0
- package/dist/api/core/library/safetyMiddleware.js +215 -0
- package/dist/api/core/library/tauri.d.ts +71 -0
- package/dist/api/core/library/tauri.js +222 -0
- package/dist/api/core/library/tauriImport.d.ts +5 -0
- package/dist/api/core/library/tauriImport.js +7 -0
- package/dist/api/core/library/terminalAutoApproveSettings.d.ts +32 -0
- package/dist/api/core/library/terminalAutoApproveSettings.js +529 -0
- package/dist/api/core/library/workspaceIdentity.d.ts +10 -0
- package/dist/api/core/library/workspaceIdentity.js +49 -0
- package/dist/api/core/library/workspaceSummary.d.ts +19 -0
- package/dist/api/core/library/workspaceSummary.js +35 -0
- package/dist/api/core/skills/agent-customization/SKILL.md +27 -0
- package/dist/api/core/skills/bug-fix/SKILL.md +21 -0
- package/dist/api/core/skills/create-pr/SKILL.md +32 -0
- package/dist/api/core/skills/dev-server/SKILL.md +20 -0
- package/dist/api/core/skills/fix-suggestions/SKILL.md +21 -0
- package/dist/api/core/skills/github-search/SKILL.md +21 -0
- package/dist/api/core/skills/github-summary/SKILL.md +25 -0
- package/dist/api/core/skills/integration-tests/SKILL.md +29 -0
- package/dist/api/core/skills/pr-comments/SKILL.md +23 -0
- package/dist/api/core/skills/project-setup/SKILL.md +20 -0
- package/dist/api/core/skills/search-integration/SKILL.md +19 -0
- package/dist/api/core/skills/search-results/SKILL.md +19 -0
- package/dist/api/core/skills/typescript-upgrade/SKILL.md +21 -0
- package/dist/api/data/runStore.d.ts +46 -0
- package/dist/api/data/runStore.js +244 -0
- package/dist/api/helpers/agentUtils.d.ts +11 -0
- package/dist/api/helpers/agentUtils.js +23 -0
- package/dist/api/helpers/modelTokenLimits.d.ts +2 -0
- package/dist/api/helpers/modelTokenLimits.js +158 -0
- package/dist/api/helpers/observationalMemory.d.ts +10 -0
- package/dist/api/helpers/observationalMemory.js +36 -0
- package/dist/api/helpers/promptBudget.d.ts +30 -0
- package/dist/api/helpers/promptBudget.js +216 -0
- package/dist/api/helpers/resolveImageMessageParts.d.ts +9 -0
- package/dist/api/helpers/resolveImageMessageParts.js +140 -0
- package/dist/api/helpers/slashCommands.d.ts +31 -0
- package/dist/api/helpers/slashCommands.js +129 -0
- package/dist/api/helpers/tokenUsage.d.ts +19 -0
- package/dist/api/helpers/tokenUsage.js +81 -0
- package/dist/api/routes/fileRoutes.d.ts +2 -0
- package/dist/api/routes/fileRoutes.js +108 -0
- package/dist/api/routes/lspDocumentRoutes.d.ts +4 -0
- package/dist/api/routes/lspDocumentRoutes.js +84 -0
- package/dist/api/routes/lspHierarchyRoutes.d.ts +4 -0
- package/dist/api/routes/lspHierarchyRoutes.js +122 -0
- package/dist/api/routes/lspPositionRoutes.d.ts +4 -0
- package/dist/api/routes/lspPositionRoutes.js +263 -0
- package/dist/api/routes/lspQueryRoutes.d.ts +4 -0
- package/dist/api/routes/lspQueryRoutes.js +679 -0
- package/dist/api/routes/lspResolveRoutes.d.ts +4 -0
- package/dist/api/routes/lspResolveRoutes.js +143 -0
- package/dist/api/routes/lspSemanticDocumentRoutes.d.ts +4 -0
- package/dist/api/routes/lspSemanticDocumentRoutes.js +98 -0
- package/dist/api/routes/semanticRoutes.d.ts +2 -0
- package/dist/api/routes/semanticRoutes.js +126 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +160 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +38 -0
- package/package.json +100 -0
|
@@ -0,0 +1,4171 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import fs from "fs/promises";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { truncateEnvironmentResponse } from "./helpers/agentUtils.js";
|
|
7
|
+
import { getEnvironmentSnapshot, formatSnapshotAsMarkdown, } from "./core/agent/utils/environmentSnapshot.js";
|
|
8
|
+
import { sanitizeMcpServers, toStableMcpFingerprint, } from "./core/library/mcpSettings.js";
|
|
9
|
+
import { getTerminalAutoApproveRulesFingerprint, sanitizeTerminalAutoApproveRules, } from "./core/library/terminalAutoApproveSettings.js";
|
|
10
|
+
import { executeMcpToolByKey, listMcpServerTools, } from "./core/agent/tools/mcpTools.js";
|
|
11
|
+
import { initializeLanguageModelTools, getAvailableTools, } from "./core/agent/tools/languageModelToolsIntegration.js";
|
|
12
|
+
import { executeCommand } from "./core/agent/tools/executeCommand.js";
|
|
13
|
+
import { countUniqueToolCalls, getToolCallSignature, normalizeToolLifecycle, resolveToolExecutionStatus, } from "./core/agent/utils/toolLifecycle.js";
|
|
14
|
+
import { redactToolResult } from "./core/agent/utils/toolResultSafetyProcessor.js";
|
|
15
|
+
import { serializeToolResultsForContinuation } from "./core/containers/chat/toolResultSerialization.js";
|
|
16
|
+
import { createDefaultAgentRegistry, ExternalAgentLifecycleManager, HostSessionManager, } from "./core/agent/host/index.js";
|
|
17
|
+
import { truncateText, buildPromptWithinTokenBudget, resolveModelInputTokenLimit, resolveModelSupportsVision, } from "./helpers/promptBudget.js";
|
|
18
|
+
import { isLikelyImageFile, resolveImageMessageParts, } from "./helpers/resolveImageMessageParts.js";
|
|
19
|
+
import { parseSlashCommandRequest, getSlashCommandDescriptors, executeRegisteredSlashCommand, isSlashCommandsFeatureEnabled, } from "./helpers/slashCommands.js";
|
|
20
|
+
import { normalizeTokenUsage, mergeTokenUsage, extractTokenUsageFromChunkPayload, logTokenUsageSource, buildTokenUsageDebug, } from "./helpers/tokenUsage.js";
|
|
21
|
+
import { sanitizeObservationalMemorySettings, } from "./helpers/observationalMemory.js";
|
|
22
|
+
import { registerFileRoutes } from "./routes/fileRoutes.js";
|
|
23
|
+
import { registerLspDocumentRoutes } from "./routes/lspDocumentRoutes.js";
|
|
24
|
+
import { registerLspHierarchyRoutes } from "./routes/lspHierarchyRoutes.js";
|
|
25
|
+
import { registerLspPositionRoutes } from "./routes/lspPositionRoutes.js";
|
|
26
|
+
import { registerLspQueryRoutes } from "./routes/lspQueryRoutes.js";
|
|
27
|
+
import { registerLspResolveRoutes } from "./routes/lspResolveRoutes.js";
|
|
28
|
+
import { registerLspSemanticDocumentRoutes } from "./routes/lspSemanticDocumentRoutes.js";
|
|
29
|
+
import { registerSemanticRoutes } from "./routes/semanticRoutes.js";
|
|
30
|
+
import { asRunLifecycleState, deleteRunDataBatch, getRunSnapshot, isRunCancellationRequested, isSafeRunId, listRunEvents, requestRunCancellation, safePersistRunLifecycleEvent, } from "./data/runStore.js";
|
|
31
|
+
const router = express.Router();
|
|
32
|
+
const activeChatSessionTurns = new Set();
|
|
33
|
+
/**
|
|
34
|
+
* Safely extract error message from any thrown value.
|
|
35
|
+
* Handles Error objects, strings, null/undefined, and other values.
|
|
36
|
+
*/
|
|
37
|
+
const getErrorMessage = (err) => {
|
|
38
|
+
if (err instanceof Error) {
|
|
39
|
+
return err.message;
|
|
40
|
+
}
|
|
41
|
+
if (typeof err === "string") {
|
|
42
|
+
return err;
|
|
43
|
+
}
|
|
44
|
+
if (err === null || err === undefined) {
|
|
45
|
+
return String(err);
|
|
46
|
+
}
|
|
47
|
+
return String(err);
|
|
48
|
+
};
|
|
49
|
+
const sanitizeToolArgsForWorkspace = (toolName, args, isWebWorkspace) => {
|
|
50
|
+
const normalizedArgs = args || {};
|
|
51
|
+
if (!isWebWorkspace || toolName !== "getWorkspaceInfo") {
|
|
52
|
+
return normalizedArgs;
|
|
53
|
+
}
|
|
54
|
+
const sanitizedArgs = { ...normalizedArgs };
|
|
55
|
+
delete sanitizedArgs.workspacePath;
|
|
56
|
+
return sanitizedArgs;
|
|
57
|
+
};
|
|
58
|
+
const isRecordValue = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
59
|
+
const resolveToolResultArgs = (toolName, toolCallId, explicitArgs, argsByCallId, anonymousCallArgs) => {
|
|
60
|
+
const explicitRecord = isRecordValue(explicitArgs)
|
|
61
|
+
? explicitArgs
|
|
62
|
+
: undefined;
|
|
63
|
+
if (toolCallId) {
|
|
64
|
+
const mappedArgs = argsByCallId.get(toolCallId);
|
|
65
|
+
if (explicitRecord &&
|
|
66
|
+
(!mappedArgs ||
|
|
67
|
+
getToolCallSignature(toolName, explicitRecord) !==
|
|
68
|
+
getToolCallSignature(toolName, mappedArgs))) {
|
|
69
|
+
return explicitRecord;
|
|
70
|
+
}
|
|
71
|
+
return mappedArgs || explicitRecord || {};
|
|
72
|
+
}
|
|
73
|
+
if (explicitRecord) {
|
|
74
|
+
const explicitSignature = getToolCallSignature(toolName, explicitRecord);
|
|
75
|
+
const matchingAnonymousCall = anonymousCallArgs.find((call) => !call.consumed &&
|
|
76
|
+
call.toolName === toolName &&
|
|
77
|
+
getToolCallSignature(call.toolName, call.args || {}) ===
|
|
78
|
+
explicitSignature);
|
|
79
|
+
if (matchingAnonymousCall) {
|
|
80
|
+
matchingAnonymousCall.consumed = true;
|
|
81
|
+
return matchingAnonymousCall.args || {};
|
|
82
|
+
}
|
|
83
|
+
return explicitRecord;
|
|
84
|
+
}
|
|
85
|
+
const matchingAnonymousCall = anonymousCallArgs.find((call) => !call.consumed && call.toolName === toolName);
|
|
86
|
+
if (!matchingAnonymousCall)
|
|
87
|
+
return {};
|
|
88
|
+
matchingAnonymousCall.consumed = true;
|
|
89
|
+
return matchingAnonymousCall.args || {};
|
|
90
|
+
};
|
|
91
|
+
const unwrapProcessedToolResult = (result) => {
|
|
92
|
+
let current = result;
|
|
93
|
+
while (current &&
|
|
94
|
+
typeof current === "object" &&
|
|
95
|
+
"value" in current &&
|
|
96
|
+
typeof current.type === "string") {
|
|
97
|
+
current = current.value;
|
|
98
|
+
}
|
|
99
|
+
return current;
|
|
100
|
+
};
|
|
101
|
+
const extractProcessedToolResults = (steps) => {
|
|
102
|
+
const argsByCallId = new Map();
|
|
103
|
+
const anonymousCallArgs = [];
|
|
104
|
+
const results = [];
|
|
105
|
+
for (const step of steps) {
|
|
106
|
+
for (const item of step.content || []) {
|
|
107
|
+
if (item.type !== "tool-call" || !item.toolName)
|
|
108
|
+
continue;
|
|
109
|
+
const args = item.args || item.input || {};
|
|
110
|
+
if (item.toolCallId) {
|
|
111
|
+
argsByCallId.set(item.toolCallId, args);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
anonymousCallArgs.push({
|
|
115
|
+
toolName: item.toolName,
|
|
116
|
+
args,
|
|
117
|
+
consumed: false,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const item of step.content || []) {
|
|
122
|
+
if (item.type !== "tool-result" || !item.toolName)
|
|
123
|
+
continue;
|
|
124
|
+
const result = unwrapProcessedToolResult(item.result ?? item.output ?? item.content ?? item.data);
|
|
125
|
+
results.push({
|
|
126
|
+
name: item.toolName,
|
|
127
|
+
args: resolveToolResultArgs(item.toolName, item.toolCallId, item.args || item.input, argsByCallId, anonymousCallArgs),
|
|
128
|
+
result: redactToolResult(result).result,
|
|
129
|
+
toolCallId: item.toolCallId,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return results;
|
|
134
|
+
};
|
|
135
|
+
const buildOrderedPersistedToolActions = (steps, pendingToolCalls, executedToolResults, sanitizeArgs) => {
|
|
136
|
+
const pendingEntries = pendingToolCalls.map((action) => ({
|
|
137
|
+
action,
|
|
138
|
+
consumed: false,
|
|
139
|
+
}));
|
|
140
|
+
const executedEntries = executedToolResults.map((action) => ({
|
|
141
|
+
action,
|
|
142
|
+
consumed: false,
|
|
143
|
+
}));
|
|
144
|
+
const actions = [];
|
|
145
|
+
const takeById = (entries, toolCallId) => {
|
|
146
|
+
if (!toolCallId)
|
|
147
|
+
return undefined;
|
|
148
|
+
const entry = entries.find((candidate) => !candidate.consumed && candidate.action.toolCallId === toolCallId);
|
|
149
|
+
if (entry)
|
|
150
|
+
entry.consumed = true;
|
|
151
|
+
return entry;
|
|
152
|
+
};
|
|
153
|
+
const takeBySignature = (entries, toolName, args) => {
|
|
154
|
+
const signature = getToolCallSignature(toolName, args);
|
|
155
|
+
const entry = entries.find((candidate) => !candidate.consumed &&
|
|
156
|
+
getToolCallSignature(candidate.action.name, candidate.action.args || {}) === signature);
|
|
157
|
+
if (entry)
|
|
158
|
+
entry.consumed = true;
|
|
159
|
+
return entry;
|
|
160
|
+
};
|
|
161
|
+
const takeByName = (entries, toolName) => {
|
|
162
|
+
const entry = entries.find((candidate) => !candidate.consumed && candidate.action.name === toolName);
|
|
163
|
+
if (entry)
|
|
164
|
+
entry.consumed = true;
|
|
165
|
+
return entry;
|
|
166
|
+
};
|
|
167
|
+
for (const step of steps || []) {
|
|
168
|
+
for (const item of step.content || []) {
|
|
169
|
+
if (!item.toolName)
|
|
170
|
+
continue;
|
|
171
|
+
if (item.type === "tool-call") {
|
|
172
|
+
const args = sanitizeArgs(item.toolName, item.input || item.args);
|
|
173
|
+
const entry = takeById(pendingEntries, item.toolCallId) ||
|
|
174
|
+
takeBySignature(pendingEntries, item.toolName, args);
|
|
175
|
+
if (!entry)
|
|
176
|
+
continue;
|
|
177
|
+
actions.push({
|
|
178
|
+
eventType: "tool_call",
|
|
179
|
+
...entry.action,
|
|
180
|
+
});
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (item.type === "tool-result") {
|
|
184
|
+
const explicitArgs = item.args || item.input;
|
|
185
|
+
const hasExplicitArgs = isRecordValue(explicitArgs);
|
|
186
|
+
const args = sanitizeArgs(item.toolName, explicitArgs);
|
|
187
|
+
const entry = takeById(executedEntries, item.toolCallId) ||
|
|
188
|
+
(hasExplicitArgs
|
|
189
|
+
? takeBySignature(executedEntries, item.toolName, args)
|
|
190
|
+
: undefined) ||
|
|
191
|
+
takeByName(executedEntries, item.toolName);
|
|
192
|
+
if (!entry)
|
|
193
|
+
continue;
|
|
194
|
+
actions.push({
|
|
195
|
+
eventType: "tool_result",
|
|
196
|
+
...entry.action,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const entry of pendingEntries) {
|
|
202
|
+
if (!entry.consumed) {
|
|
203
|
+
actions.push({ eventType: "tool_call", ...entry.action });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const entry of executedEntries) {
|
|
207
|
+
if (!entry.consumed) {
|
|
208
|
+
actions.push({ eventType: "tool_result", ...entry.action });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return actions;
|
|
212
|
+
};
|
|
213
|
+
const reconcileToolLifecycleSnapshots = (streamedPending, snapshotPending, streamedResults, snapshotResults) => {
|
|
214
|
+
const reconcile = (streamed, snapshot) => {
|
|
215
|
+
const merged = [...streamed];
|
|
216
|
+
const claimedStreamedIndexes = new Set();
|
|
217
|
+
const streamedById = new Map(streamed
|
|
218
|
+
.map((entry, index) => entry.toolCallId
|
|
219
|
+
? [
|
|
220
|
+
`${entry.toolCallId}:${getToolCallSignature(entry.name, entry.args || {})}`,
|
|
221
|
+
index,
|
|
222
|
+
]
|
|
223
|
+
: undefined)
|
|
224
|
+
.filter((entry) => entry !== undefined));
|
|
225
|
+
for (const entry of snapshot) {
|
|
226
|
+
if (entry.toolCallId) {
|
|
227
|
+
const matchingIndex = streamedById.get(`${entry.toolCallId}:${getToolCallSignature(entry.name, entry.args || {})}`);
|
|
228
|
+
if (matchingIndex !== undefined) {
|
|
229
|
+
merged[matchingIndex] = entry;
|
|
230
|
+
claimedStreamedIndexes.add(matchingIndex);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const matchingIndex = streamed.findIndex((streamedEntry, index) => !claimedStreamedIndexes.has(index) &&
|
|
235
|
+
(!streamedEntry.toolCallId || !entry.toolCallId) &&
|
|
236
|
+
getToolCallSignature(streamedEntry.name, streamedEntry.args || {}) ===
|
|
237
|
+
getToolCallSignature(entry.name, entry.args || {}));
|
|
238
|
+
if (matchingIndex === -1) {
|
|
239
|
+
merged.push(entry);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
claimedStreamedIndexes.add(matchingIndex);
|
|
243
|
+
merged[matchingIndex] = {
|
|
244
|
+
...merged[matchingIndex],
|
|
245
|
+
...entry,
|
|
246
|
+
toolCallId: entry.toolCallId ?? merged[matchingIndex].toolCallId,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return merged;
|
|
250
|
+
};
|
|
251
|
+
return {
|
|
252
|
+
pendingToolCalls: reconcile(streamedPending, snapshotPending),
|
|
253
|
+
executedToolResults: reconcile(streamedResults, snapshotResults),
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
// Cache environment snapshots per workspace to avoid regenerating
|
|
257
|
+
const envSnapshotCache = new Map();
|
|
258
|
+
// Cache agent instances keyed by "modelId:workspacePath" to avoid repeating
|
|
259
|
+
// workspace.init on every request. Keyed by "modelId:workspacePath" only —
|
|
260
|
+
// enabled-skill filtering is applied per-request (injected into contextInfo)
|
|
261
|
+
// so it never inflates the key space.
|
|
262
|
+
//
|
|
263
|
+
// ── Cache contract ────────────────────────────────────────────────────────────
|
|
264
|
+
// A cached agent MUST be safe to reuse across distinct requests that share the
|
|
265
|
+
// same (modelId, workspacePath) tuple. This means:
|
|
266
|
+
//
|
|
267
|
+
// SAFE to capture at construction time (part of the key or truly static):
|
|
268
|
+
// • modelId – part of the cache key
|
|
269
|
+
// • workspacePath – part of the cache key; baked into tool path-resolvers
|
|
270
|
+
// • Mastra Workspace instance (LocalFilesystem + BM25 index)
|
|
271
|
+
// • Wrapped tool implementations (path already resolved via closure)
|
|
272
|
+
// • Static system-prompt instructions
|
|
273
|
+
//
|
|
274
|
+
// MUST NOT be captured at construction time (varies per-request/per-user):
|
|
275
|
+
// • Enabled-skills list → inject via contextInfo prefix
|
|
276
|
+
// • Auth tokens / API keys → read from env at request time, not constructor
|
|
277
|
+
// • Per-session or per-user identity
|
|
278
|
+
// • Anything derived from the HTTP request (headers, body, caller context)
|
|
279
|
+
//
|
|
280
|
+
// If future code needs to vary any of the "MUST NOT" items, inject it through
|
|
281
|
+
// the `contextInfo` argument of agent.generate() rather than expanding the
|
|
282
|
+
// constructor or the cache key.
|
|
283
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
284
|
+
const AGENT_CACHE_MAX_SIZE = 50;
|
|
285
|
+
const MAX_INLINE_FILE_CONTENT_CHARS = 6000;
|
|
286
|
+
const MAX_CONVERSATION_MESSAGES = 12;
|
|
287
|
+
const MAX_CONVERSATION_MESSAGE_CHARS = 4000;
|
|
288
|
+
const MAX_CONVERSATION_MESSAGE_TOKENS = 1200;
|
|
289
|
+
const MAX_CONTEXT_FILES = 8;
|
|
290
|
+
const DEFAULT_PROMPT_TOKEN_BUDGET_RATIO = 0.55;
|
|
291
|
+
const MIN_PROMPT_TOKEN_BUDGET = 4096;
|
|
292
|
+
const PROMPT_TOKEN_RESERVE = 6144;
|
|
293
|
+
const agentCache = new Map();
|
|
294
|
+
let agentCoreModulePromise;
|
|
295
|
+
const loadAgentCoreModule = () => {
|
|
296
|
+
if (!agentCoreModulePromise) {
|
|
297
|
+
agentCoreModulePromise = import("./core/agent/index.js");
|
|
298
|
+
}
|
|
299
|
+
return agentCoreModulePromise;
|
|
300
|
+
};
|
|
301
|
+
const REMOTE_CHAT_SESSIONS_DIR = path.join(os.homedir(), ".iris", "chat-sessions");
|
|
302
|
+
const EDIT_TOOL_NAMES = new Set(["writeFile", "editFile", "applyDiff"]);
|
|
303
|
+
const toRunStopReasonFromReflection = (reflectionStopReason) => {
|
|
304
|
+
if (reflectionStopReason === "resolved")
|
|
305
|
+
return "completed";
|
|
306
|
+
if (reflectionStopReason === "no_progress")
|
|
307
|
+
return "no_progress";
|
|
308
|
+
if (reflectionStopReason === "max_attempts")
|
|
309
|
+
return "max_attempts";
|
|
310
|
+
return "none";
|
|
311
|
+
};
|
|
312
|
+
const parsePositiveIntQuery = (value, fallback, min, max) => {
|
|
313
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
314
|
+
if (typeof first !== "string") {
|
|
315
|
+
return fallback;
|
|
316
|
+
}
|
|
317
|
+
const parsed = Number.parseInt(first, 10);
|
|
318
|
+
if (!Number.isFinite(parsed)) {
|
|
319
|
+
return fallback;
|
|
320
|
+
}
|
|
321
|
+
return Math.max(min, Math.min(max, parsed));
|
|
322
|
+
};
|
|
323
|
+
const isSafeSessionId = (sessionId) => /^[A-Za-z0-9._:-]+$/.test(sessionId);
|
|
324
|
+
const getRemoteChatSessionPath = (sessionId) => path.join(REMOTE_CHAT_SESSIONS_DIR, `${sessionId}.json`);
|
|
325
|
+
const extractRunIdsFromRemoteSession = (session) => {
|
|
326
|
+
if (!session || !Array.isArray(session.messages)) {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
const collected = new Set();
|
|
330
|
+
const visit = (value, depth) => {
|
|
331
|
+
if (depth > 12 || value === null || value === undefined) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (typeof value === "string") {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (Array.isArray(value)) {
|
|
338
|
+
value.forEach((item) => visit(item, depth + 1));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (typeof value !== "object") {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const record = value;
|
|
345
|
+
const candidateRunId = typeof record.runId === "string"
|
|
346
|
+
? record.runId.trim()
|
|
347
|
+
: typeof record.run_id === "string"
|
|
348
|
+
? record.run_id.trim()
|
|
349
|
+
: "";
|
|
350
|
+
if (candidateRunId && isSafeRunId(candidateRunId)) {
|
|
351
|
+
collected.add(candidateRunId);
|
|
352
|
+
}
|
|
353
|
+
Object.values(record).forEach((entry) => visit(entry, depth + 1));
|
|
354
|
+
};
|
|
355
|
+
session.messages.forEach((message) => visit(message, 0));
|
|
356
|
+
return Array.from(collected);
|
|
357
|
+
};
|
|
358
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
359
|
+
setTimeout(resolve, ms);
|
|
360
|
+
});
|
|
361
|
+
const isSynthesisOnlyContinuationMessage = (message) => {
|
|
362
|
+
if (!message || message.role !== "user") {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
return (message.continuationType === "tool_results" ||
|
|
366
|
+
message.continuationType === "final_synthesis");
|
|
367
|
+
};
|
|
368
|
+
const normalizeEnabledSkills = (input) => {
|
|
369
|
+
if (!Array.isArray(input)) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
const normalized = input
|
|
373
|
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
374
|
+
.filter((value) => /^[a-zA-Z0-9._-]{1,80}$/.test(value));
|
|
375
|
+
return Array.from(new Set(normalized));
|
|
376
|
+
};
|
|
377
|
+
const isRetryableModelError = (error) => {
|
|
378
|
+
const err = error;
|
|
379
|
+
if (err?.isRetryable)
|
|
380
|
+
return true;
|
|
381
|
+
const statusCode = err?.statusCode;
|
|
382
|
+
const code = err?.data?.error?.code;
|
|
383
|
+
const type = err?.data?.error?.type;
|
|
384
|
+
return Boolean((typeof statusCode === "number" && statusCode >= 500) ||
|
|
385
|
+
code === "server_error" ||
|
|
386
|
+
type === "server_error");
|
|
387
|
+
};
|
|
388
|
+
const serializeToolResultsForConversation = (toolResults) => {
|
|
389
|
+
if (!Array.isArray(toolResults) || toolResults.length === 0) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
const body = serializeToolResultsForContinuation(toolResults, "unknown_tool");
|
|
393
|
+
return `Tool execution results:\n\n${body}`;
|
|
394
|
+
};
|
|
395
|
+
const isSlashCommandExecutionAllowed = (req, isTauri, enableSlashCommands) => {
|
|
396
|
+
if (!enableSlashCommands)
|
|
397
|
+
return false;
|
|
398
|
+
if (!isSlashCommandsFeatureEnabled())
|
|
399
|
+
return false;
|
|
400
|
+
return Boolean(isTauri && hasDesktopAuth(req));
|
|
401
|
+
};
|
|
402
|
+
router.get("/slash-commands", (req, res) => {
|
|
403
|
+
const enableSlashCommandsRaw = String(req.query.enableSlashCommands || "true");
|
|
404
|
+
const clientEnabled = !["0", "false", "off", "no"].includes(enableSlashCommandsRaw.trim().toLowerCase());
|
|
405
|
+
if (!clientEnabled || !isSlashCommandsFeatureEnabled()) {
|
|
406
|
+
return res.json({
|
|
407
|
+
success: true,
|
|
408
|
+
enabled: false,
|
|
409
|
+
commands: [],
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return res.json({
|
|
413
|
+
success: true,
|
|
414
|
+
enabled: true,
|
|
415
|
+
commands: getSlashCommandDescriptors(),
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
const generateWithRetry = async (agent, prompt, generateOptions, abortSignal, maxAttempts = 3) => {
|
|
419
|
+
let lastError;
|
|
420
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
421
|
+
if (abortSignal.aborted) {
|
|
422
|
+
throw new Error("Run cancelled");
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
return await agent.generate(prompt, generateOptions);
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
lastError = error;
|
|
429
|
+
if (abortSignal.aborted) {
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
const shouldRetry = isRetryableModelError(error) && attempt < maxAttempts;
|
|
433
|
+
if (!shouldRetry) {
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
const backoffMs = 600 * 2 ** (attempt - 1);
|
|
437
|
+
console.warn(`[agent] transient model error (attempt ${attempt}/${maxAttempts}); retrying in ${backoffMs}ms`);
|
|
438
|
+
await sleep(backoffMs);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
throw lastError;
|
|
442
|
+
};
|
|
443
|
+
const asGenerateResultFromTurn = (turnResult) => {
|
|
444
|
+
if (turnResult.raw && typeof turnResult.raw === "object") {
|
|
445
|
+
return turnResult.raw;
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
text: turnResult.text,
|
|
449
|
+
};
|
|
450
|
+
};
|
|
451
|
+
const createGeneratedAgentRuntimeAdapter = (agent) => {
|
|
452
|
+
const turnAbortControllers = new Map();
|
|
453
|
+
const createTurnAbortController = (sessionId) => {
|
|
454
|
+
const controller = new AbortController();
|
|
455
|
+
turnAbortControllers.set(sessionId, controller);
|
|
456
|
+
return controller;
|
|
457
|
+
};
|
|
458
|
+
const clearTurnAbortController = (sessionId, controller) => {
|
|
459
|
+
const active = turnAbortControllers.get(sessionId);
|
|
460
|
+
if (active === controller) {
|
|
461
|
+
turnAbortControllers.delete(sessionId);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
return {
|
|
465
|
+
descriptor: {
|
|
466
|
+
id: "generated-agent-runtime",
|
|
467
|
+
name: "Generated Agent Runtime",
|
|
468
|
+
version: "0.1.0",
|
|
469
|
+
source: "external",
|
|
470
|
+
},
|
|
471
|
+
async startSession(context) {
|
|
472
|
+
return {
|
|
473
|
+
sessionId: context.sessionId,
|
|
474
|
+
agentId: "generated-agent-runtime",
|
|
475
|
+
createdAt: Date.now(),
|
|
476
|
+
};
|
|
477
|
+
},
|
|
478
|
+
async runTurn(request) {
|
|
479
|
+
const metadata = request.metadata && typeof request.metadata === "object"
|
|
480
|
+
? request.metadata
|
|
481
|
+
: {};
|
|
482
|
+
const modelInput = metadata.modelInput !== undefined
|
|
483
|
+
? metadata.modelInput
|
|
484
|
+
: request.input;
|
|
485
|
+
const generateOptions = metadata.generateOptions && typeof metadata.generateOptions === "object"
|
|
486
|
+
? metadata.generateOptions
|
|
487
|
+
: {};
|
|
488
|
+
const requestContext = generateOptions.requestContext;
|
|
489
|
+
requestContext?.set?.("onPreToolUse", request.onPreToolUse);
|
|
490
|
+
const turnAbortController = createTurnAbortController(request.sessionId);
|
|
491
|
+
const generateOptionsWithAbort = {
|
|
492
|
+
...generateOptions,
|
|
493
|
+
abortSignal: turnAbortController.signal,
|
|
494
|
+
};
|
|
495
|
+
let result;
|
|
496
|
+
try {
|
|
497
|
+
result = await agent.generate(modelInput, generateOptionsWithAbort);
|
|
498
|
+
}
|
|
499
|
+
finally {
|
|
500
|
+
clearTurnAbortController(request.sessionId, turnAbortController);
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
text: result.text || "",
|
|
504
|
+
toolCalls: Array.isArray(result.toolCalls)
|
|
505
|
+
? result.toolCalls
|
|
506
|
+
: undefined,
|
|
507
|
+
raw: result,
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
async runTurnStream(request) {
|
|
511
|
+
const metadata = request.metadata && typeof request.metadata === "object"
|
|
512
|
+
? request.metadata
|
|
513
|
+
: {};
|
|
514
|
+
const modelInput = metadata.modelInput !== undefined
|
|
515
|
+
? metadata.modelInput
|
|
516
|
+
: request.input;
|
|
517
|
+
const generateOptions = metadata.generateOptions && typeof metadata.generateOptions === "object"
|
|
518
|
+
? metadata.generateOptions
|
|
519
|
+
: {};
|
|
520
|
+
const requestContext = generateOptions.requestContext;
|
|
521
|
+
requestContext?.set?.("onPreToolUse", request.onPreToolUse);
|
|
522
|
+
if (typeof agent.stream !== "function") {
|
|
523
|
+
throw new Error("Generated agent runtime does not support streaming");
|
|
524
|
+
}
|
|
525
|
+
const turnAbortController = createTurnAbortController(request.sessionId);
|
|
526
|
+
const generateOptionsWithAbort = {
|
|
527
|
+
...generateOptions,
|
|
528
|
+
abortSignal: turnAbortController.signal,
|
|
529
|
+
};
|
|
530
|
+
let rawStreamResult;
|
|
531
|
+
try {
|
|
532
|
+
rawStreamResult = await agent.stream(modelInput, generateOptionsWithAbort);
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
clearTurnAbortController(request.sessionId, turnAbortController);
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
const tee = rawStreamResult.fullStream.tee;
|
|
539
|
+
const [hostEventSource, transportSource] = typeof tee === "function"
|
|
540
|
+
? tee.call(rawStreamResult.fullStream)
|
|
541
|
+
: [
|
|
542
|
+
new ReadableStream({
|
|
543
|
+
start(controller) {
|
|
544
|
+
controller.close();
|
|
545
|
+
},
|
|
546
|
+
}),
|
|
547
|
+
rawStreamResult.fullStream,
|
|
548
|
+
];
|
|
549
|
+
const mappedStream = hostEventSource.pipeThrough(new TransformStream({
|
|
550
|
+
transform(chunk, controller) {
|
|
551
|
+
if (!chunk?.type)
|
|
552
|
+
return;
|
|
553
|
+
if (chunk.type === "text-delta" || chunk.type === "reasoning-delta") {
|
|
554
|
+
const text = String(chunk.payload?.text || "");
|
|
555
|
+
if (text)
|
|
556
|
+
controller.enqueue({ type: "text-delta", text });
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (chunk.type === "tool-call") {
|
|
560
|
+
const toolName = typeof chunk.payload?.toolName === "string"
|
|
561
|
+
? chunk.payload.toolName
|
|
562
|
+
: "unknown_tool";
|
|
563
|
+
controller.enqueue({
|
|
564
|
+
type: "tool-call",
|
|
565
|
+
name: toolName,
|
|
566
|
+
args: chunk.payload?.args && typeof chunk.payload.args === "object"
|
|
567
|
+
? chunk.payload.args
|
|
568
|
+
: undefined,
|
|
569
|
+
});
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (chunk.type === "tool-result") {
|
|
573
|
+
const toolName = typeof chunk.payload?.toolName === "string"
|
|
574
|
+
? chunk.payload.toolName
|
|
575
|
+
: "unknown_tool";
|
|
576
|
+
controller.enqueue({
|
|
577
|
+
type: "tool-result",
|
|
578
|
+
name: toolName,
|
|
579
|
+
result: chunk.payload?.result ??
|
|
580
|
+
chunk.payload?.output ??
|
|
581
|
+
chunk.payload?.content ??
|
|
582
|
+
chunk.payload?.data,
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (chunk.type === "tool-suspended" || chunk.type === "tool_suspended") {
|
|
587
|
+
controller.enqueue({
|
|
588
|
+
type: "approval-required",
|
|
589
|
+
reason: "Tool suspended and awaiting user input",
|
|
590
|
+
payload: chunk.payload,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
flush(controller) {
|
|
595
|
+
controller.enqueue({ type: "done" });
|
|
596
|
+
},
|
|
597
|
+
}));
|
|
598
|
+
const transportStreamResult = {
|
|
599
|
+
...rawStreamResult,
|
|
600
|
+
fullStream: transportSource,
|
|
601
|
+
};
|
|
602
|
+
const result = {
|
|
603
|
+
stream: mappedStream,
|
|
604
|
+
getFinalResult: async () => {
|
|
605
|
+
const [text, toolCalls, usage, steps] = await Promise.all([
|
|
606
|
+
rawStreamResult.text,
|
|
607
|
+
rawStreamResult.toolCalls,
|
|
608
|
+
Promise.resolve(rawStreamResult.usage).catch(() => undefined),
|
|
609
|
+
Promise.resolve(rawStreamResult.steps).catch(() => undefined),
|
|
610
|
+
]);
|
|
611
|
+
return {
|
|
612
|
+
text: text || "",
|
|
613
|
+
toolCalls: Array.isArray(toolCalls)
|
|
614
|
+
? toolCalls
|
|
615
|
+
.filter((item) => typeof item?.toolName === "string")
|
|
616
|
+
.map((item) => ({
|
|
617
|
+
name: item.toolName,
|
|
618
|
+
args: item.args || {},
|
|
619
|
+
toolCallId: item.toolCallId,
|
|
620
|
+
}))
|
|
621
|
+
: undefined,
|
|
622
|
+
raw: {
|
|
623
|
+
text,
|
|
624
|
+
toolCalls,
|
|
625
|
+
usage,
|
|
626
|
+
steps,
|
|
627
|
+
streamResult: rawStreamResult,
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
},
|
|
631
|
+
rawStreamResult: transportStreamResult,
|
|
632
|
+
};
|
|
633
|
+
return {
|
|
634
|
+
...result,
|
|
635
|
+
getFinalResult: async () => {
|
|
636
|
+
try {
|
|
637
|
+
return await result.getFinalResult();
|
|
638
|
+
}
|
|
639
|
+
finally {
|
|
640
|
+
clearTurnAbortController(request.sessionId, turnAbortController);
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
},
|
|
645
|
+
async cancelTurn(sessionId) {
|
|
646
|
+
const controller = turnAbortControllers.get(sessionId);
|
|
647
|
+
if (!controller)
|
|
648
|
+
return;
|
|
649
|
+
controller.abort();
|
|
650
|
+
turnAbortControllers.delete(sessionId);
|
|
651
|
+
},
|
|
652
|
+
async endSession(sessionId) {
|
|
653
|
+
// No-op: GeneratedAgent lifecycle is managed by agent cache.
|
|
654
|
+
turnAbortControllers.delete(sessionId);
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
};
|
|
658
|
+
const generateWithSessionRetry = async (session, prompt, generateOptions, abortSignal, maxAttempts = 3) => {
|
|
659
|
+
let lastError;
|
|
660
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
661
|
+
if (abortSignal.aborted) {
|
|
662
|
+
throw new Error("Run cancelled");
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
const turnResult = await session.sendAndWait({
|
|
666
|
+
prompt: typeof prompt === "string" ? prompt : "[multimodal-prompt]",
|
|
667
|
+
metadata: {
|
|
668
|
+
modelInput: prompt,
|
|
669
|
+
generateOptions,
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
return asGenerateResultFromTurn(turnResult);
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
lastError = error;
|
|
676
|
+
if (abortSignal.aborted) {
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
const shouldRetry = isRetryableModelError(error) && attempt < maxAttempts;
|
|
680
|
+
if (!shouldRetry) {
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
683
|
+
const backoffMs = 600 * 2 ** (attempt - 1);
|
|
684
|
+
console.warn(`[agent] transient model error (attempt ${attempt}/${maxAttempts}); retrying in ${backoffMs}ms`);
|
|
685
|
+
await sleep(backoffMs);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
throw lastError;
|
|
689
|
+
};
|
|
690
|
+
async function getOrCreateAgent(modelId, workspacePath, mcpServers = [], preferredAgentId, terminalAutoApproveRules, useMastraObservationalMemory, observationalMemorySettings, streamErrorRetry) {
|
|
691
|
+
// Initialize Language Model Tools system with MCP servers
|
|
692
|
+
try {
|
|
693
|
+
await initializeLanguageModelTools(mcpServers, workspacePath);
|
|
694
|
+
console.log("✅ Language Model Tools initialized");
|
|
695
|
+
}
|
|
696
|
+
catch (error) {
|
|
697
|
+
console.warn("⚠️ Failed to initialize Language Model Tools:", error);
|
|
698
|
+
}
|
|
699
|
+
const mcpFingerprint = toStableMcpFingerprint(mcpServers);
|
|
700
|
+
const normalizedPreferredAgentId = preferredAgentId?.trim() || "";
|
|
701
|
+
const terminalRulesFingerprint = getTerminalAutoApproveRulesFingerprint(terminalAutoApproveRules);
|
|
702
|
+
const memorySettingsFingerprint = JSON.stringify(observationalMemorySettings || {});
|
|
703
|
+
const streamErrorRetryFingerprint = JSON.stringify(streamErrorRetry || {});
|
|
704
|
+
const memoryFingerprint = useMastraObservationalMemory
|
|
705
|
+
? `om:on:${memorySettingsFingerprint}`
|
|
706
|
+
: "om:off";
|
|
707
|
+
const key = `${modelId}:${workspacePath}:${mcpFingerprint}:${normalizedPreferredAgentId}:${terminalRulesFingerprint}:${memoryFingerprint}:retry:${streamErrorRetryFingerprint}`;
|
|
708
|
+
if (agentCache.has(key)) {
|
|
709
|
+
// Move to end to mark as recently used (LRU semantics via Map insertion order).
|
|
710
|
+
const existing = agentCache.get(key);
|
|
711
|
+
agentCache.delete(key);
|
|
712
|
+
agentCache.set(key, existing);
|
|
713
|
+
return existing;
|
|
714
|
+
}
|
|
715
|
+
let manifestExternalRegistration;
|
|
716
|
+
const externalManifestPath = process.env.IRIS_AGENT_EXTERNAL_AGENT_MANIFEST_PATH?.trim();
|
|
717
|
+
if (externalManifestPath) {
|
|
718
|
+
try {
|
|
719
|
+
const lifecycle = new ExternalAgentLifecycleManager(externalManifestPath);
|
|
720
|
+
const externalRegistration = await lifecycle.createRegistration({
|
|
721
|
+
modelId,
|
|
722
|
+
workspacePath,
|
|
723
|
+
mcpServers,
|
|
724
|
+
});
|
|
725
|
+
manifestExternalRegistration = {
|
|
726
|
+
id: externalRegistration.descriptor.id,
|
|
727
|
+
name: externalRegistration.descriptor.name,
|
|
728
|
+
description: externalRegistration.descriptor.description ||
|
|
729
|
+
"Manifest-based external agent runtime.",
|
|
730
|
+
runtimeFactory: () => Promise.resolve(externalRegistration.runtimeFactory()),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
const err = error;
|
|
735
|
+
console.warn("[api] external manifest runtime unavailable, falling back to iris:", err.message);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const agentCore = await loadAgentCoreModule();
|
|
739
|
+
const registry = createDefaultAgentRegistry({
|
|
740
|
+
irisFactory: () => agentCore.createCodingAgent(modelId, workspacePath, {
|
|
741
|
+
mcpServers,
|
|
742
|
+
terminalAutoApproveRules,
|
|
743
|
+
// Non-observational requests already include the bounded conversation
|
|
744
|
+
// history in the prompt. Enabling Mastra memory there would replay it
|
|
745
|
+
// a second time and can exceed provider context windows.
|
|
746
|
+
enableMemory: useMastraObservationalMemory === true,
|
|
747
|
+
goalMaxRuns: 12,
|
|
748
|
+
useMastraObservationalMemory,
|
|
749
|
+
observationalMemorySettings,
|
|
750
|
+
streamErrorRetry,
|
|
751
|
+
}),
|
|
752
|
+
externalRegistration: manifestExternalRegistration,
|
|
753
|
+
});
|
|
754
|
+
const agent = await registry.createRuntime(normalizedPreferredAgentId || undefined, {
|
|
755
|
+
requiredCapabilities: ["tool_calling", "streaming"],
|
|
756
|
+
requiredPermissions: ["workspace_read"],
|
|
757
|
+
});
|
|
758
|
+
if (agentCache.size >= AGENT_CACHE_MAX_SIZE) {
|
|
759
|
+
// Evict the least-recently-used entry (first key in insertion order).
|
|
760
|
+
const firstKey = agentCache.keys().next().value;
|
|
761
|
+
if (firstKey !== undefined) {
|
|
762
|
+
agentCache.delete(firstKey);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
agentCache.set(key, agent);
|
|
766
|
+
return agent;
|
|
767
|
+
}
|
|
768
|
+
const inspectMcpServersForChat = async (servers, workspacePath) => {
|
|
769
|
+
const summaries = [];
|
|
770
|
+
for (const server of servers) {
|
|
771
|
+
try {
|
|
772
|
+
const tools = await listMcpServerTools(server, workspacePath);
|
|
773
|
+
summaries.push({
|
|
774
|
+
name: server.name,
|
|
775
|
+
command: server.command,
|
|
776
|
+
toolNames: tools.map((tool) => tool.name),
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
const err = error;
|
|
781
|
+
summaries.push({
|
|
782
|
+
name: server.name,
|
|
783
|
+
command: server.command,
|
|
784
|
+
toolNames: [],
|
|
785
|
+
error: err.message || "Failed to inspect MCP server",
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return summaries;
|
|
790
|
+
};
|
|
791
|
+
const formatMcpContext = (summaries) => {
|
|
792
|
+
if (summaries.length === 0)
|
|
793
|
+
return "";
|
|
794
|
+
const lines = ["", "**MCP SERVER STATUS:**"];
|
|
795
|
+
for (const summary of summaries) {
|
|
796
|
+
if (summary.error) {
|
|
797
|
+
lines.push(`- **${summary.name}** (${summary.command}): unavailable - ${summary.error}`);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (summary.toolNames.length === 0) {
|
|
801
|
+
lines.push(`- **${summary.name}** (${summary.command}): connected but exposed no tools`);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
lines.push(`- **${summary.name}** (${summary.command}): ${summary.toolNames.length} tool(s) available - ${summary.toolNames.join(", ")}`);
|
|
805
|
+
}
|
|
806
|
+
lines.push("- If the user asks why an MCP action failed, explain using the status above before suggesting a retry.");
|
|
807
|
+
return lines.join("\n");
|
|
808
|
+
};
|
|
809
|
+
const hasTerminalToolResults = (toolResults) => toolResults.some((toolResult) => {
|
|
810
|
+
const status = resolveToolExecutionStatus(toolResult.result);
|
|
811
|
+
return status === "completed" || status === "failed";
|
|
812
|
+
});
|
|
813
|
+
const hasNonterminalToolResults = (toolResults) => toolResults.some((toolResult) => {
|
|
814
|
+
const status = resolveToolExecutionStatus(toolResult.result);
|
|
815
|
+
return (status === "pending" ||
|
|
816
|
+
status === "in_progress" ||
|
|
817
|
+
status === "unknown");
|
|
818
|
+
});
|
|
819
|
+
const getToolResultPayload = (result) => {
|
|
820
|
+
if (!result || typeof result !== "object") {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
const direct = result;
|
|
824
|
+
const nestedValue = direct.value;
|
|
825
|
+
if (nestedValue && typeof nestedValue === "object") {
|
|
826
|
+
return nestedValue;
|
|
827
|
+
}
|
|
828
|
+
return direct;
|
|
829
|
+
};
|
|
830
|
+
const extractValidationFailures = (executedToolResults) => {
|
|
831
|
+
const failures = [];
|
|
832
|
+
for (const toolResult of executedToolResults) {
|
|
833
|
+
const payload = getToolResultPayload(toolResult.result);
|
|
834
|
+
if (!payload)
|
|
835
|
+
continue;
|
|
836
|
+
const validation = payload.validation;
|
|
837
|
+
if (!validation || typeof validation !== "object")
|
|
838
|
+
continue;
|
|
839
|
+
for (const phase of ["lint", "test"]) {
|
|
840
|
+
const phaseResult = validation[phase];
|
|
841
|
+
if (!phaseResult || typeof phaseResult !== "object")
|
|
842
|
+
continue;
|
|
843
|
+
const record = phaseResult;
|
|
844
|
+
if (record.enabled !== true || record.success !== false)
|
|
845
|
+
continue;
|
|
846
|
+
failures.push({
|
|
847
|
+
phase,
|
|
848
|
+
command: typeof record.command === "string" ? record.command : undefined,
|
|
849
|
+
error: typeof record.error === "string" ? record.error : undefined,
|
|
850
|
+
stdout: typeof record.stdout === "string" ? record.stdout : undefined,
|
|
851
|
+
stderr: typeof record.stderr === "string" ? record.stderr : undefined,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return failures;
|
|
856
|
+
};
|
|
857
|
+
const isAutoFixValidationEnabled = () => {
|
|
858
|
+
const raw = process.env.IRIS_AGENT_AUTO_FIX_VALIDATION;
|
|
859
|
+
if (!raw)
|
|
860
|
+
return true;
|
|
861
|
+
const normalized = raw.trim().toLowerCase();
|
|
862
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
863
|
+
return true;
|
|
864
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
865
|
+
return false;
|
|
866
|
+
return true;
|
|
867
|
+
};
|
|
868
|
+
const asPositiveInt = (value, fallback, min, max) => {
|
|
869
|
+
if (!value)
|
|
870
|
+
return fallback;
|
|
871
|
+
const parsed = Number.parseInt(value, 10);
|
|
872
|
+
if (!Number.isFinite(parsed))
|
|
873
|
+
return fallback;
|
|
874
|
+
return Math.max(min, Math.min(max, parsed));
|
|
875
|
+
};
|
|
876
|
+
const sanitizeStreamErrorRetryRequest = (value) => {
|
|
877
|
+
if (!value || typeof value !== "object") {
|
|
878
|
+
return undefined;
|
|
879
|
+
}
|
|
880
|
+
const input = value;
|
|
881
|
+
const normalized = {};
|
|
882
|
+
if (typeof input.enabled === "boolean") {
|
|
883
|
+
normalized.enabled = input.enabled;
|
|
884
|
+
}
|
|
885
|
+
if (typeof input.retryUnknownErrors === "boolean") {
|
|
886
|
+
normalized.retryUnknownErrors = input.retryUnknownErrors;
|
|
887
|
+
}
|
|
888
|
+
if (typeof input.maxRetries === "number" &&
|
|
889
|
+
Number.isFinite(input.maxRetries)) {
|
|
890
|
+
normalized.maxRetries = Math.max(0, Math.min(10, Math.floor(input.maxRetries)));
|
|
891
|
+
}
|
|
892
|
+
if (typeof input.baseDelayMs === "number" &&
|
|
893
|
+
Number.isFinite(input.baseDelayMs)) {
|
|
894
|
+
normalized.baseDelayMs = Math.max(50, Math.min(30_000, Math.floor(input.baseDelayMs)));
|
|
895
|
+
}
|
|
896
|
+
if (typeof input.maxDelayMs === "number" &&
|
|
897
|
+
Number.isFinite(input.maxDelayMs)) {
|
|
898
|
+
normalized.maxDelayMs = Math.max(50, Math.min(120_000, Math.floor(input.maxDelayMs)));
|
|
899
|
+
}
|
|
900
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
901
|
+
};
|
|
902
|
+
const readWorkspaceAirisConfigForAgent = async (workspaceRoot) => {
|
|
903
|
+
const normalizedRoot = workspaceRoot.trim();
|
|
904
|
+
if (!normalizedRoot) {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
const airisDirPath = path.join(normalizedRoot, ".airis");
|
|
908
|
+
const settingsPath = path.join(airisDirPath, "settings.json");
|
|
909
|
+
let airisStat = null;
|
|
910
|
+
try {
|
|
911
|
+
airisStat = await fs.stat(airisDirPath);
|
|
912
|
+
}
|
|
913
|
+
catch (error) {
|
|
914
|
+
const fsError = error;
|
|
915
|
+
if (!fsError || fsError.code !== "ENOENT") {
|
|
916
|
+
throw error;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
let raw = "";
|
|
920
|
+
if (airisStat?.isDirectory()) {
|
|
921
|
+
raw = await fs.readFile(settingsPath, "utf8").catch((error) => {
|
|
922
|
+
const fsError = error;
|
|
923
|
+
if (fsError && fsError.code === "ENOENT") {
|
|
924
|
+
return "";
|
|
925
|
+
}
|
|
926
|
+
throw error;
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
else if (airisStat?.isFile()) {
|
|
930
|
+
// Backward compatibility: old installs used a single .airis JSON file.
|
|
931
|
+
raw = await fs.readFile(airisDirPath, "utf8");
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
if (!raw) {
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
const parsed = JSON.parse(raw);
|
|
940
|
+
if (!parsed || typeof parsed !== "object") {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
return parsed;
|
|
944
|
+
};
|
|
945
|
+
const shouldUseMastraManagedContextMode = (useMastraObservationalMemory) => {
|
|
946
|
+
return useMastraObservationalMemory === true;
|
|
947
|
+
};
|
|
948
|
+
const isCodingObjectiveRequest = (message, hasToolResults, isSynthesisOnlyContinuation) => {
|
|
949
|
+
if (hasToolResults || isSynthesisOnlyContinuation)
|
|
950
|
+
return false;
|
|
951
|
+
return /\b(add|build|change|create|debug|edit|fix|implement|integrate|migrate|modify|refactor|remove|replace|test|update)\b/i.test(message);
|
|
952
|
+
};
|
|
953
|
+
const asBoundedFloat = (value, fallback, min, max) => {
|
|
954
|
+
if (!value)
|
|
955
|
+
return fallback;
|
|
956
|
+
const parsed = Number.parseFloat(value);
|
|
957
|
+
if (!Number.isFinite(parsed))
|
|
958
|
+
return fallback;
|
|
959
|
+
return Math.max(min, Math.min(max, parsed));
|
|
960
|
+
};
|
|
961
|
+
const getModelExecutionProfile = (modelId) => {
|
|
962
|
+
const normalized = modelId.toLowerCase();
|
|
963
|
+
const baseProfile = {
|
|
964
|
+
generateRetryAttempts: 3,
|
|
965
|
+
reflectionMaxAttempts: 2,
|
|
966
|
+
reflectionRetryAttempts: 2,
|
|
967
|
+
reflectionMaxSteps: 8,
|
|
968
|
+
maxStepsCapDesktop: 50,
|
|
969
|
+
maxOutputTokens: 8192,
|
|
970
|
+
};
|
|
971
|
+
if (normalized.includes("gpt-5") ||
|
|
972
|
+
normalized.includes("o3") ||
|
|
973
|
+
normalized.includes("o4")) {
|
|
974
|
+
baseProfile.reflectionMaxAttempts = 3;
|
|
975
|
+
baseProfile.reflectionMaxSteps = 10;
|
|
976
|
+
}
|
|
977
|
+
else if (normalized.includes("claude")) {
|
|
978
|
+
baseProfile.reflectionMaxAttempts = 2;
|
|
979
|
+
baseProfile.reflectionMaxSteps = 8;
|
|
980
|
+
}
|
|
981
|
+
else if (normalized.includes("gemini")) {
|
|
982
|
+
baseProfile.reflectionMaxAttempts = 2;
|
|
983
|
+
baseProfile.reflectionMaxSteps = 7;
|
|
984
|
+
}
|
|
985
|
+
baseProfile.generateRetryAttempts = asPositiveInt(process.env.IRIS_AGENT_MODEL_RETRY_ATTEMPTS, baseProfile.generateRetryAttempts, 1, 5);
|
|
986
|
+
baseProfile.reflectionMaxAttempts = asPositiveInt(process.env.IRIS_AGENT_REFLECTION_MAX, baseProfile.reflectionMaxAttempts, 1, 4);
|
|
987
|
+
baseProfile.reflectionRetryAttempts = asPositiveInt(process.env.IRIS_AGENT_REFLECTION_RETRY_ATTEMPTS, baseProfile.reflectionRetryAttempts, 1, 4);
|
|
988
|
+
baseProfile.reflectionMaxSteps = asPositiveInt(process.env.IRIS_AGENT_REFLECTION_MAX_STEPS, baseProfile.reflectionMaxSteps, 1, 20);
|
|
989
|
+
baseProfile.maxOutputTokens = asPositiveInt(process.env.IRIS_AGENT_MAX_OUTPUT_TOKENS, baseProfile.maxOutputTokens, 256, 65536);
|
|
990
|
+
return baseProfile;
|
|
991
|
+
};
|
|
992
|
+
const isHuggingFaceModelId = (candidateModelId) => {
|
|
993
|
+
const normalized = (candidateModelId || "").toLowerCase();
|
|
994
|
+
if (!normalized)
|
|
995
|
+
return false;
|
|
996
|
+
if (normalized.startsWith("huggingface/"))
|
|
997
|
+
return true;
|
|
998
|
+
return normalized.endsWith(":fireworks-ai");
|
|
999
|
+
};
|
|
1000
|
+
const isOpenRouterModelId = (candidateModelId) => {
|
|
1001
|
+
const normalized = (candidateModelId || "").toLowerCase();
|
|
1002
|
+
if (!normalized)
|
|
1003
|
+
return false;
|
|
1004
|
+
if (normalized.startsWith("openrouter/"))
|
|
1005
|
+
return true;
|
|
1006
|
+
if (!normalized.includes("/"))
|
|
1007
|
+
return false;
|
|
1008
|
+
if (isHuggingFaceModelId(normalized))
|
|
1009
|
+
return false;
|
|
1010
|
+
if (normalized.startsWith("google/"))
|
|
1011
|
+
return false;
|
|
1012
|
+
if (normalized.startsWith("ollama/"))
|
|
1013
|
+
return false;
|
|
1014
|
+
if (normalized.startsWith("local/"))
|
|
1015
|
+
return false;
|
|
1016
|
+
return true;
|
|
1017
|
+
};
|
|
1018
|
+
const extractProviderErrorDetails = (error) => {
|
|
1019
|
+
const errorRecord = error;
|
|
1020
|
+
const providerError = errorRecord?.data
|
|
1021
|
+
?.error;
|
|
1022
|
+
const code = providerError?.code ?? providerError?.type;
|
|
1023
|
+
const message = providerError?.metadata?.raw ||
|
|
1024
|
+
providerError?.message ||
|
|
1025
|
+
(errorRecord instanceof Error ? errorRecord.message : "") ||
|
|
1026
|
+
"Failed to generate response";
|
|
1027
|
+
return {
|
|
1028
|
+
code: code === undefined ? undefined : String(code),
|
|
1029
|
+
message,
|
|
1030
|
+
};
|
|
1031
|
+
};
|
|
1032
|
+
const getRateLimitErrorMessage = (modelId) => isOpenRouterModelId(modelId)
|
|
1033
|
+
? "⚠️ OpenRouter rate limit reached. Please wait a moment and try again, or switch models/providers."
|
|
1034
|
+
: "⚠️ Provider rate limit reached. Please wait a moment and try again, or switch providers.";
|
|
1035
|
+
const normalizeProviderRequestError = (modelId, message, code) => {
|
|
1036
|
+
const normalizedMessage = (message || "").trim();
|
|
1037
|
+
const lowerMessage = normalizedMessage.toLowerCase();
|
|
1038
|
+
const lowerCode = (code || "").toLowerCase();
|
|
1039
|
+
const isRateLimitError = lowerCode === "429" ||
|
|
1040
|
+
lowerCode.includes("rate_limit") ||
|
|
1041
|
+
lowerCode.includes("rate limit") ||
|
|
1042
|
+
lowerMessage.includes("rate_limit") ||
|
|
1043
|
+
lowerMessage.includes("rate limit") ||
|
|
1044
|
+
lowerMessage.includes("rate-limited") ||
|
|
1045
|
+
lowerMessage.includes("too many requests");
|
|
1046
|
+
const looksLikeProviderAuthFailure = lowerMessage.includes("user not found") ||
|
|
1047
|
+
lowerMessage.includes("invalid api key") ||
|
|
1048
|
+
lowerMessage.includes("unauthorized") ||
|
|
1049
|
+
lowerCode.includes("401");
|
|
1050
|
+
if (isRateLimitError) {
|
|
1051
|
+
return getRateLimitErrorMessage(modelId);
|
|
1052
|
+
}
|
|
1053
|
+
if (!looksLikeProviderAuthFailure) {
|
|
1054
|
+
return normalizedMessage || "Failed to generate response";
|
|
1055
|
+
}
|
|
1056
|
+
if (isOpenRouterModelId(modelId)) {
|
|
1057
|
+
return "OpenRouter rejected the configured API key. Replace OPENROUTER_API_KEY in Settings and click Save & Activate Keys.";
|
|
1058
|
+
}
|
|
1059
|
+
if (isHuggingFaceModelId(modelId)) {
|
|
1060
|
+
return "HuggingFace rejected the configured token. Replace HF_TOKEN in Settings and click Save & Activate Keys.";
|
|
1061
|
+
}
|
|
1062
|
+
return normalizedMessage || "Failed to generate response";
|
|
1063
|
+
};
|
|
1064
|
+
const extractToolExecutionFailures = (executedToolResults) => {
|
|
1065
|
+
const failures = [];
|
|
1066
|
+
for (const toolResult of executedToolResults) {
|
|
1067
|
+
if (toolResult.status !== "failed")
|
|
1068
|
+
continue;
|
|
1069
|
+
const payload = getToolResultPayload(toolResult.result) || {};
|
|
1070
|
+
const error = typeof payload.error === "string"
|
|
1071
|
+
? payload.error
|
|
1072
|
+
: typeof toolResult.result?.error === "string"
|
|
1073
|
+
? toolResult.result.error || ""
|
|
1074
|
+
: undefined;
|
|
1075
|
+
failures.push({
|
|
1076
|
+
toolName: toolResult.name,
|
|
1077
|
+
args: toolResult.args,
|
|
1078
|
+
error,
|
|
1079
|
+
status: toolResult.status,
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
return failures;
|
|
1083
|
+
};
|
|
1084
|
+
const buildFailureSignature = (validationFailures, toolFailures) => {
|
|
1085
|
+
const validation = validationFailures
|
|
1086
|
+
.map((failure) => ({
|
|
1087
|
+
phase: failure.phase,
|
|
1088
|
+
command: failure.command || "",
|
|
1089
|
+
error: failure.error || "",
|
|
1090
|
+
stderr: failure.stderr || "",
|
|
1091
|
+
}))
|
|
1092
|
+
.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
1093
|
+
const tools = toolFailures
|
|
1094
|
+
.map((failure) => ({
|
|
1095
|
+
toolName: failure.toolName,
|
|
1096
|
+
status: failure.status,
|
|
1097
|
+
error: failure.error || "",
|
|
1098
|
+
}))
|
|
1099
|
+
.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
1100
|
+
return JSON.stringify({ validation, tools });
|
|
1101
|
+
};
|
|
1102
|
+
const resolveGitSafetyMode = () => {
|
|
1103
|
+
const raw = (process.env.IRIS_AGENT_GIT_SAFETY_MODE || "suggest")
|
|
1104
|
+
.trim()
|
|
1105
|
+
.toLowerCase();
|
|
1106
|
+
if (raw === "off")
|
|
1107
|
+
return "off";
|
|
1108
|
+
if (raw === "enforce")
|
|
1109
|
+
return "enforce";
|
|
1110
|
+
return "suggest";
|
|
1111
|
+
};
|
|
1112
|
+
const getReflectionNoProgressRepeatThreshold = () => asPositiveInt(process.env.IRIS_AGENT_REFLECTION_NO_PROGRESS_REPEATS, 1, 1, 5);
|
|
1113
|
+
const detectGitRepo = async (workspacePath) => {
|
|
1114
|
+
try {
|
|
1115
|
+
await fs.access(path.join(workspacePath, ".git"));
|
|
1116
|
+
return true;
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
const hasSuccessfulEditExecution = (executedToolResults) => executedToolResults.some((result) => EDIT_TOOL_NAMES.has(result.name) && result.status === "completed");
|
|
1123
|
+
const MAX_PERSISTED_TOOL_RESULT_BYTES = 16 * 1024;
|
|
1124
|
+
const PERSISTED_TOOL_RESULT_PREVIEW_CHARS = 4 * 1024;
|
|
1125
|
+
const PERSISTED_TOOL_RESULT_SUMMARY_FIELD_CHARS = 1024;
|
|
1126
|
+
const truncatePersistedSummaryString = (value) => {
|
|
1127
|
+
if (value.length <= PERSISTED_TOOL_RESULT_SUMMARY_FIELD_CHARS) {
|
|
1128
|
+
return value;
|
|
1129
|
+
}
|
|
1130
|
+
return value.slice(0, PERSISTED_TOOL_RESULT_SUMMARY_FIELD_CHARS);
|
|
1131
|
+
};
|
|
1132
|
+
const getJsonByteLength = (value) => {
|
|
1133
|
+
const serialized = JSON.stringify(value);
|
|
1134
|
+
if (serialized === undefined)
|
|
1135
|
+
return null;
|
|
1136
|
+
return Buffer.byteLength(serialized, "utf8");
|
|
1137
|
+
};
|
|
1138
|
+
const enforcePersistedSummaryByteLimit = (summary) => {
|
|
1139
|
+
const bounded = { ...summary };
|
|
1140
|
+
const currentByteLength = () => getJsonByteLength(bounded) ?? 0;
|
|
1141
|
+
const shrinkStringField = (key) => {
|
|
1142
|
+
const value = bounded[key];
|
|
1143
|
+
if (typeof value !== "string" || value.length === 0)
|
|
1144
|
+
return;
|
|
1145
|
+
while (currentByteLength() > MAX_PERSISTED_TOOL_RESULT_BYTES) {
|
|
1146
|
+
const current = bounded[key];
|
|
1147
|
+
if (typeof current !== "string" || current.length === 0)
|
|
1148
|
+
break;
|
|
1149
|
+
const overage = currentByteLength() - MAX_PERSISTED_TOOL_RESULT_BYTES;
|
|
1150
|
+
const nextLength = Math.max(0, current.length - Math.max(1, overage));
|
|
1151
|
+
bounded[key] = current.slice(0, nextLength);
|
|
1152
|
+
if (nextLength === 0)
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
shrinkStringField("preview");
|
|
1157
|
+
const stringKeys = Object.keys(bounded)
|
|
1158
|
+
.filter((key) => key !== "preview" && typeof bounded[key] === "string")
|
|
1159
|
+
.sort((a, b) => String(bounded[b]).length - String(bounded[a]).length);
|
|
1160
|
+
for (const key of stringKeys) {
|
|
1161
|
+
if (currentByteLength() <= MAX_PERSISTED_TOOL_RESULT_BYTES)
|
|
1162
|
+
break;
|
|
1163
|
+
shrinkStringField(key);
|
|
1164
|
+
}
|
|
1165
|
+
if (currentByteLength() > MAX_PERSISTED_TOOL_RESULT_BYTES) {
|
|
1166
|
+
for (const key of ["preview", ...stringKeys]) {
|
|
1167
|
+
if (currentByteLength() <= MAX_PERSISTED_TOOL_RESULT_BYTES)
|
|
1168
|
+
break;
|
|
1169
|
+
delete bounded[key];
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return bounded;
|
|
1173
|
+
};
|
|
1174
|
+
const summarizeToolResultForPersistence = (result) => {
|
|
1175
|
+
let serialized;
|
|
1176
|
+
try {
|
|
1177
|
+
serialized = JSON.stringify(result);
|
|
1178
|
+
}
|
|
1179
|
+
catch {
|
|
1180
|
+
return {
|
|
1181
|
+
truncated: true,
|
|
1182
|
+
reason: "Tool result could not be serialized for persistence",
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
if (serialized === undefined) {
|
|
1186
|
+
return {
|
|
1187
|
+
truncated: true,
|
|
1188
|
+
reason: "Tool result has no JSON representation for persistence",
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
const originalByteLength = Buffer.byteLength(serialized, "utf8");
|
|
1192
|
+
if (originalByteLength <= MAX_PERSISTED_TOOL_RESULT_BYTES) {
|
|
1193
|
+
return result;
|
|
1194
|
+
}
|
|
1195
|
+
const summary = {
|
|
1196
|
+
truncated: true,
|
|
1197
|
+
originalByteLength,
|
|
1198
|
+
preview: serialized.slice(0, PERSISTED_TOOL_RESULT_PREVIEW_CHARS),
|
|
1199
|
+
};
|
|
1200
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
1201
|
+
return enforcePersistedSummaryByteLimit(summary);
|
|
1202
|
+
}
|
|
1203
|
+
const record = result;
|
|
1204
|
+
for (const key of [
|
|
1205
|
+
"success",
|
|
1206
|
+
"status",
|
|
1207
|
+
"error",
|
|
1208
|
+
"exitCode",
|
|
1209
|
+
"filePath",
|
|
1210
|
+
"path",
|
|
1211
|
+
"totalFiles",
|
|
1212
|
+
"filesWithMatches",
|
|
1213
|
+
"totalMatches",
|
|
1214
|
+
]) {
|
|
1215
|
+
const value = record[key];
|
|
1216
|
+
if (typeof value === "string") {
|
|
1217
|
+
summary[key] = truncatePersistedSummaryString(value);
|
|
1218
|
+
}
|
|
1219
|
+
else if (typeof value === "number" || typeof value === "boolean") {
|
|
1220
|
+
summary[key] = value;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return enforcePersistedSummaryByteLimit(summary);
|
|
1224
|
+
};
|
|
1225
|
+
const PERSISTED_TOOL_ARG_METADATA_KEYS = [
|
|
1226
|
+
"filePath",
|
|
1227
|
+
"path",
|
|
1228
|
+
"targetPath",
|
|
1229
|
+
"oldPath",
|
|
1230
|
+
"newPath",
|
|
1231
|
+
"startLine",
|
|
1232
|
+
"endLine",
|
|
1233
|
+
"line",
|
|
1234
|
+
"limit",
|
|
1235
|
+
"query",
|
|
1236
|
+
"pattern",
|
|
1237
|
+
"command",
|
|
1238
|
+
"cwd",
|
|
1239
|
+
"replaceAll",
|
|
1240
|
+
];
|
|
1241
|
+
const summarizeToolArgsForPersistence = (args) => {
|
|
1242
|
+
let serialized;
|
|
1243
|
+
try {
|
|
1244
|
+
serialized = JSON.stringify(args);
|
|
1245
|
+
}
|
|
1246
|
+
catch {
|
|
1247
|
+
return {
|
|
1248
|
+
truncated: true,
|
|
1249
|
+
reason: "Tool arguments could not be serialized for persistence",
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
if (serialized === undefined) {
|
|
1253
|
+
return {
|
|
1254
|
+
truncated: true,
|
|
1255
|
+
reason: "Tool arguments have no JSON representation for persistence",
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
const originalByteLength = Buffer.byteLength(serialized, "utf8");
|
|
1259
|
+
if (originalByteLength <= MAX_PERSISTED_TOOL_RESULT_BYTES) {
|
|
1260
|
+
return args;
|
|
1261
|
+
}
|
|
1262
|
+
const summary = {
|
|
1263
|
+
truncated: true,
|
|
1264
|
+
originalByteLength,
|
|
1265
|
+
preview: serialized.slice(0, PERSISTED_TOOL_RESULT_PREVIEW_CHARS),
|
|
1266
|
+
};
|
|
1267
|
+
if (!isRecordValue(args)) {
|
|
1268
|
+
return enforcePersistedSummaryByteLimit(summary);
|
|
1269
|
+
}
|
|
1270
|
+
for (const key of PERSISTED_TOOL_ARG_METADATA_KEYS) {
|
|
1271
|
+
const value = args[key];
|
|
1272
|
+
if (typeof value === "string") {
|
|
1273
|
+
summary[key] = truncatePersistedSummaryString(value);
|
|
1274
|
+
}
|
|
1275
|
+
else if (typeof value === "number" || typeof value === "boolean") {
|
|
1276
|
+
summary[key] = value;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return enforcePersistedSummaryByteLimit(summary);
|
|
1280
|
+
};
|
|
1281
|
+
const normalizeToolActionPayload = (eventType, payload) => {
|
|
1282
|
+
if (!payload ||
|
|
1283
|
+
(eventType !== "tool_call" && eventType !== "tool_result")) {
|
|
1284
|
+
return payload;
|
|
1285
|
+
}
|
|
1286
|
+
const name = typeof payload.name === "string"
|
|
1287
|
+
? payload.name
|
|
1288
|
+
: typeof payload.toolName === "string"
|
|
1289
|
+
? payload.toolName
|
|
1290
|
+
: undefined;
|
|
1291
|
+
if (!name)
|
|
1292
|
+
return payload;
|
|
1293
|
+
return {
|
|
1294
|
+
...payload,
|
|
1295
|
+
name,
|
|
1296
|
+
toolName: typeof payload.toolName === "string" ? payload.toolName : name,
|
|
1297
|
+
};
|
|
1298
|
+
};
|
|
1299
|
+
/**
|
|
1300
|
+
* Format directory tree for display
|
|
1301
|
+
*/
|
|
1302
|
+
function formatDirectoryTree(tree, prefix = "", isLast = true) {
|
|
1303
|
+
if (!tree)
|
|
1304
|
+
return "";
|
|
1305
|
+
let result = "";
|
|
1306
|
+
const connector = isLast ? "└── " : "├── ";
|
|
1307
|
+
const extension = isLast ? " " : "│ ";
|
|
1308
|
+
result += prefix + connector + tree.name;
|
|
1309
|
+
if (tree.type === "directory") {
|
|
1310
|
+
result += "/";
|
|
1311
|
+
}
|
|
1312
|
+
result += "\n";
|
|
1313
|
+
if (tree.children && tree.children.length > 0) {
|
|
1314
|
+
// Sort: directories first, then files
|
|
1315
|
+
const sorted = [...tree.children].sort((a, b) => {
|
|
1316
|
+
if (a.type === b.type)
|
|
1317
|
+
return a.name.localeCompare(b.name);
|
|
1318
|
+
return a.type === "directory" ? -1 : 1;
|
|
1319
|
+
});
|
|
1320
|
+
sorted.forEach((child, index) => {
|
|
1321
|
+
const childIsLast = index === sorted.length - 1;
|
|
1322
|
+
result += formatDirectoryTree(child, prefix + extension, childIsLast);
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
return result;
|
|
1326
|
+
}
|
|
1327
|
+
// POST /api/agent/chat - Send a message to the agent
|
|
1328
|
+
router.post("/chat", async (req, res) => {
|
|
1329
|
+
console.log("🔍 === REQUEST RECEIVED ===");
|
|
1330
|
+
console.log("Body keys:", Object.keys(req.body));
|
|
1331
|
+
console.log("Message:", req.body.message?.substring(0, 100));
|
|
1332
|
+
console.log("FilesInContext:", req.body.filesInContext);
|
|
1333
|
+
let requestedModelId = "gpt-4o";
|
|
1334
|
+
let activeRunId;
|
|
1335
|
+
let activeHostSession;
|
|
1336
|
+
let activeHostSessionManager;
|
|
1337
|
+
// Set longer timeout for this specific route (5 minutes)
|
|
1338
|
+
req.setTimeout(5 * 60 * 1000);
|
|
1339
|
+
res.setTimeout(5 * 60 * 1000);
|
|
1340
|
+
try {
|
|
1341
|
+
const { message, chatSessionId: rawChatSessionId, runId, conversationHistory, toolResults, modelId = "gpt-4o", filesInContext, workspaceRoot, workspaceStructure, isTauri, contextSummary, // Accumulated context from previous interactions
|
|
1342
|
+
enabledSkills: rawEnabledSkills, enableSlashCommands = true, approvalMode, preferredAgentId, mcpServers: rawMcpServers, terminalAutoApproveRules: rawTerminalAutoApproveRules, useMastraObservationalMemory: rawUseMastraObservationalMemory, observationalMemorySettings: rawObservationalMemorySettings, streamErrorRetry: rawStreamErrorRetry, stream = false, } = req.body;
|
|
1343
|
+
requestedModelId = modelId;
|
|
1344
|
+
if (typeof runId === "string" &&
|
|
1345
|
+
runId.trim().length > 0 &&
|
|
1346
|
+
!isSafeRunId(runId.trim())) {
|
|
1347
|
+
return res
|
|
1348
|
+
.status(400)
|
|
1349
|
+
.json({ success: false, error: "Invalid run id" });
|
|
1350
|
+
}
|
|
1351
|
+
const chatSessionId = typeof rawChatSessionId === "string" ? rawChatSessionId.trim() : "";
|
|
1352
|
+
if (chatSessionId && !isSafeSessionId(chatSessionId)) {
|
|
1353
|
+
return res
|
|
1354
|
+
.status(400)
|
|
1355
|
+
.json({ success: false, error: "Invalid chat session id" });
|
|
1356
|
+
}
|
|
1357
|
+
const resolvedRunId = typeof runId === "string" && runId.trim().length > 0
|
|
1358
|
+
? runId.trim()
|
|
1359
|
+
: `run_${randomUUID()}`;
|
|
1360
|
+
const workspaceMutationGenerationId = randomUUID();
|
|
1361
|
+
activeRunId = resolvedRunId;
|
|
1362
|
+
const mastraThreadId = chatSessionId || resolvedRunId;
|
|
1363
|
+
if (chatSessionId) {
|
|
1364
|
+
const activeTurnKey = `chat:${mastraThreadId}`;
|
|
1365
|
+
if (activeChatSessionTurns.has(activeTurnKey)) {
|
|
1366
|
+
return res.status(409).json({
|
|
1367
|
+
success: false,
|
|
1368
|
+
runId: resolvedRunId,
|
|
1369
|
+
lifecycleState: "queued",
|
|
1370
|
+
stopReason: "none",
|
|
1371
|
+
error: "Another prompt is already running for this chat session. Wait for it to finish before starting a new turn.",
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
activeChatSessionTurns.add(activeTurnKey);
|
|
1375
|
+
const releaseActiveTurn = () => {
|
|
1376
|
+
activeChatSessionTurns.delete(activeTurnKey);
|
|
1377
|
+
};
|
|
1378
|
+
res.once("finish", releaseActiveTurn);
|
|
1379
|
+
res.once("close", releaseActiveTurn);
|
|
1380
|
+
}
|
|
1381
|
+
const mastraMemoryScope = rawUseMastraObservationalMemory === true
|
|
1382
|
+
? {
|
|
1383
|
+
thread: mastraThreadId,
|
|
1384
|
+
resource: `iris-chat:${mastraThreadId}`,
|
|
1385
|
+
}
|
|
1386
|
+
: undefined;
|
|
1387
|
+
let lifecycleState = "queued";
|
|
1388
|
+
let stopReason = "none";
|
|
1389
|
+
const persistLifecycle = async (nextState, nextStopReason, eventType, payload) => {
|
|
1390
|
+
lifecycleState = nextState;
|
|
1391
|
+
stopReason = nextStopReason;
|
|
1392
|
+
await safePersistRunLifecycleEvent({
|
|
1393
|
+
runId: resolvedRunId,
|
|
1394
|
+
lifecycleState: nextState,
|
|
1395
|
+
stopReason: nextStopReason,
|
|
1396
|
+
eventType,
|
|
1397
|
+
payload,
|
|
1398
|
+
objective: typeof message === "string" ? message.slice(0, 2000) : "",
|
|
1399
|
+
workspacePath: workspaceRoot || process.cwd(),
|
|
1400
|
+
modelId,
|
|
1401
|
+
});
|
|
1402
|
+
};
|
|
1403
|
+
let isWebWorkspaceForPersistence = false;
|
|
1404
|
+
const persistToolEvent = async (eventType, payload, eventLifecycleState = lifecycleState) => {
|
|
1405
|
+
const toolName = typeof payload.name === "string"
|
|
1406
|
+
? payload.name
|
|
1407
|
+
: typeof payload.toolName === "string"
|
|
1408
|
+
? payload.toolName
|
|
1409
|
+
: "";
|
|
1410
|
+
const effectiveArgs = "args" in payload && isRecordValue(payload.args)
|
|
1411
|
+
? sanitizeToolArgsForWorkspace(toolName, payload.args, isWebWorkspaceForPersistence)
|
|
1412
|
+
: payload.args;
|
|
1413
|
+
const args = "args" in payload
|
|
1414
|
+
? summarizeToolArgsForPersistence(redactToolResult(effectiveArgs).result)
|
|
1415
|
+
: undefined;
|
|
1416
|
+
const result = "result" in payload
|
|
1417
|
+
? summarizeToolResultForPersistence(payload.result)
|
|
1418
|
+
: undefined;
|
|
1419
|
+
const persistedPayload = {
|
|
1420
|
+
...payload,
|
|
1421
|
+
...("args" in payload ? { args } : {}),
|
|
1422
|
+
...("result" in payload ? { result } : {}),
|
|
1423
|
+
};
|
|
1424
|
+
await safePersistRunLifecycleEvent({
|
|
1425
|
+
runId: resolvedRunId,
|
|
1426
|
+
lifecycleState: eventLifecycleState,
|
|
1427
|
+
stopReason,
|
|
1428
|
+
eventType,
|
|
1429
|
+
payload: persistedPayload,
|
|
1430
|
+
objective: typeof message === "string" ? message.slice(0, 2000) : "",
|
|
1431
|
+
workspacePath: workspaceRoot || process.cwd(),
|
|
1432
|
+
modelId,
|
|
1433
|
+
});
|
|
1434
|
+
};
|
|
1435
|
+
const transitionToCancelled = async (phase) => {
|
|
1436
|
+
await persistLifecycle("cancelled", "cancelled", "cancelled", {
|
|
1437
|
+
phase,
|
|
1438
|
+
});
|
|
1439
|
+
};
|
|
1440
|
+
const isCancelled = async () => {
|
|
1441
|
+
try {
|
|
1442
|
+
return await isRunCancellationRequested(resolvedRunId);
|
|
1443
|
+
}
|
|
1444
|
+
catch (error) {
|
|
1445
|
+
const err = error;
|
|
1446
|
+
// Fail-closed: if we can't confirm the cancellation state, treat the
|
|
1447
|
+
// run as cancelled rather than letting it continue (fail-open) or
|
|
1448
|
+
// throwing (which would abort the whole in-flight stream/response
|
|
1449
|
+
// with an unhandled 500 on every per-chunk check).
|
|
1450
|
+
console.warn("[agent] Failed to read run cancellation state, treating run as cancelled:", err.message);
|
|
1451
|
+
return true;
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
const turnAbortController = new AbortController();
|
|
1455
|
+
let turnCancellationForwarded = false;
|
|
1456
|
+
const requestTurnCancellation = async () => {
|
|
1457
|
+
if (!turnAbortController.signal.aborted) {
|
|
1458
|
+
turnAbortController.abort();
|
|
1459
|
+
}
|
|
1460
|
+
if (turnCancellationForwarded) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
turnCancellationForwarded = true;
|
|
1464
|
+
if (!activeHostSession) {
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
try {
|
|
1468
|
+
await activeHostSession.cancel();
|
|
1469
|
+
}
|
|
1470
|
+
catch (error) {
|
|
1471
|
+
console.warn("[agent] Failed to propagate turn cancellation to host session:", getErrorMessage(error));
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
const cancelledDuringWait = {
|
|
1475
|
+
__cancelledDuringWait: true,
|
|
1476
|
+
};
|
|
1477
|
+
const isCancelledWaitResult = (value) => Boolean(value &&
|
|
1478
|
+
typeof value === "object" &&
|
|
1479
|
+
"__cancelledDuringWait" in value &&
|
|
1480
|
+
value
|
|
1481
|
+
.__cancelledDuringWait === true);
|
|
1482
|
+
const waitForResultOrCancellation = async (work) => {
|
|
1483
|
+
let stopped = false;
|
|
1484
|
+
const waitForCancellation = (async () => {
|
|
1485
|
+
while (!stopped) {
|
|
1486
|
+
if (await isCancelled()) {
|
|
1487
|
+
await requestTurnCancellation();
|
|
1488
|
+
return cancelledDuringWait;
|
|
1489
|
+
}
|
|
1490
|
+
await sleep(75);
|
|
1491
|
+
}
|
|
1492
|
+
return cancelledDuringWait;
|
|
1493
|
+
})();
|
|
1494
|
+
try {
|
|
1495
|
+
const settled = await Promise.race([work, waitForCancellation]);
|
|
1496
|
+
return settled;
|
|
1497
|
+
}
|
|
1498
|
+
finally {
|
|
1499
|
+
stopped = true;
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
await persistLifecycle("queued", "none", "request_received", {
|
|
1503
|
+
hasConversationHistory: Array.isArray(conversationHistory) && conversationHistory.length > 0,
|
|
1504
|
+
hasToolResults: Array.isArray(toolResults) && toolResults.length > 0,
|
|
1505
|
+
});
|
|
1506
|
+
const enabledSkills = normalizeEnabledSkills(rawEnabledSkills);
|
|
1507
|
+
if (isTauri && approvalMode && approvalMode !== "Default Approvals") {
|
|
1508
|
+
return res.status(400).json({
|
|
1509
|
+
success: false,
|
|
1510
|
+
error: "Invalid approval mode for Tauri: only 'Default Approvals' is allowed",
|
|
1511
|
+
toolCalls: [],
|
|
1512
|
+
executedToolResults: [],
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
const requestedMcpServers = sanitizeMcpServers(rawMcpServers);
|
|
1516
|
+
const terminalAutoApproveRules = sanitizeTerminalAutoApproveRules(rawTerminalAutoApproveRules);
|
|
1517
|
+
const useMastraObservationalMemory = typeof rawUseMastraObservationalMemory === "boolean"
|
|
1518
|
+
? rawUseMastraObservationalMemory
|
|
1519
|
+
: undefined;
|
|
1520
|
+
const observationalMemorySettings = sanitizeObservationalMemorySettings(rawObservationalMemorySettings);
|
|
1521
|
+
const streamErrorRetry = sanitizeStreamErrorRetryRequest(rawStreamErrorRetry);
|
|
1522
|
+
const allowMcp = Boolean(isTauri && hasDesktopAuth(req));
|
|
1523
|
+
const mcpServers = allowMcp ? requestedMcpServers : [];
|
|
1524
|
+
if (!allowMcp && requestedMcpServers.length > 0) {
|
|
1525
|
+
console.warn("[mcp] Ignoring MCP servers without desktop auth");
|
|
1526
|
+
}
|
|
1527
|
+
console.log("📨 Request:", {
|
|
1528
|
+
messageLength: message?.length,
|
|
1529
|
+
hasHistory: !!conversationHistory,
|
|
1530
|
+
historyLength: conversationHistory?.length || 0,
|
|
1531
|
+
hasToolResults: Array.isArray(toolResults) && toolResults.length > 0,
|
|
1532
|
+
enabledSkillsCount: enabledSkills.length,
|
|
1533
|
+
isTauri,
|
|
1534
|
+
});
|
|
1535
|
+
// Build messages array with optional structured tool-result continuation.
|
|
1536
|
+
let messages = Array.isArray(conversationHistory)
|
|
1537
|
+
? [...conversationHistory]
|
|
1538
|
+
: [];
|
|
1539
|
+
if (messages.length === 0 &&
|
|
1540
|
+
typeof message === "string" &&
|
|
1541
|
+
message.length > 0) {
|
|
1542
|
+
messages.push({ role: "user", content: message });
|
|
1543
|
+
}
|
|
1544
|
+
const toolResultsContinuationMessage = serializeToolResultsForConversation(toolResults);
|
|
1545
|
+
if (toolResultsContinuationMessage) {
|
|
1546
|
+
messages.push({
|
|
1547
|
+
role: "user",
|
|
1548
|
+
content: toolResultsContinuationMessage,
|
|
1549
|
+
continuationType: "tool_results",
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
// If Tauri mode, add note about tool execution
|
|
1553
|
+
if (isTauri) {
|
|
1554
|
+
console.log("🖥️ Tauri mode - tools will be executed in frontend");
|
|
1555
|
+
}
|
|
1556
|
+
if (messages.length === 0) {
|
|
1557
|
+
return res.status(400).json({ error: "Message is required" });
|
|
1558
|
+
}
|
|
1559
|
+
const effectiveMessage = typeof message === "string" && message.length > 0
|
|
1560
|
+
? message
|
|
1561
|
+
: messages[messages.length - 1]?.content || "";
|
|
1562
|
+
// Use provided workspace root or default to current directory
|
|
1563
|
+
const workspacePath = workspaceRoot || process.cwd();
|
|
1564
|
+
const workspaceAirisConfig = await readWorkspaceAirisConfigForAgent(workspacePath).catch((error) => {
|
|
1565
|
+
const err = error;
|
|
1566
|
+
console.warn("[agent] failed to read workspace AIRIS config:", err.message);
|
|
1567
|
+
return null;
|
|
1568
|
+
});
|
|
1569
|
+
const workspaceStreamErrorRetry = sanitizeStreamErrorRetryRequest(workspaceAirisConfig?.agent?.streamErrorRetry);
|
|
1570
|
+
const effectiveStreamErrorRetry = streamErrorRetry ?? workspaceStreamErrorRetry;
|
|
1571
|
+
await persistLifecycle("running", "none", "run_started", {
|
|
1572
|
+
workspacePath,
|
|
1573
|
+
modelId,
|
|
1574
|
+
});
|
|
1575
|
+
if (await isCancelled()) {
|
|
1576
|
+
if (!stream) {
|
|
1577
|
+
await transitionToCancelled("run_started");
|
|
1578
|
+
return res.status(409).json({
|
|
1579
|
+
success: false,
|
|
1580
|
+
runId: resolvedRunId,
|
|
1581
|
+
lifecycleState,
|
|
1582
|
+
stopReason,
|
|
1583
|
+
error: "Run was cancelled",
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const modelProfile = getModelExecutionProfile(modelId);
|
|
1588
|
+
const gitSafetyMode = resolveGitSafetyMode();
|
|
1589
|
+
const gitDetected = await detectGitRepo(workspacePath);
|
|
1590
|
+
const parsedSlashCommand = parseSlashCommandRequest(effectiveMessage);
|
|
1591
|
+
if (parsedSlashCommand) {
|
|
1592
|
+
if (!enableSlashCommands) {
|
|
1593
|
+
// User disabled slash command execution in settings; treat this as normal chat input.
|
|
1594
|
+
}
|
|
1595
|
+
else if (!isSlashCommandExecutionAllowed(req, isTauri, enableSlashCommands)) {
|
|
1596
|
+
return res.status(403).json({
|
|
1597
|
+
success: false,
|
|
1598
|
+
error: "Slash command execution is disabled or not allowed in this context",
|
|
1599
|
+
toolCalls: [],
|
|
1600
|
+
executedToolResults: [],
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
else {
|
|
1604
|
+
const builtInSlashResult = executeRegisteredSlashCommand({
|
|
1605
|
+
parsedCommand: parsedSlashCommand,
|
|
1606
|
+
conversationHistory,
|
|
1607
|
+
contextSummary,
|
|
1608
|
+
});
|
|
1609
|
+
if (builtInSlashResult) {
|
|
1610
|
+
return res.json(builtInSlashResult);
|
|
1611
|
+
}
|
|
1612
|
+
const slashCommand = parsedSlashCommand.shellCommand;
|
|
1613
|
+
if (!slashCommand) {
|
|
1614
|
+
return res.status(400).json({
|
|
1615
|
+
success: false,
|
|
1616
|
+
error: `Slash command '/${parsedSlashCommand.name}' requires a shell command argument`,
|
|
1617
|
+
toolCalls: [],
|
|
1618
|
+
executedToolResults: [],
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
const toolArgs = {
|
|
1622
|
+
command: slashCommand,
|
|
1623
|
+
cwd: workspacePath,
|
|
1624
|
+
description: `Slash command: ${slashCommand}`,
|
|
1625
|
+
};
|
|
1626
|
+
const commandResult = await executeCommand({
|
|
1627
|
+
...toolArgs,
|
|
1628
|
+
workspaceRoot: workspacePath,
|
|
1629
|
+
});
|
|
1630
|
+
const status = resolveToolExecutionStatus(commandResult);
|
|
1631
|
+
if (status === "pending_confirmation") {
|
|
1632
|
+
const confirmationId = commandResult.confirmationId ||
|
|
1633
|
+
"";
|
|
1634
|
+
if (!confirmationId) {
|
|
1635
|
+
return res.status(500).json({
|
|
1636
|
+
success: false,
|
|
1637
|
+
error: "Command entered pending confirmation state without confirmationId",
|
|
1638
|
+
toolCalls: [],
|
|
1639
|
+
executedToolResults: [
|
|
1640
|
+
{
|
|
1641
|
+
name: "executeCommand",
|
|
1642
|
+
args: toolArgs,
|
|
1643
|
+
result: commandResult,
|
|
1644
|
+
status,
|
|
1645
|
+
},
|
|
1646
|
+
],
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
return res.json({
|
|
1650
|
+
success: true,
|
|
1651
|
+
response: `Pending approval for command: ${slashCommand}`,
|
|
1652
|
+
requiresConfirmation: true,
|
|
1653
|
+
pendingConfirmations: [
|
|
1654
|
+
{
|
|
1655
|
+
confirmationId,
|
|
1656
|
+
command: commandResult.command ||
|
|
1657
|
+
slashCommand,
|
|
1658
|
+
action: commandResult.action,
|
|
1659
|
+
target: commandResult.target,
|
|
1660
|
+
toolName: "executeCommand",
|
|
1661
|
+
toolArgs,
|
|
1662
|
+
},
|
|
1663
|
+
],
|
|
1664
|
+
toolCalls: [],
|
|
1665
|
+
executedToolResults: [
|
|
1666
|
+
{
|
|
1667
|
+
name: "executeCommand",
|
|
1668
|
+
args: toolArgs,
|
|
1669
|
+
result: commandResult,
|
|
1670
|
+
status,
|
|
1671
|
+
},
|
|
1672
|
+
],
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
if (status === "pending" || status === "in_progress") {
|
|
1676
|
+
return res.json({
|
|
1677
|
+
success: false,
|
|
1678
|
+
response: status === "in_progress"
|
|
1679
|
+
? "Command is still running"
|
|
1680
|
+
: "Command is pending",
|
|
1681
|
+
status,
|
|
1682
|
+
taskId: typeof commandResult.taskId ===
|
|
1683
|
+
"string"
|
|
1684
|
+
? commandResult.taskId
|
|
1685
|
+
: undefined,
|
|
1686
|
+
toolCalls: [],
|
|
1687
|
+
executedToolResults: [
|
|
1688
|
+
{
|
|
1689
|
+
name: "executeCommand",
|
|
1690
|
+
args: toolArgs,
|
|
1691
|
+
result: commandResult,
|
|
1692
|
+
status,
|
|
1693
|
+
},
|
|
1694
|
+
],
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
const commandSucceeded = status === "completed";
|
|
1698
|
+
const commandError = commandResult.error &&
|
|
1699
|
+
typeof commandResult.error === "string"
|
|
1700
|
+
? commandResult.error || "Command failed"
|
|
1701
|
+
: "Command failed";
|
|
1702
|
+
const stdoutText = typeof commandResult.stdout === "string"
|
|
1703
|
+
? commandResult.stdout || ""
|
|
1704
|
+
: "";
|
|
1705
|
+
const stderrText = typeof commandResult.stderr === "string"
|
|
1706
|
+
? commandResult.stderr || ""
|
|
1707
|
+
: "";
|
|
1708
|
+
const responseText = commandSucceeded
|
|
1709
|
+
? stdoutText || "Command executed"
|
|
1710
|
+
: stderrText || commandError;
|
|
1711
|
+
return res.json({
|
|
1712
|
+
success: commandSucceeded,
|
|
1713
|
+
response: responseText,
|
|
1714
|
+
...(commandSucceeded ? {} : { error: commandError }),
|
|
1715
|
+
toolCalls: [],
|
|
1716
|
+
executedToolResults: [
|
|
1717
|
+
{
|
|
1718
|
+
name: "executeCommand",
|
|
1719
|
+
args: toolArgs,
|
|
1720
|
+
result: commandResult,
|
|
1721
|
+
status,
|
|
1722
|
+
},
|
|
1723
|
+
],
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
const mcpDiscovery = allowMcp
|
|
1728
|
+
? await inspectMcpServersForChat(mcpServers, workspacePath)
|
|
1729
|
+
: [];
|
|
1730
|
+
// Detect if this is a web-based workspace (virtual path)
|
|
1731
|
+
const isWebWorkspace = workspacePath.startsWith("/workspace/");
|
|
1732
|
+
isWebWorkspaceForPersistence = isWebWorkspace;
|
|
1733
|
+
// Generate environment snapshot for first message (non-web workspaces only)
|
|
1734
|
+
let envSnapshotMarkdown = "";
|
|
1735
|
+
const isFirstMessage = !conversationHistory || conversationHistory.length <= 1;
|
|
1736
|
+
if (!isWebWorkspace && isFirstMessage) {
|
|
1737
|
+
console.log("📸 Generating environment snapshot for first message...");
|
|
1738
|
+
try {
|
|
1739
|
+
// Check cache first
|
|
1740
|
+
const cacheKey = workspacePath;
|
|
1741
|
+
let snapshot = envSnapshotCache.get(cacheKey);
|
|
1742
|
+
if (!snapshot) {
|
|
1743
|
+
snapshot = await getEnvironmentSnapshot(workspacePath);
|
|
1744
|
+
// Cache for 5 minutes
|
|
1745
|
+
envSnapshotCache.set(cacheKey, snapshot);
|
|
1746
|
+
const evictionTimer = setTimeout(() => envSnapshotCache.delete(cacheKey), 5 * 60 * 1000);
|
|
1747
|
+
evictionTimer.unref();
|
|
1748
|
+
}
|
|
1749
|
+
envSnapshotMarkdown = formatSnapshotAsMarkdown(snapshot);
|
|
1750
|
+
console.log("✅ Environment snapshot generated");
|
|
1751
|
+
}
|
|
1752
|
+
catch (error) {
|
|
1753
|
+
const err = error;
|
|
1754
|
+
console.warn("⚠️ Failed to generate environment snapshot:", err.message);
|
|
1755
|
+
// Continue without snapshot
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
// Get or create agent for this model/workspace combination
|
|
1759
|
+
const effectivePreferredAgentId = preferredAgentId ||
|
|
1760
|
+
process.env.IRIS_AGENT_PREFERRED_AGENT_ID ||
|
|
1761
|
+
undefined;
|
|
1762
|
+
const agent = await getOrCreateAgent(modelId, workspacePath, mcpServers, effectivePreferredAgentId, terminalAutoApproveRules, useMastraObservationalMemory, observationalMemorySettings, effectiveStreamErrorRetry);
|
|
1763
|
+
const hostSessionId = chatSessionId || resolvedRunId;
|
|
1764
|
+
activeHostSessionManager = new HostSessionManager(() => Promise.resolve(createGeneratedAgentRuntimeAdapter(agent)), {
|
|
1765
|
+
workspacePath,
|
|
1766
|
+
modelId,
|
|
1767
|
+
metadata: {
|
|
1768
|
+
runId: resolvedRunId,
|
|
1769
|
+
preferredAgentId: effectivePreferredAgentId || "iris",
|
|
1770
|
+
},
|
|
1771
|
+
});
|
|
1772
|
+
activeHostSession = await activeHostSessionManager.resumeSession(hostSessionId, {
|
|
1773
|
+
modelId,
|
|
1774
|
+
metadata: {
|
|
1775
|
+
runId: resolvedRunId,
|
|
1776
|
+
threadId: mastraThreadId,
|
|
1777
|
+
},
|
|
1778
|
+
});
|
|
1779
|
+
// Build workspace context
|
|
1780
|
+
let contextInfo = `
|
|
1781
|
+
|
|
1782
|
+
**WORKSPACE:**`;
|
|
1783
|
+
if (isWebWorkspace) {
|
|
1784
|
+
contextInfo += `
|
|
1785
|
+
- **Environment:** Web workspace
|
|
1786
|
+
- **Name:** ${workspacePath.split("/").pop()}`;
|
|
1787
|
+
if (workspaceStructure) {
|
|
1788
|
+
contextInfo += `
|
|
1789
|
+
- **Structure:**
|
|
1790
|
+
\`\`\`
|
|
1791
|
+
${formatDirectoryTree(workspaceStructure)}
|
|
1792
|
+
\`\`\``;
|
|
1793
|
+
}
|
|
1794
|
+
contextInfo += `
|
|
1795
|
+
- Treat paths as workspace-relative (for example, \`src/index.js\`).
|
|
1796
|
+
- Use the provided structure and file context; request missing file contents only when needed.
|
|
1797
|
+
- Answer directly; avoid generic follow-up questions.`;
|
|
1798
|
+
}
|
|
1799
|
+
else {
|
|
1800
|
+
contextInfo += `
|
|
1801
|
+
- **Root:** ${workspacePath}
|
|
1802
|
+
- Use \`getWorkspaceInfo\` with \`{ "workspacePath": "${workspacePath}", "includeTree": true }\`.
|
|
1803
|
+
- Use workspace-relative paths with file tools (for example, \`src/index.js\`).`;
|
|
1804
|
+
if (gitDetected && gitSafetyMode !== "off") {
|
|
1805
|
+
contextInfo += `
|
|
1806
|
+
- **Git Safety:** repository detected.
|
|
1807
|
+
- Preserve existing worktree changes; do not stage, commit, stash, or revert them unless the user explicitly requests it.
|
|
1808
|
+
- Keep edits scoped and reversible, and avoid destructive git commands.
|
|
1809
|
+
- If edits fail, provide undo guidance and a minimal recovery plan.`;
|
|
1810
|
+
}
|
|
1811
|
+
// Add environment snapshot if this is the first message
|
|
1812
|
+
if (envSnapshotMarkdown) {
|
|
1813
|
+
contextInfo += `
|
|
1814
|
+
|
|
1815
|
+
${envSnapshotMarkdown}`;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
if (mcpDiscovery.length > 0) {
|
|
1819
|
+
contextInfo += `
|
|
1820
|
+
${formatMcpContext(mcpDiscovery)}`;
|
|
1821
|
+
}
|
|
1822
|
+
// Add accumulated context summary if available
|
|
1823
|
+
if (contextSummary && contextSummary.length > 0) {
|
|
1824
|
+
contextInfo += `
|
|
1825
|
+
|
|
1826
|
+
**ACCUMULATED KNOWLEDGE (from previous interactions):**
|
|
1827
|
+
_You have discovered the following in earlier interactions. Use this to avoid repeating searches._
|
|
1828
|
+
|
|
1829
|
+
`;
|
|
1830
|
+
for (const ctx of contextSummary.slice(-10)) {
|
|
1831
|
+
// Last 10 contexts only
|
|
1832
|
+
contextInfo += `- ${ctx.id}: ${ctx.preview}...\n`;
|
|
1833
|
+
}
|
|
1834
|
+
contextInfo +=
|
|
1835
|
+
"\n_Use this accumulated knowledge to build upon previous work._\n";
|
|
1836
|
+
}
|
|
1837
|
+
// Add safety check for filesInContext
|
|
1838
|
+
const safeFilesInContext = (filesInContext || []).slice(0, MAX_CONTEXT_FILES);
|
|
1839
|
+
if ((filesInContext || []).length > MAX_CONTEXT_FILES) {
|
|
1840
|
+
contextInfo += `\n\n*Only the first ${MAX_CONTEXT_FILES} files in context are included for prompt budget safety.*`;
|
|
1841
|
+
}
|
|
1842
|
+
if (safeFilesInContext.length > 0) {
|
|
1843
|
+
contextInfo += "\n\n**Files in Context:**";
|
|
1844
|
+
// Check if files already have content (from web workspace)
|
|
1845
|
+
const filesHaveContent = safeFilesInContext.some((f) => f.content !== undefined);
|
|
1846
|
+
if (filesHaveContent) {
|
|
1847
|
+
// Files from web workspace with content already provided
|
|
1848
|
+
for (const file of safeFilesInContext) {
|
|
1849
|
+
contextInfo += `\n\n### File: ${file.name}`;
|
|
1850
|
+
contextInfo += `\n**Path:** \`${file.path}\`\n`;
|
|
1851
|
+
if (file.content) {
|
|
1852
|
+
const inlineContent = truncateText(file.content, MAX_INLINE_FILE_CONTENT_CHARS);
|
|
1853
|
+
contextInfo += `\`\`\`\n${inlineContent}\n\`\`\`\n`;
|
|
1854
|
+
}
|
|
1855
|
+
else if (file.skippedReason) {
|
|
1856
|
+
contextInfo += `*Skipped inline file: ${file.skippedReason}*\n`;
|
|
1857
|
+
}
|
|
1858
|
+
else if (file.error) {
|
|
1859
|
+
contextInfo += `*Error reading file: ${file.error}*\n`;
|
|
1860
|
+
}
|
|
1861
|
+
else {
|
|
1862
|
+
contextInfo += "*Content not available*\n";
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
else if (!isWebWorkspace) {
|
|
1867
|
+
// For non-web workspaces, read actual file contents from filesystem
|
|
1868
|
+
for (const file of safeFilesInContext) {
|
|
1869
|
+
contextInfo += `\n\n### File: ${file.name}`;
|
|
1870
|
+
contextInfo += `\n**Path:** \`${file.path}\`\n`;
|
|
1871
|
+
try {
|
|
1872
|
+
// Resolve file path relative to workspace
|
|
1873
|
+
const filePath = path.isAbsolute(file.path)
|
|
1874
|
+
? file.path
|
|
1875
|
+
: path.join(workspacePath, file.path);
|
|
1876
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
1877
|
+
const inlineContent = truncateText(content, MAX_INLINE_FILE_CONTENT_CHARS);
|
|
1878
|
+
contextInfo += `\`\`\`\n${inlineContent}\n\`\`\`\n`;
|
|
1879
|
+
}
|
|
1880
|
+
catch (err) {
|
|
1881
|
+
contextInfo += `*Error reading file: ${getErrorMessage(err)}*\n`;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
else {
|
|
1886
|
+
// For web workspaces without content
|
|
1887
|
+
contextInfo += `\n${safeFilesInContext
|
|
1888
|
+
.map((f) => `- ${f.name}: ${f.path}`)
|
|
1889
|
+
.join("\n")}`;
|
|
1890
|
+
contextInfo +=
|
|
1891
|
+
"\n\n**Note:** File contents should be provided by the user or requested through the interface.";
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
const modelInputTokenLimit = resolveModelInputTokenLimit(modelId);
|
|
1895
|
+
const promptBudgetRatio = asBoundedFloat(process.env.IRIS_AGENT_PROMPT_TOKEN_BUDGET_RATIO, DEFAULT_PROMPT_TOKEN_BUDGET_RATIO, 0.2, 0.8);
|
|
1896
|
+
const promptTokenBudget = Math.max(MIN_PROMPT_TOKEN_BUDGET, Math.min(Math.floor(modelInputTokenLimit * promptBudgetRatio), Math.max(MIN_PROMPT_TOKEN_BUDGET, modelInputTokenLimit - PROMPT_TOKEN_RESERVE)));
|
|
1897
|
+
const mastraManagedContextMode = shouldUseMastraManagedContextMode(useMastraObservationalMemory);
|
|
1898
|
+
const promptBuild = buildPromptWithinTokenBudget({
|
|
1899
|
+
effectiveMessage: truncateText(typeof messages[messages.length - 1]?.content === "string"
|
|
1900
|
+
? messages[messages.length - 1].content
|
|
1901
|
+
: effectiveMessage, MAX_CONVERSATION_MESSAGE_CHARS),
|
|
1902
|
+
conversationHistory: mastraManagedContextMode ? undefined : messages,
|
|
1903
|
+
contextInfo,
|
|
1904
|
+
maxPromptTokens: promptTokenBudget,
|
|
1905
|
+
maxConversationMessages: MAX_CONVERSATION_MESSAGES,
|
|
1906
|
+
maxConversationMessageTokens: MAX_CONVERSATION_MESSAGE_TOKENS,
|
|
1907
|
+
continuationInstruction: isSynthesisOnlyContinuationMessage(messages[messages.length - 1])
|
|
1908
|
+
? "**IMPORTANT:** The tool results above contain the information needed to answer the user's question. Please analyze these results and provide a clear, detailed response that directly answers what the user asked. Do not request more tools unless absolutely necessary."
|
|
1909
|
+
: undefined,
|
|
1910
|
+
});
|
|
1911
|
+
const budgetedContextInfo = promptBuild.budgetedContextInfo;
|
|
1912
|
+
const budgetedConversationHistory = promptBuild.budgetedConversationHistory;
|
|
1913
|
+
const prompt = promptBuild.prompt;
|
|
1914
|
+
const multimodalImageParts = resolveModelSupportsVision(modelId)
|
|
1915
|
+
? await resolveImageMessageParts(safeFilesInContext, workspacePath, isWebWorkspace, hasDesktopAuth(req))
|
|
1916
|
+
: [];
|
|
1917
|
+
const modelInput = multimodalImageParts.length > 0
|
|
1918
|
+
? [
|
|
1919
|
+
{
|
|
1920
|
+
role: "user",
|
|
1921
|
+
content: [
|
|
1922
|
+
{
|
|
1923
|
+
type: "text",
|
|
1924
|
+
text: prompt,
|
|
1925
|
+
},
|
|
1926
|
+
...multimodalImageParts,
|
|
1927
|
+
],
|
|
1928
|
+
},
|
|
1929
|
+
]
|
|
1930
|
+
: prompt;
|
|
1931
|
+
console.log("📏 Prompt budget", {
|
|
1932
|
+
modelInputTokenLimit,
|
|
1933
|
+
promptBudgetRatio,
|
|
1934
|
+
promptTokenBudget,
|
|
1935
|
+
contextInfoChars: budgetedContextInfo.length,
|
|
1936
|
+
promptChars: prompt.length,
|
|
1937
|
+
promptEstimatedTokens: promptBuild.promptEstimatedTokens,
|
|
1938
|
+
historyMessages: budgetedConversationHistory.length,
|
|
1939
|
+
contextFilesIncluded: safeFilesInContext.length,
|
|
1940
|
+
multimodalImagesIncluded: multimodalImageParts.length,
|
|
1941
|
+
});
|
|
1942
|
+
// Generate response with agent
|
|
1943
|
+
// When tools are executed on frontend (web or Tauri), we need to handle continuation
|
|
1944
|
+
// - If conversation history contains tool results, we want a TEXT RESPONSE, not more tools
|
|
1945
|
+
// - Otherwise, get tool calls and return them to frontend
|
|
1946
|
+
// Check whether the last message requires a synthesis-only continuation.
|
|
1947
|
+
const lastMessage = messages[messages.length - 1];
|
|
1948
|
+
const isSynthesisOnlyContinuation = isSynthesisOnlyContinuationMessage(lastMessage);
|
|
1949
|
+
if (typeof effectiveMessage === "string" &&
|
|
1950
|
+
isCodingObjectiveRequest(effectiveMessage, Array.isArray(toolResults) && toolResults.length > 0, isSynthesisOnlyContinuation) &&
|
|
1951
|
+
typeof agent.setObjective === "function") {
|
|
1952
|
+
await agent.setObjective(effectiveMessage, {
|
|
1953
|
+
threadId: mastraThreadId,
|
|
1954
|
+
resourceId: `iris-chat:${mastraThreadId}`,
|
|
1955
|
+
maxRuns: 12,
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
// Extract any relevant options from the request body
|
|
1959
|
+
// Note: isWebWorkspace already declared at line 89
|
|
1960
|
+
// Get maxSteps from request body (sourced from settings.agentMaxSteps in the client),
|
|
1961
|
+
// falling back to the desktop cap. The same hard cap applies to both web and desktop
|
|
1962
|
+
// so the user-configured value is always honoured.
|
|
1963
|
+
const requestedMaxSteps = req.body.maxSteps;
|
|
1964
|
+
const defaultMaxSteps = modelProfile.maxStepsCapDesktop;
|
|
1965
|
+
const hardCap = 100;
|
|
1966
|
+
let maxSteps = Math.max(1, Math.min(hardCap, typeof requestedMaxSteps === "number"
|
|
1967
|
+
? requestedMaxSteps
|
|
1968
|
+
: defaultMaxSteps));
|
|
1969
|
+
const maxToolCalls = maxSteps;
|
|
1970
|
+
// Cap completion tokens so providers don't bill for the model's full max
|
|
1971
|
+
// context window. Honors a per-request override, clamped to a safe range.
|
|
1972
|
+
const requestedMaxOutputTokens = req.body.maxOutputTokens;
|
|
1973
|
+
const maxOutputTokens = Math.max(256, Math.min(65536, typeof requestedMaxOutputTokens === "number" &&
|
|
1974
|
+
Number.isFinite(requestedMaxOutputTokens)
|
|
1975
|
+
? requestedMaxOutputTokens
|
|
1976
|
+
: modelProfile.maxOutputTokens));
|
|
1977
|
+
const toolCallBudget = {
|
|
1978
|
+
limit: maxToolCalls,
|
|
1979
|
+
admitted: 0,
|
|
1980
|
+
};
|
|
1981
|
+
const stopWhenToolBudgetReached = ({ steps, }) => toolCallBudget.admitted >= maxToolCalls ||
|
|
1982
|
+
toolCallBudget.stopReason === "repeated_call" ||
|
|
1983
|
+
steps.reduce((total, step) => total + (Array.isArray(step.toolCalls) ? step.toolCalls.length : 0), 0) >= maxToolCalls;
|
|
1984
|
+
const { createAgentRequestContext } = await loadAgentCoreModule();
|
|
1985
|
+
const agentRequestContext = createAgentRequestContext(enabledSkills, {
|
|
1986
|
+
workspaceMutationGenerationId,
|
|
1987
|
+
toolCallBudget,
|
|
1988
|
+
workspaceRoot: workspacePath,
|
|
1989
|
+
isWebWorkspace,
|
|
1990
|
+
gitDetected,
|
|
1991
|
+
modelId,
|
|
1992
|
+
contextFilesMeta: safeFilesInContext.map((file) => ({
|
|
1993
|
+
name: typeof file?.name === "string" ? file.name : undefined,
|
|
1994
|
+
path: typeof file?.path === "string" ? file.path : undefined,
|
|
1995
|
+
type: typeof file?.type === "string" ? file.type : undefined,
|
|
1996
|
+
size: typeof file?.size === "number" ? file.size : undefined,
|
|
1997
|
+
isImage: isLikelyImageFile(file),
|
|
1998
|
+
})),
|
|
1999
|
+
multimodalImageCount: multimodalImageParts.length,
|
|
2000
|
+
});
|
|
2001
|
+
const generateOptions = {
|
|
2002
|
+
maxSteps: maxSteps,
|
|
2003
|
+
maxOutputTokens,
|
|
2004
|
+
...(mastraMemoryScope ? { memory: mastraMemoryScope } : {}),
|
|
2005
|
+
toolChoice: "auto",
|
|
2006
|
+
toolCallConcurrency: 1,
|
|
2007
|
+
stopWhen: stopWhenToolBudgetReached,
|
|
2008
|
+
requestContext: agentRequestContext,
|
|
2009
|
+
abortSignal: turnAbortController.signal,
|
|
2010
|
+
};
|
|
2011
|
+
if (isSynthesisOnlyContinuation) {
|
|
2012
|
+
generateOptions.maxSteps = 1;
|
|
2013
|
+
generateOptions.toolChoice = "none";
|
|
2014
|
+
}
|
|
2015
|
+
const generateBackendOnlySynthesis = async (completedToolResults, stopReason) => {
|
|
2016
|
+
const completedResults = completedToolResults.length > 0
|
|
2017
|
+
? serializeToolResultsForContinuation(completedToolResults.map(({ name, result }) => ({
|
|
2018
|
+
name,
|
|
2019
|
+
result,
|
|
2020
|
+
})), "unknown_tool")
|
|
2021
|
+
: "No tools completed before the action budget was exhausted.";
|
|
2022
|
+
const stopReasonExplanation = stopReason === "repeated_call"
|
|
2023
|
+
? "The same tool was called with identical arguments several times in a row, so tool execution was stopped to avoid a repetitive loop. Briefly tell the user this happened (which tool, and that it was called repeatedly with the same arguments) before summarizing whatever was accomplished, based on the actual outcomes in the completed tool results below — do not assume the repeated calls failed."
|
|
2024
|
+
: stopReason === "empty_final_response"
|
|
2025
|
+
? "Tool execution finished, but the model returned no final response. Summarize the completed work from the tool results below in a concise final answer."
|
|
2026
|
+
: "The server-side tool-action budget is exhausted.";
|
|
2027
|
+
const synthesisPrompt = `${prompt}\n\n${stopReasonExplanation} Do not call any tools. Produce the final answer now using only the conversation and completed tool results below. Clearly distinguish verified findings from uncertainty.\n\nCompleted tool results:\n${completedResults}`;
|
|
2028
|
+
const synthesisOptions = {
|
|
2029
|
+
maxSteps: 1,
|
|
2030
|
+
maxOutputTokens,
|
|
2031
|
+
...(mastraMemoryScope ? { memory: mastraMemoryScope } : {}),
|
|
2032
|
+
toolChoice: "none",
|
|
2033
|
+
toolCallConcurrency: 1,
|
|
2034
|
+
requestContext: agentRequestContext,
|
|
2035
|
+
abortSignal: turnAbortController.signal,
|
|
2036
|
+
};
|
|
2037
|
+
if (activeHostSession) {
|
|
2038
|
+
return await waitForResultOrCancellation(generateWithSessionRetry(activeHostSession, synthesisPrompt, synthesisOptions, turnAbortController.signal, modelProfile.generateRetryAttempts));
|
|
2039
|
+
}
|
|
2040
|
+
return await waitForResultOrCancellation(generateWithRetry(agent, synthesisPrompt, synthesisOptions, turnAbortController.signal, modelProfile.generateRetryAttempts));
|
|
2041
|
+
};
|
|
2042
|
+
if (stream) {
|
|
2043
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
2044
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
2045
|
+
res.setHeader("Connection", "keep-alive");
|
|
2046
|
+
// Prevent idle intermediaries (and strict clients) from closing long-running
|
|
2047
|
+
// streams while tools execute without emitting deltas.
|
|
2048
|
+
const STREAM_KEEPALIVE_MS = 15_000;
|
|
2049
|
+
let keepAliveTimer = null;
|
|
2050
|
+
const stopKeepAlive = () => {
|
|
2051
|
+
if (keepAliveTimer) {
|
|
2052
|
+
clearInterval(keepAliveTimer);
|
|
2053
|
+
keepAliveTimer = null;
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
const startKeepAlive = () => {
|
|
2057
|
+
stopKeepAlive();
|
|
2058
|
+
keepAliveTimer = setInterval(() => {
|
|
2059
|
+
if (res.writableEnded || res.destroyed) {
|
|
2060
|
+
stopKeepAlive();
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
try {
|
|
2064
|
+
res.write(`: keepalive ${Date.now()}\n\n`);
|
|
2065
|
+
}
|
|
2066
|
+
catch {
|
|
2067
|
+
stopKeepAlive();
|
|
2068
|
+
}
|
|
2069
|
+
}, STREAM_KEEPALIVE_MS);
|
|
2070
|
+
};
|
|
2071
|
+
res.on("close", stopKeepAlive);
|
|
2072
|
+
startKeepAlive();
|
|
2073
|
+
const writeEvent = (event, data) => {
|
|
2074
|
+
res.write(`event: ${event}\n`);
|
|
2075
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
2076
|
+
};
|
|
2077
|
+
try {
|
|
2078
|
+
if (await isCancelled()) {
|
|
2079
|
+
await transitionToCancelled("before_stream_start");
|
|
2080
|
+
writeEvent("lifecycle", {
|
|
2081
|
+
runId: resolvedRunId,
|
|
2082
|
+
lifecycleState,
|
|
2083
|
+
stopReason,
|
|
2084
|
+
});
|
|
2085
|
+
writeEvent("done", {
|
|
2086
|
+
success: false,
|
|
2087
|
+
runId: resolvedRunId,
|
|
2088
|
+
lifecycleState,
|
|
2089
|
+
stopReason,
|
|
2090
|
+
cancelled: true,
|
|
2091
|
+
response: "Run cancelled",
|
|
2092
|
+
toolCalls: [],
|
|
2093
|
+
executedToolResults: [],
|
|
2094
|
+
suspendedTools: [],
|
|
2095
|
+
thoughtSteps: [],
|
|
2096
|
+
model: modelId,
|
|
2097
|
+
autoFixAttempted: false,
|
|
2098
|
+
autoFixFailureCount: 0,
|
|
2099
|
+
maxStepsReached: false,
|
|
2100
|
+
stepsUsed: 0,
|
|
2101
|
+
maxSteps,
|
|
2102
|
+
});
|
|
2103
|
+
stopKeepAlive();
|
|
2104
|
+
res.end();
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
await persistLifecycle("running", "none", "model_stream_started", {
|
|
2108
|
+
maxSteps,
|
|
2109
|
+
maxOutputTokens,
|
|
2110
|
+
});
|
|
2111
|
+
writeEvent("lifecycle", {
|
|
2112
|
+
runId: resolvedRunId,
|
|
2113
|
+
lifecycleState,
|
|
2114
|
+
stopReason,
|
|
2115
|
+
});
|
|
2116
|
+
const streamResultPromise = activeHostSession
|
|
2117
|
+
? (async () => {
|
|
2118
|
+
const hostStreamResult = await activeHostSession.sendStream({
|
|
2119
|
+
prompt: typeof modelInput === "string"
|
|
2120
|
+
? modelInput
|
|
2121
|
+
: "[multimodal-prompt]",
|
|
2122
|
+
metadata: {
|
|
2123
|
+
modelInput,
|
|
2124
|
+
generateOptions,
|
|
2125
|
+
},
|
|
2126
|
+
});
|
|
2127
|
+
const rawFromSession = hostStreamResult
|
|
2128
|
+
.rawStreamResult;
|
|
2129
|
+
if (rawFromSession) {
|
|
2130
|
+
void hostStreamResult.stream.cancel().catch((error) => {
|
|
2131
|
+
console.error("Failed to cancel host stream adapter:", error);
|
|
2132
|
+
});
|
|
2133
|
+
return rawFromSession;
|
|
2134
|
+
}
|
|
2135
|
+
throw new Error("Host session stream transport unavailable for SSE chunk pipeline");
|
|
2136
|
+
})()
|
|
2137
|
+
: agent.stream(modelInput, generateOptions);
|
|
2138
|
+
const streamResultOrCancellation = await waitForResultOrCancellation(streamResultPromise);
|
|
2139
|
+
if (isCancelledWaitResult(streamResultOrCancellation)) {
|
|
2140
|
+
await transitionToCancelled("before_stream_reader");
|
|
2141
|
+
writeEvent("lifecycle", {
|
|
2142
|
+
runId: resolvedRunId,
|
|
2143
|
+
lifecycleState,
|
|
2144
|
+
stopReason,
|
|
2145
|
+
});
|
|
2146
|
+
writeEvent("done", {
|
|
2147
|
+
success: false,
|
|
2148
|
+
runId: resolvedRunId,
|
|
2149
|
+
lifecycleState,
|
|
2150
|
+
stopReason,
|
|
2151
|
+
cancelled: true,
|
|
2152
|
+
response: "Run cancelled",
|
|
2153
|
+
toolCalls: [],
|
|
2154
|
+
executedToolResults: [],
|
|
2155
|
+
suspendedTools: [],
|
|
2156
|
+
thoughtSteps: [],
|
|
2157
|
+
model: modelId,
|
|
2158
|
+
autoFixAttempted: false,
|
|
2159
|
+
autoFixFailureCount: 0,
|
|
2160
|
+
maxStepsReached: false,
|
|
2161
|
+
stepsUsed: 0,
|
|
2162
|
+
maxSteps,
|
|
2163
|
+
});
|
|
2164
|
+
stopKeepAlive();
|
|
2165
|
+
res.end();
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
const streamResult = streamResultOrCancellation;
|
|
2169
|
+
const reader = streamResult.fullStream.getReader();
|
|
2170
|
+
const thoughtBuffer = [];
|
|
2171
|
+
const streamedPendingToolCalls = [];
|
|
2172
|
+
const streamedExecutedToolResults = [];
|
|
2173
|
+
const streamedSuspendedTools = [];
|
|
2174
|
+
const streamedToolCallArgs = new Map();
|
|
2175
|
+
const streamedAnonymousToolCallArgs = [];
|
|
2176
|
+
const emittedPendingCounts = new Map();
|
|
2177
|
+
let streamTokenUsage;
|
|
2178
|
+
let streamUsageSeenInChunks = false;
|
|
2179
|
+
const getPendingKey = (call) => {
|
|
2180
|
+
const signature = getToolCallSignature(call.name, call.args || {});
|
|
2181
|
+
if (call.toolCallId)
|
|
2182
|
+
return `id:${call.toolCallId}:${signature}`;
|
|
2183
|
+
return signature;
|
|
2184
|
+
};
|
|
2185
|
+
const getEmittedInvocationKey = (toolName, toolCallId, args) => toolCallId
|
|
2186
|
+
? `id:${toolCallId}:${getToolCallSignature(toolName, args)}`
|
|
2187
|
+
: getToolCallSignature(toolName, args);
|
|
2188
|
+
const emittedToolCallKeys = new Set();
|
|
2189
|
+
const persistedStreamToolActionKeys = new Set();
|
|
2190
|
+
const getPersistedStreamToolActionKey = (eventType, toolName, toolCallId, args) => `${eventType}:${getEmittedInvocationKey(toolName, toolCallId, args)}`;
|
|
2191
|
+
const resolveStreamToolResultArgs = (toolName, toolCallId, chunkArgs) => resolveToolResultArgs(toolName, toolCallId, chunkArgs, streamedToolCallArgs, streamedAnonymousToolCallArgs);
|
|
2192
|
+
while (true) {
|
|
2193
|
+
const streamReadOrCancellation = await waitForResultOrCancellation(reader.read());
|
|
2194
|
+
if (isCancelledWaitResult(streamReadOrCancellation)) {
|
|
2195
|
+
await reader.cancel().catch(() => undefined);
|
|
2196
|
+
await transitionToCancelled("stream_read_wait");
|
|
2197
|
+
writeEvent("lifecycle", {
|
|
2198
|
+
runId: resolvedRunId,
|
|
2199
|
+
lifecycleState,
|
|
2200
|
+
stopReason,
|
|
2201
|
+
});
|
|
2202
|
+
writeEvent("done", {
|
|
2203
|
+
success: false,
|
|
2204
|
+
runId: resolvedRunId,
|
|
2205
|
+
lifecycleState,
|
|
2206
|
+
stopReason,
|
|
2207
|
+
cancelled: true,
|
|
2208
|
+
response: "Run cancelled",
|
|
2209
|
+
toolCalls: [],
|
|
2210
|
+
executedToolResults: [],
|
|
2211
|
+
suspendedTools: streamedSuspendedTools,
|
|
2212
|
+
thoughtSteps: thoughtBuffer.length > 0 ? [thoughtBuffer.join("")] : [],
|
|
2213
|
+
model: modelId,
|
|
2214
|
+
autoFixAttempted: false,
|
|
2215
|
+
autoFixFailureCount: 0,
|
|
2216
|
+
maxStepsReached: false,
|
|
2217
|
+
stepsUsed: 0,
|
|
2218
|
+
maxSteps,
|
|
2219
|
+
});
|
|
2220
|
+
stopKeepAlive();
|
|
2221
|
+
res.end();
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
const { value, done } = streamReadOrCancellation;
|
|
2225
|
+
if (done)
|
|
2226
|
+
break;
|
|
2227
|
+
if (await isCancelled()) {
|
|
2228
|
+
await requestTurnCancellation();
|
|
2229
|
+
await reader.cancel().catch(() => undefined);
|
|
2230
|
+
await transitionToCancelled("stream_iteration");
|
|
2231
|
+
writeEvent("lifecycle", {
|
|
2232
|
+
runId: resolvedRunId,
|
|
2233
|
+
lifecycleState,
|
|
2234
|
+
stopReason,
|
|
2235
|
+
});
|
|
2236
|
+
writeEvent("done", {
|
|
2237
|
+
success: false,
|
|
2238
|
+
runId: resolvedRunId,
|
|
2239
|
+
lifecycleState,
|
|
2240
|
+
stopReason,
|
|
2241
|
+
cancelled: true,
|
|
2242
|
+
response: "Run cancelled",
|
|
2243
|
+
toolCalls: [],
|
|
2244
|
+
executedToolResults: [],
|
|
2245
|
+
suspendedTools: streamedSuspendedTools,
|
|
2246
|
+
thoughtSteps: thoughtBuffer.length > 0 ? [thoughtBuffer.join("")] : [],
|
|
2247
|
+
model: modelId,
|
|
2248
|
+
autoFixAttempted: false,
|
|
2249
|
+
autoFixFailureCount: 0,
|
|
2250
|
+
maxStepsReached: false,
|
|
2251
|
+
stepsUsed: 0,
|
|
2252
|
+
maxSteps,
|
|
2253
|
+
});
|
|
2254
|
+
stopKeepAlive();
|
|
2255
|
+
res.end();
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
const chunk = value;
|
|
2259
|
+
if (!chunk?.type)
|
|
2260
|
+
continue;
|
|
2261
|
+
if (chunk.payload) {
|
|
2262
|
+
const chunkUsage = extractTokenUsageFromChunkPayload(chunk.payload);
|
|
2263
|
+
if (chunkUsage) {
|
|
2264
|
+
streamUsageSeenInChunks = true;
|
|
2265
|
+
}
|
|
2266
|
+
streamTokenUsage = mergeTokenUsage(streamTokenUsage, chunkUsage);
|
|
2267
|
+
}
|
|
2268
|
+
if (chunk.type === "reasoning-delta") {
|
|
2269
|
+
const delta = String(chunk.payload?.text || "");
|
|
2270
|
+
if (delta) {
|
|
2271
|
+
thoughtBuffer.push(delta);
|
|
2272
|
+
writeEvent("thought_delta", { text: delta });
|
|
2273
|
+
}
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
if (chunk.type === "text-delta") {
|
|
2277
|
+
const delta = String(chunk.payload?.text || "");
|
|
2278
|
+
if (delta) {
|
|
2279
|
+
writeEvent("text_delta", { text: delta });
|
|
2280
|
+
}
|
|
2281
|
+
continue;
|
|
2282
|
+
}
|
|
2283
|
+
if (chunk.type === "tool-call") {
|
|
2284
|
+
const toolName = chunk.payload?.toolName;
|
|
2285
|
+
if (typeof toolName !== "string" || toolName.length === 0) {
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
const pendingCall = {
|
|
2289
|
+
name: toolName,
|
|
2290
|
+
args: chunk.payload?.args || {},
|
|
2291
|
+
toolCallId: typeof chunk.payload?.toolCallId === "string"
|
|
2292
|
+
? chunk.payload.toolCallId
|
|
2293
|
+
: undefined,
|
|
2294
|
+
};
|
|
2295
|
+
streamedPendingToolCalls.push(pendingCall);
|
|
2296
|
+
if (pendingCall.toolCallId) {
|
|
2297
|
+
streamedToolCallArgs.set(pendingCall.toolCallId, pendingCall.args || {});
|
|
2298
|
+
}
|
|
2299
|
+
else {
|
|
2300
|
+
streamedAnonymousToolCallArgs.push({
|
|
2301
|
+
toolName,
|
|
2302
|
+
args: pendingCall.args || {},
|
|
2303
|
+
consumed: false,
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
const persistedActionKey = getPersistedStreamToolActionKey("tool_call", toolName, pendingCall.toolCallId, pendingCall.args || {});
|
|
2307
|
+
persistedStreamToolActionKeys.add(persistedActionKey);
|
|
2308
|
+
await persistToolEvent("tool_call", {
|
|
2309
|
+
name: toolName,
|
|
2310
|
+
toolName,
|
|
2311
|
+
args: pendingCall.args,
|
|
2312
|
+
toolCallId: pendingCall.toolCallId,
|
|
2313
|
+
status: "pending",
|
|
2314
|
+
}, "waiting_tool");
|
|
2315
|
+
const normalized = normalizeToolLifecycle(streamedPendingToolCalls, streamedExecutedToolResults);
|
|
2316
|
+
// Anonymous calls sharing a signature are distinct invocations,
|
|
2317
|
+
// so track how many of each key were emitted instead of a boolean.
|
|
2318
|
+
const pendingCountsInSnapshot = new Map();
|
|
2319
|
+
for (const call of normalized.pendingToolCalls) {
|
|
2320
|
+
const key = getPendingKey(call);
|
|
2321
|
+
const occurrence = (pendingCountsInSnapshot.get(key) || 0) + 1;
|
|
2322
|
+
pendingCountsInSnapshot.set(key, occurrence);
|
|
2323
|
+
if (occurrence <= (emittedPendingCounts.get(key) || 0))
|
|
2324
|
+
continue;
|
|
2325
|
+
emittedPendingCounts.set(key, occurrence);
|
|
2326
|
+
if (call.toolCallId) {
|
|
2327
|
+
emittedToolCallKeys.add(getEmittedInvocationKey(call.name, call.toolCallId, call.args || {}));
|
|
2328
|
+
}
|
|
2329
|
+
writeEvent("tool_call", {
|
|
2330
|
+
name: call.name,
|
|
2331
|
+
args: call.args,
|
|
2332
|
+
toolCallId: typeof call.toolCallId === "string"
|
|
2333
|
+
? call.toolCallId
|
|
2334
|
+
: undefined,
|
|
2335
|
+
status: "pending",
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
if (chunk.type === "tool-result") {
|
|
2341
|
+
const toolName = chunk.payload?.toolName;
|
|
2342
|
+
if (typeof toolName !== "string" || toolName.length === 0) {
|
|
2343
|
+
continue;
|
|
2344
|
+
}
|
|
2345
|
+
const toolResult = chunk.payload?.result ??
|
|
2346
|
+
chunk.payload?.output ??
|
|
2347
|
+
chunk.payload?.content ??
|
|
2348
|
+
chunk.payload?.data;
|
|
2349
|
+
const safeToolResult = redactToolResult(toolResult).result;
|
|
2350
|
+
const toolCallId = typeof chunk.payload?.toolCallId === "string"
|
|
2351
|
+
? chunk.payload.toolCallId
|
|
2352
|
+
: undefined;
|
|
2353
|
+
const resultArgs = resolveStreamToolResultArgs(toolName, toolCallId, chunk.payload?.args);
|
|
2354
|
+
console.log("[DIFF-TRACE] API raw tool result", {
|
|
2355
|
+
toolName,
|
|
2356
|
+
toolCallId: chunk.payload?.toolCallId,
|
|
2357
|
+
resultType: typeof toolResult,
|
|
2358
|
+
resultKeys: toolResult && typeof toolResult === "object"
|
|
2359
|
+
? Object.keys(toolResult)
|
|
2360
|
+
: [],
|
|
2361
|
+
});
|
|
2362
|
+
streamedExecutedToolResults.push({
|
|
2363
|
+
name: toolName,
|
|
2364
|
+
args: resultArgs,
|
|
2365
|
+
result: safeToolResult,
|
|
2366
|
+
toolCallId,
|
|
2367
|
+
});
|
|
2368
|
+
const emittedInvocationKey = getEmittedInvocationKey(toolName, toolCallId, resultArgs);
|
|
2369
|
+
if (toolCallId && !emittedToolCallKeys.has(emittedInvocationKey)) {
|
|
2370
|
+
emittedToolCallKeys.add(emittedInvocationKey);
|
|
2371
|
+
writeEvent("tool_call", {
|
|
2372
|
+
name: toolName,
|
|
2373
|
+
args: resultArgs,
|
|
2374
|
+
toolCallId,
|
|
2375
|
+
status: "pending",
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
writeEvent("tool_result", {
|
|
2379
|
+
name: toolName,
|
|
2380
|
+
args: resultArgs,
|
|
2381
|
+
result: safeToolResult,
|
|
2382
|
+
toolCallId,
|
|
2383
|
+
status: resolveToolExecutionStatus(safeToolResult),
|
|
2384
|
+
});
|
|
2385
|
+
const persistedResultActionKey = getPersistedStreamToolActionKey("tool_result", toolName, toolCallId, resultArgs);
|
|
2386
|
+
persistedStreamToolActionKeys.add(persistedResultActionKey);
|
|
2387
|
+
await persistToolEvent("tool_result", {
|
|
2388
|
+
name: toolName,
|
|
2389
|
+
toolName,
|
|
2390
|
+
args: resultArgs,
|
|
2391
|
+
result: safeToolResult,
|
|
2392
|
+
toolCallId,
|
|
2393
|
+
status: resolveToolExecutionStatus(safeToolResult),
|
|
2394
|
+
}, "running");
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
if (chunk.type === "tool-suspended" ||
|
|
2398
|
+
chunk.type === "tool_suspended") {
|
|
2399
|
+
const toolName = chunk.payload?.toolName;
|
|
2400
|
+
if (typeof toolName !== "string" || toolName.length === 0) {
|
|
2401
|
+
continue;
|
|
2402
|
+
}
|
|
2403
|
+
const suspended = {
|
|
2404
|
+
name: toolName,
|
|
2405
|
+
toolCallId: typeof chunk.payload?.toolCallId === "string"
|
|
2406
|
+
? chunk.payload.toolCallId
|
|
2407
|
+
: undefined,
|
|
2408
|
+
suspendPayload: chunk.payload?.suspendPayload &&
|
|
2409
|
+
typeof chunk.payload.suspendPayload === "object"
|
|
2410
|
+
? chunk.payload.suspendPayload
|
|
2411
|
+
: undefined,
|
|
2412
|
+
};
|
|
2413
|
+
streamedSuspendedTools.push(suspended);
|
|
2414
|
+
writeEvent("tool_suspended", {
|
|
2415
|
+
name: suspended.name,
|
|
2416
|
+
toolCallId: suspended.toolCallId,
|
|
2417
|
+
suspendPayload: suspended.suspendPayload,
|
|
2418
|
+
});
|
|
2419
|
+
void safePersistRunLifecycleEvent({
|
|
2420
|
+
runId: resolvedRunId,
|
|
2421
|
+
lifecycleState: "waiting_user_input",
|
|
2422
|
+
stopReason: "awaiting_user_input",
|
|
2423
|
+
eventType: "tool_suspended",
|
|
2424
|
+
payload: {
|
|
2425
|
+
toolName: suspended.name,
|
|
2426
|
+
toolCallId: suspended.toolCallId,
|
|
2427
|
+
},
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
const finalTextOrCancellation = await waitForResultOrCancellation(streamResult.text);
|
|
2432
|
+
if (isCancelledWaitResult(finalTextOrCancellation)) {
|
|
2433
|
+
await transitionToCancelled("before_stream_final_text");
|
|
2434
|
+
writeEvent("lifecycle", {
|
|
2435
|
+
runId: resolvedRunId,
|
|
2436
|
+
lifecycleState,
|
|
2437
|
+
stopReason,
|
|
2438
|
+
});
|
|
2439
|
+
writeEvent("done", {
|
|
2440
|
+
success: false,
|
|
2441
|
+
runId: resolvedRunId,
|
|
2442
|
+
lifecycleState,
|
|
2443
|
+
stopReason,
|
|
2444
|
+
cancelled: true,
|
|
2445
|
+
response: "Run cancelled",
|
|
2446
|
+
toolCalls: [],
|
|
2447
|
+
executedToolResults: [],
|
|
2448
|
+
suspendedTools: streamedSuspendedTools,
|
|
2449
|
+
thoughtSteps: thoughtBuffer.length > 0 ? [thoughtBuffer.join("")] : [],
|
|
2450
|
+
model: modelId,
|
|
2451
|
+
autoFixAttempted: false,
|
|
2452
|
+
autoFixFailureCount: 0,
|
|
2453
|
+
maxStepsReached: false,
|
|
2454
|
+
stepsUsed: 0,
|
|
2455
|
+
maxSteps,
|
|
2456
|
+
});
|
|
2457
|
+
stopKeepAlive();
|
|
2458
|
+
res.end();
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
const finalText = finalTextOrCancellation;
|
|
2462
|
+
const streamedToolCallsOrCancellation = await waitForResultOrCancellation(streamResult.toolCalls);
|
|
2463
|
+
if (isCancelledWaitResult(streamedToolCallsOrCancellation)) {
|
|
2464
|
+
await transitionToCancelled("before_stream_tool_calls");
|
|
2465
|
+
writeEvent("lifecycle", {
|
|
2466
|
+
runId: resolvedRunId,
|
|
2467
|
+
lifecycleState,
|
|
2468
|
+
stopReason,
|
|
2469
|
+
});
|
|
2470
|
+
writeEvent("done", {
|
|
2471
|
+
success: false,
|
|
2472
|
+
runId: resolvedRunId,
|
|
2473
|
+
lifecycleState,
|
|
2474
|
+
stopReason,
|
|
2475
|
+
cancelled: true,
|
|
2476
|
+
response: "Run cancelled",
|
|
2477
|
+
toolCalls: [],
|
|
2478
|
+
executedToolResults: [],
|
|
2479
|
+
suspendedTools: streamedSuspendedTools,
|
|
2480
|
+
thoughtSteps: thoughtBuffer.length > 0 ? [thoughtBuffer.join("")] : [],
|
|
2481
|
+
model: modelId,
|
|
2482
|
+
autoFixAttempted: false,
|
|
2483
|
+
autoFixFailureCount: 0,
|
|
2484
|
+
maxStepsReached: false,
|
|
2485
|
+
stepsUsed: 0,
|
|
2486
|
+
maxSteps,
|
|
2487
|
+
});
|
|
2488
|
+
stopKeepAlive();
|
|
2489
|
+
res.end();
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
const streamedToolCalls = streamedToolCallsOrCancellation;
|
|
2493
|
+
const streamStepsOrCancellation = await waitForResultOrCancellation(Promise.resolve(streamResult.steps).catch(() => undefined));
|
|
2494
|
+
if (isCancelledWaitResult(streamStepsOrCancellation)) {
|
|
2495
|
+
await transitionToCancelled("before_stream_steps");
|
|
2496
|
+
writeEvent("lifecycle", {
|
|
2497
|
+
runId: resolvedRunId,
|
|
2498
|
+
lifecycleState,
|
|
2499
|
+
stopReason,
|
|
2500
|
+
});
|
|
2501
|
+
writeEvent("done", {
|
|
2502
|
+
success: false,
|
|
2503
|
+
runId: resolvedRunId,
|
|
2504
|
+
lifecycleState,
|
|
2505
|
+
stopReason,
|
|
2506
|
+
cancelled: true,
|
|
2507
|
+
response: "Run cancelled",
|
|
2508
|
+
toolCalls: [],
|
|
2509
|
+
executedToolResults: [],
|
|
2510
|
+
suspendedTools: streamedSuspendedTools,
|
|
2511
|
+
thoughtSteps: thoughtBuffer.length > 0 ? [thoughtBuffer.join("")] : [],
|
|
2512
|
+
model: modelId,
|
|
2513
|
+
autoFixAttempted: false,
|
|
2514
|
+
autoFixFailureCount: 0,
|
|
2515
|
+
maxStepsReached: false,
|
|
2516
|
+
stepsUsed: 0,
|
|
2517
|
+
maxSteps,
|
|
2518
|
+
});
|
|
2519
|
+
stopKeepAlive();
|
|
2520
|
+
res.end();
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
const streamSteps = streamStepsOrCancellation;
|
|
2524
|
+
const streamStepsUsed = Array.isArray(streamSteps)
|
|
2525
|
+
? streamSteps.length
|
|
2526
|
+
: 0;
|
|
2527
|
+
const streamToolCallsFromSteps = Array.isArray(streamSteps)
|
|
2528
|
+
? streamSteps.reduce((total, step) => total +
|
|
2529
|
+
(Array.isArray(step.toolCalls) ? step.toolCalls.length : 0), 0)
|
|
2530
|
+
: undefined;
|
|
2531
|
+
const streamLastStep = Array.isArray(streamSteps)
|
|
2532
|
+
? streamSteps[streamSteps.length - 1]
|
|
2533
|
+
: undefined;
|
|
2534
|
+
const streamLastStepHadToolCalls = Array.isArray(streamLastStep?.toolCalls) &&
|
|
2535
|
+
streamLastStep.toolCalls.length > 0;
|
|
2536
|
+
const streamUsageRawOrCancellation = await waitForResultOrCancellation(Promise.resolve(streamResult.usage).catch(() => undefined));
|
|
2537
|
+
if (isCancelledWaitResult(streamUsageRawOrCancellation)) {
|
|
2538
|
+
await transitionToCancelled("before_stream_usage");
|
|
2539
|
+
writeEvent("lifecycle", {
|
|
2540
|
+
runId: resolvedRunId,
|
|
2541
|
+
lifecycleState,
|
|
2542
|
+
stopReason,
|
|
2543
|
+
});
|
|
2544
|
+
writeEvent("done", {
|
|
2545
|
+
success: false,
|
|
2546
|
+
runId: resolvedRunId,
|
|
2547
|
+
lifecycleState,
|
|
2548
|
+
stopReason,
|
|
2549
|
+
cancelled: true,
|
|
2550
|
+
response: "Run cancelled",
|
|
2551
|
+
toolCalls: [],
|
|
2552
|
+
executedToolResults: [],
|
|
2553
|
+
suspendedTools: streamedSuspendedTools,
|
|
2554
|
+
thoughtSteps: thoughtBuffer.length > 0 ? [thoughtBuffer.join("")] : [],
|
|
2555
|
+
model: modelId,
|
|
2556
|
+
autoFixAttempted: false,
|
|
2557
|
+
autoFixFailureCount: 0,
|
|
2558
|
+
maxStepsReached: false,
|
|
2559
|
+
stepsUsed: 0,
|
|
2560
|
+
maxSteps,
|
|
2561
|
+
});
|
|
2562
|
+
stopKeepAlive();
|
|
2563
|
+
res.end();
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2566
|
+
const streamUsageFromResult = normalizeTokenUsage(streamUsageRawOrCancellation);
|
|
2567
|
+
const streamUsageSeenInFinal = Boolean(streamUsageFromResult);
|
|
2568
|
+
streamTokenUsage = mergeTokenUsage(streamTokenUsage, streamUsageFromResult);
|
|
2569
|
+
const streamUsageSource = streamUsageSeenInFinal
|
|
2570
|
+
? "final"
|
|
2571
|
+
: streamUsageSeenInChunks
|
|
2572
|
+
? "stream"
|
|
2573
|
+
: "none";
|
|
2574
|
+
logTokenUsageSource({
|
|
2575
|
+
mode: "stream",
|
|
2576
|
+
source: streamUsageSource,
|
|
2577
|
+
usage: streamTokenUsage,
|
|
2578
|
+
modelId,
|
|
2579
|
+
});
|
|
2580
|
+
const typedStreamedToolCalls = (streamedToolCalls || []);
|
|
2581
|
+
const mappedToolCalls = typedStreamedToolCalls.reduce((calls, call) => {
|
|
2582
|
+
const toolName = call?.toolName;
|
|
2583
|
+
if (typeof toolName !== "string" || toolName.length === 0) {
|
|
2584
|
+
return calls;
|
|
2585
|
+
}
|
|
2586
|
+
calls.push({
|
|
2587
|
+
name: toolName,
|
|
2588
|
+
args: call.args || {},
|
|
2589
|
+
toolCallId: typeof call.toolCallId === "string"
|
|
2590
|
+
? call.toolCallId
|
|
2591
|
+
: undefined,
|
|
2592
|
+
});
|
|
2593
|
+
return calls;
|
|
2594
|
+
}, []);
|
|
2595
|
+
const processedStreamToolResults = Array.isArray(streamSteps)
|
|
2596
|
+
? extractProcessedToolResults(streamSteps)
|
|
2597
|
+
: [];
|
|
2598
|
+
const toolResultMetadata = new Map([...streamedExecutedToolResults, ...processedStreamToolResults]
|
|
2599
|
+
.filter((entry) => entry.toolCallId)
|
|
2600
|
+
.map((entry) => [entry.toolCallId, entry]));
|
|
2601
|
+
const bridgedStreamToolResults = agent
|
|
2602
|
+
.takeProcessedWorkspaceResults?.(workspaceMutationGenerationId, Array.from(toolResultMetadata.keys()))
|
|
2603
|
+
.map(({ toolCallId, result }) => {
|
|
2604
|
+
const metadata = toolResultMetadata.get(toolCallId);
|
|
2605
|
+
return {
|
|
2606
|
+
name: metadata?.name || "workspaceMutation",
|
|
2607
|
+
args: metadata?.args || {},
|
|
2608
|
+
toolCallId,
|
|
2609
|
+
result,
|
|
2610
|
+
};
|
|
2611
|
+
}) || [];
|
|
2612
|
+
const reconciledStreamLifecycleSnapshots = reconcileToolLifecycleSnapshots(streamedPendingToolCalls, mappedToolCalls, streamedExecutedToolResults, [...processedStreamToolResults, ...bridgedStreamToolResults]);
|
|
2613
|
+
const reconciledStreamToolResults = reconciledStreamLifecycleSnapshots.executedToolResults;
|
|
2614
|
+
console.log("[DIFF-TRACE] API reconciled tool results", {
|
|
2615
|
+
rawCount: streamedExecutedToolResults.length,
|
|
2616
|
+
processedCount: processedStreamToolResults.length,
|
|
2617
|
+
bridgedCount: bridgedStreamToolResults.length,
|
|
2618
|
+
resultKeys: reconciledStreamToolResults.map((entry) => entry.result && typeof entry.result === "object"
|
|
2619
|
+
? Object.keys(entry.result)
|
|
2620
|
+
: []),
|
|
2621
|
+
});
|
|
2622
|
+
const streamInputToolCalls = reconciledStreamLifecycleSnapshots.pendingToolCalls;
|
|
2623
|
+
console.log("DEBUG_STREAM_LIFECYCLE_INPUT", {
|
|
2624
|
+
streamedPendingCount: streamInputToolCalls.length,
|
|
2625
|
+
streamedPendingToolNames: streamInputToolCalls.map((call) => call.name),
|
|
2626
|
+
reconciledResultCount: reconciledStreamToolResults.length,
|
|
2627
|
+
reconciledResultKeys: reconciledStreamToolResults.map((entry) => entry.result && typeof entry.result === "object"
|
|
2628
|
+
? Object.keys(entry.result)
|
|
2629
|
+
: []),
|
|
2630
|
+
});
|
|
2631
|
+
const normalizedStreamLifecycle = normalizeToolLifecycle(streamInputToolCalls, reconciledStreamToolResults);
|
|
2632
|
+
let normalizedStreamToolCalls = normalizedStreamLifecycle.pendingToolCalls.map((call) => ({
|
|
2633
|
+
name: call.name,
|
|
2634
|
+
args: call.args,
|
|
2635
|
+
toolCallId: call.toolCallId,
|
|
2636
|
+
status: "pending",
|
|
2637
|
+
}));
|
|
2638
|
+
const normalizedStreamExecutedResults = normalizedStreamLifecycle.executedToolResults.map((toolResult) => ({
|
|
2639
|
+
name: toolResult.name,
|
|
2640
|
+
args: toolResult.args,
|
|
2641
|
+
result: toolResult.result,
|
|
2642
|
+
toolCallId: toolResult.toolCallId,
|
|
2643
|
+
status: resolveToolExecutionStatus(toolResult.result),
|
|
2644
|
+
}));
|
|
2645
|
+
const normalizedStreamToolCallsUsed = countUniqueToolCalls(streamInputToolCalls, reconciledStreamToolResults);
|
|
2646
|
+
const streamToolCallsUsed = typeof streamToolCallsFromSteps === "number"
|
|
2647
|
+
? Math.max(streamToolCallsFromSteps, normalizedStreamToolCallsUsed)
|
|
2648
|
+
: normalizedStreamToolCallsUsed;
|
|
2649
|
+
const hasStreamToolActivity = normalizedStreamToolCalls.length > 0 ||
|
|
2650
|
+
normalizedStreamExecutedResults.length > 0 ||
|
|
2651
|
+
Array.isArray(streamSteps);
|
|
2652
|
+
const hasStreamPendingToolCalls = normalizedStreamToolCalls.length > 0;
|
|
2653
|
+
const hasStreamBudgetOverflow = streamToolCallsUsed > maxToolCalls ||
|
|
2654
|
+
toolCallBudget.stopReason === "repeated_call";
|
|
2655
|
+
const streamBudgetReached = hasStreamBudgetOverflow ||
|
|
2656
|
+
(streamToolCallsUsed >= maxToolCalls &&
|
|
2657
|
+
(hasStreamPendingToolCalls || streamLastStepHadToolCalls));
|
|
2658
|
+
console.log("DEBUG_STREAM_BUDGET", {
|
|
2659
|
+
streamToolCallsUsed,
|
|
2660
|
+
maxToolCalls,
|
|
2661
|
+
toolCallBudgetStopReason: toolCallBudget.stopReason,
|
|
2662
|
+
streamLastStepHadToolCalls,
|
|
2663
|
+
hasStreamToolActivity,
|
|
2664
|
+
pendingToolCallCount: normalizedStreamToolCalls.length,
|
|
2665
|
+
executedResultCount: normalizedStreamExecutedResults.length,
|
|
2666
|
+
streamBudgetReached,
|
|
2667
|
+
finalTextPresent: typeof finalText === "string" && finalText.trim().length > 0,
|
|
2668
|
+
});
|
|
2669
|
+
const streamPendingConfirmations = normalizedStreamExecutedResults.filter((toolResult) => resolveToolExecutionStatus(toolResult.result) ===
|
|
2670
|
+
"pending_confirmation");
|
|
2671
|
+
let finalResponseText = finalText;
|
|
2672
|
+
let backendSynthesisPerformed = false;
|
|
2673
|
+
const streamEndedWithoutFinalText = typeof finalText !== "string" || finalText.trim().length === 0;
|
|
2674
|
+
const hasStreamTerminalToolResults = hasTerminalToolResults(normalizedStreamExecutedResults);
|
|
2675
|
+
const hasStreamNonterminalToolResults = hasNonterminalToolResults(normalizedStreamExecutedResults);
|
|
2676
|
+
const hasStreamResultSnapshots = normalizedStreamExecutedResults.length > 0;
|
|
2677
|
+
const shouldSynthesizeAfterCompletedToolWork = streamedSuspendedTools.length === 0 &&
|
|
2678
|
+
streamPendingConfirmations.length === 0 &&
|
|
2679
|
+
((hasStreamPendingToolCalls &&
|
|
2680
|
+
!hasStreamNonterminalToolResults &&
|
|
2681
|
+
(hasStreamBudgetOverflow ||
|
|
2682
|
+
toolCallBudget.stopReason === "repeated_call" ||
|
|
2683
|
+
streamToolCallsUsed >= maxToolCalls)) ||
|
|
2684
|
+
(hasStreamTerminalToolResults &&
|
|
2685
|
+
streamEndedWithoutFinalText &&
|
|
2686
|
+
!hasStreamPendingToolCalls));
|
|
2687
|
+
if (shouldSynthesizeAfterCompletedToolWork) {
|
|
2688
|
+
const synthesisStopReason = streamBudgetReached ||
|
|
2689
|
+
toolCallBudget.stopReason === "repeated_call" ||
|
|
2690
|
+
streamToolCallsUsed >= maxToolCalls
|
|
2691
|
+
? (toolCallBudget.stopReason ?? "limit")
|
|
2692
|
+
: "empty_final_response";
|
|
2693
|
+
const synthesisResultOrCancellation = await generateBackendOnlySynthesis(normalizedStreamExecutedResults, synthesisStopReason);
|
|
2694
|
+
if (isCancelledWaitResult(synthesisResultOrCancellation)) {
|
|
2695
|
+
await transitionToCancelled("during_stream_backend_synthesis");
|
|
2696
|
+
writeEvent("lifecycle", {
|
|
2697
|
+
runId: resolvedRunId,
|
|
2698
|
+
lifecycleState,
|
|
2699
|
+
stopReason,
|
|
2700
|
+
});
|
|
2701
|
+
writeEvent("done", {
|
|
2702
|
+
success: false,
|
|
2703
|
+
runId: resolvedRunId,
|
|
2704
|
+
lifecycleState,
|
|
2705
|
+
stopReason,
|
|
2706
|
+
cancelled: true,
|
|
2707
|
+
response: "Run cancelled",
|
|
2708
|
+
toolCalls: [],
|
|
2709
|
+
executedToolResults: [],
|
|
2710
|
+
suspendedTools: streamedSuspendedTools,
|
|
2711
|
+
thoughtSteps: thoughtBuffer.length > 0 ? [thoughtBuffer.join("")] : [],
|
|
2712
|
+
model: modelId,
|
|
2713
|
+
autoFixAttempted: false,
|
|
2714
|
+
autoFixFailureCount: 0,
|
|
2715
|
+
maxStepsReached: false,
|
|
2716
|
+
stepsUsed: 0,
|
|
2717
|
+
maxSteps,
|
|
2718
|
+
});
|
|
2719
|
+
stopKeepAlive();
|
|
2720
|
+
res.end();
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
const synthesisResult = synthesisResultOrCancellation;
|
|
2724
|
+
const synthesizedText = typeof synthesisResult.text === "string" &&
|
|
2725
|
+
synthesisResult.text.trim().length > 0
|
|
2726
|
+
? synthesisResult.text
|
|
2727
|
+
: null;
|
|
2728
|
+
if (!synthesizedText && streamEndedWithoutFinalText) {
|
|
2729
|
+
throw new Error("The agent completed its tool work but returned no final response.");
|
|
2730
|
+
}
|
|
2731
|
+
if (synthesizedText) {
|
|
2732
|
+
finalResponseText = synthesizedText;
|
|
2733
|
+
normalizedStreamToolCalls = [];
|
|
2734
|
+
backendSynthesisPerformed = true;
|
|
2735
|
+
}
|
|
2736
|
+
streamTokenUsage = mergeTokenUsage(streamTokenUsage, normalizeTokenUsage(synthesisResult.usage));
|
|
2737
|
+
}
|
|
2738
|
+
let streamLifecycleState = "succeeded";
|
|
2739
|
+
let streamStopReason = "completed";
|
|
2740
|
+
if (streamedSuspendedTools.length > 0) {
|
|
2741
|
+
streamLifecycleState = "waiting_user_input";
|
|
2742
|
+
streamStopReason = "awaiting_user_input";
|
|
2743
|
+
}
|
|
2744
|
+
else if (streamPendingConfirmations.length > 0) {
|
|
2745
|
+
streamLifecycleState = "waiting_confirmation";
|
|
2746
|
+
streamStopReason = "awaiting_approval";
|
|
2747
|
+
}
|
|
2748
|
+
else if (normalizedStreamToolCalls.length > 0) {
|
|
2749
|
+
streamLifecycleState = "waiting_tool";
|
|
2750
|
+
streamStopReason = "none";
|
|
2751
|
+
}
|
|
2752
|
+
else if (streamBudgetReached && !backendSynthesisPerformed) {
|
|
2753
|
+
streamLifecycleState = "paused";
|
|
2754
|
+
streamStopReason = "max_steps_reached";
|
|
2755
|
+
}
|
|
2756
|
+
const finalStreamReplayActions = buildOrderedPersistedToolActions(streamSteps, streamInputToolCalls.map((call) => ({
|
|
2757
|
+
name: call.name,
|
|
2758
|
+
args: call.args,
|
|
2759
|
+
toolCallId: call.toolCallId,
|
|
2760
|
+
status: "pending",
|
|
2761
|
+
})), normalizedStreamExecutedResults, (toolName, args) => sanitizeToolArgsForWorkspace(toolName, args, isWebWorkspace));
|
|
2762
|
+
for (const action of finalStreamReplayActions) {
|
|
2763
|
+
const persistedActionKey = getPersistedStreamToolActionKey(action.eventType, action.name, action.toolCallId, action.args);
|
|
2764
|
+
if (persistedStreamToolActionKeys.has(persistedActionKey)) {
|
|
2765
|
+
continue;
|
|
2766
|
+
}
|
|
2767
|
+
persistedStreamToolActionKeys.add(persistedActionKey);
|
|
2768
|
+
await persistToolEvent(action.eventType, {
|
|
2769
|
+
name: action.name,
|
|
2770
|
+
toolName: action.name,
|
|
2771
|
+
args: action.args,
|
|
2772
|
+
...(action.eventType === "tool_result"
|
|
2773
|
+
? { result: action.result }
|
|
2774
|
+
: {}),
|
|
2775
|
+
toolCallId: action.toolCallId,
|
|
2776
|
+
status: action.status,
|
|
2777
|
+
}, "running");
|
|
2778
|
+
}
|
|
2779
|
+
await persistLifecycle(streamLifecycleState, streamStopReason, "stream_completed", {
|
|
2780
|
+
pendingToolCalls: normalizedStreamToolCalls.length,
|
|
2781
|
+
suspendedTools: streamedSuspendedTools.length,
|
|
2782
|
+
pendingConfirmations: streamPendingConfirmations.length,
|
|
2783
|
+
stepsUsed: streamStepsUsed,
|
|
2784
|
+
toolCallsUsed: streamToolCallsUsed,
|
|
2785
|
+
budgetReached: streamBudgetReached,
|
|
2786
|
+
toolBudgetStopReason: toolCallBudget.stopReason,
|
|
2787
|
+
});
|
|
2788
|
+
writeEvent("lifecycle", {
|
|
2789
|
+
runId: resolvedRunId,
|
|
2790
|
+
lifecycleState,
|
|
2791
|
+
stopReason,
|
|
2792
|
+
});
|
|
2793
|
+
writeEvent("done", {
|
|
2794
|
+
success: true,
|
|
2795
|
+
runId: resolvedRunId,
|
|
2796
|
+
lifecycleState,
|
|
2797
|
+
stopReason,
|
|
2798
|
+
response: finalResponseText,
|
|
2799
|
+
toolCalls: normalizedStreamToolCalls,
|
|
2800
|
+
executedToolResults: normalizedStreamExecutedResults,
|
|
2801
|
+
suspendedTools: streamedSuspendedTools,
|
|
2802
|
+
thoughtSteps: thoughtBuffer.length > 0 ? [thoughtBuffer.join("")] : [],
|
|
2803
|
+
model: modelId,
|
|
2804
|
+
autoFixAttempted: false,
|
|
2805
|
+
autoFixFailureCount: 0,
|
|
2806
|
+
maxStepsReached: streamBudgetReached && !backendSynthesisPerformed,
|
|
2807
|
+
stepsUsed: streamStepsUsed,
|
|
2808
|
+
maxSteps: maxSteps,
|
|
2809
|
+
toolCallsUsed: streamToolCallsUsed,
|
|
2810
|
+
maxToolCalls,
|
|
2811
|
+
usage: streamTokenUsage,
|
|
2812
|
+
...(buildTokenUsageDebug({
|
|
2813
|
+
mode: "stream",
|
|
2814
|
+
source: streamUsageSource,
|
|
2815
|
+
})
|
|
2816
|
+
? {
|
|
2817
|
+
tokenUsageDebug: buildTokenUsageDebug({
|
|
2818
|
+
mode: "stream",
|
|
2819
|
+
source: streamUsageSource,
|
|
2820
|
+
}),
|
|
2821
|
+
}
|
|
2822
|
+
: {}),
|
|
2823
|
+
});
|
|
2824
|
+
stopKeepAlive();
|
|
2825
|
+
res.end();
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
catch (streamError) {
|
|
2829
|
+
const { code, message: providerMessage } = extractProviderErrorDetails(streamError);
|
|
2830
|
+
await persistLifecycle("failed", "error", "stream_error", {
|
|
2831
|
+
message: providerMessage,
|
|
2832
|
+
code,
|
|
2833
|
+
});
|
|
2834
|
+
writeEvent("lifecycle", {
|
|
2835
|
+
runId: resolvedRunId,
|
|
2836
|
+
lifecycleState,
|
|
2837
|
+
stopReason,
|
|
2838
|
+
});
|
|
2839
|
+
writeEvent("error", {
|
|
2840
|
+
success: false,
|
|
2841
|
+
runId: resolvedRunId,
|
|
2842
|
+
lifecycleState,
|
|
2843
|
+
stopReason,
|
|
2844
|
+
errorCode: code,
|
|
2845
|
+
error: normalizeProviderRequestError(requestedModelId, providerMessage, code),
|
|
2846
|
+
});
|
|
2847
|
+
stopKeepAlive();
|
|
2848
|
+
res.end();
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
finally {
|
|
2852
|
+
agent.clearProcessedWorkspaceResults?.(workspaceMutationGenerationId);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
await persistLifecycle("running", "none", "model_request_started", {
|
|
2856
|
+
stream: false,
|
|
2857
|
+
maxSteps,
|
|
2858
|
+
maxOutputTokens,
|
|
2859
|
+
});
|
|
2860
|
+
if (await isCancelled()) {
|
|
2861
|
+
await transitionToCancelled("before_model_request");
|
|
2862
|
+
return res.status(409).json({
|
|
2863
|
+
success: false,
|
|
2864
|
+
runId: resolvedRunId,
|
|
2865
|
+
lifecycleState,
|
|
2866
|
+
stopReason,
|
|
2867
|
+
error: "Run was cancelled",
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
let result;
|
|
2871
|
+
try {
|
|
2872
|
+
const resultOrCancellation = await waitForResultOrCancellation(activeHostSession
|
|
2873
|
+
? generateWithSessionRetry(activeHostSession, modelInput, generateOptions, turnAbortController.signal, modelProfile.generateRetryAttempts)
|
|
2874
|
+
: generateWithRetry(agent, modelInput, generateOptions, turnAbortController.signal, modelProfile.generateRetryAttempts));
|
|
2875
|
+
if (isCancelledWaitResult(resultOrCancellation)) {
|
|
2876
|
+
await transitionToCancelled("during_model_request");
|
|
2877
|
+
return res.status(409).json({
|
|
2878
|
+
success: false,
|
|
2879
|
+
runId: resolvedRunId,
|
|
2880
|
+
lifecycleState,
|
|
2881
|
+
stopReason,
|
|
2882
|
+
error: "Run was cancelled",
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
result = resultOrCancellation;
|
|
2886
|
+
}
|
|
2887
|
+
finally {
|
|
2888
|
+
agent.clearProcessedWorkspaceResults?.(workspaceMutationGenerationId);
|
|
2889
|
+
}
|
|
2890
|
+
if (await isCancelled()) {
|
|
2891
|
+
await transitionToCancelled("after_model_response");
|
|
2892
|
+
return res.status(409).json({
|
|
2893
|
+
success: false,
|
|
2894
|
+
runId: resolvedRunId,
|
|
2895
|
+
lifecycleState,
|
|
2896
|
+
stopReason,
|
|
2897
|
+
error: "Run was cancelled",
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
await persistLifecycle("running", "none", "model_response_received", {
|
|
2901
|
+
stepsCount: result.steps?.length || 0,
|
|
2902
|
+
toolCallsCount: result.toolCalls?.length || 0,
|
|
2903
|
+
});
|
|
2904
|
+
let usageSummary = normalizeTokenUsage(result.usage);
|
|
2905
|
+
let usageSeenFromFinal = Boolean(usageSummary);
|
|
2906
|
+
console.log("=== AGENT RESULT ===");
|
|
2907
|
+
console.log("Text:", result.text);
|
|
2908
|
+
console.log("Steps:", result.steps?.length || 0);
|
|
2909
|
+
console.log("Tool calls:", result.toolCalls?.length || 0);
|
|
2910
|
+
// Truncate response if it's too long to prevent context overflow
|
|
2911
|
+
let responseText = result.text || "";
|
|
2912
|
+
if (responseText.length > 12000) {
|
|
2913
|
+
console.log(`⚠️ Truncating long response: ${responseText.length} chars -> 12000 chars`);
|
|
2914
|
+
responseText = truncateEnvironmentResponse(responseText);
|
|
2915
|
+
}
|
|
2916
|
+
const toolCalls = [];
|
|
2917
|
+
const executedToolResults = []; // Track all tools that were executed
|
|
2918
|
+
const suspendedTools = [];
|
|
2919
|
+
const toolCallArgs = new Map(); // Map to store tool call args by toolCallId
|
|
2920
|
+
const anonymousToolCallArgs = [];
|
|
2921
|
+
const thoughtSteps = []; // Intermediate agent text steps for UI display
|
|
2922
|
+
// Check steps for tool execution details
|
|
2923
|
+
if (result.steps && result.steps.length > 0) {
|
|
2924
|
+
// Log all steps for debugging
|
|
2925
|
+
result.steps.forEach((step, index) => {
|
|
2926
|
+
console.log(`Step ${index}:`, JSON.stringify(step, null, 2));
|
|
2927
|
+
// Collect ALL executed tools from all steps (not just last step)
|
|
2928
|
+
if (step.content && Array.isArray(step.content)) {
|
|
2929
|
+
// Capture intermediate text/thought content emitted in step stream
|
|
2930
|
+
step.content.forEach((item) => {
|
|
2931
|
+
const maybeText = item.text;
|
|
2932
|
+
if (item.type === "text" && typeof maybeText === "string") {
|
|
2933
|
+
const text = maybeText.trim();
|
|
2934
|
+
if (text.length > 0) {
|
|
2935
|
+
thoughtSteps.push(text);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
// First pass: collect tool-call args
|
|
2940
|
+
step.content.forEach((item) => {
|
|
2941
|
+
if (item.type === "tool-call" && item.toolName) {
|
|
2942
|
+
const args = item.args || item.input || {};
|
|
2943
|
+
if (item.toolCallId) {
|
|
2944
|
+
toolCallArgs.set(item.toolCallId, args);
|
|
2945
|
+
}
|
|
2946
|
+
else {
|
|
2947
|
+
anonymousToolCallArgs.push({
|
|
2948
|
+
toolName: item.toolName,
|
|
2949
|
+
args,
|
|
2950
|
+
consumed: false,
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
});
|
|
2955
|
+
// Second pass: collect tool results with their args
|
|
2956
|
+
step.content.forEach((item) => {
|
|
2957
|
+
// Capture tool results (tools that were already executed)
|
|
2958
|
+
if (item.type === "tool-result" && item.toolName) {
|
|
2959
|
+
// Get args from the corresponding tool-call
|
|
2960
|
+
const args = resolveToolResultArgs(item.toolName, item.toolCallId, item.args || item.input, toolCallArgs, anonymousToolCallArgs);
|
|
2961
|
+
// Result can be in different properties depending on AI SDK version
|
|
2962
|
+
// Use 'in' operator to check property existence, not truthiness
|
|
2963
|
+
// Priority: result > output > content > data
|
|
2964
|
+
let toolResult;
|
|
2965
|
+
if ("result" in item) {
|
|
2966
|
+
toolResult = item.result;
|
|
2967
|
+
}
|
|
2968
|
+
else if ("output" in item) {
|
|
2969
|
+
toolResult = item.output;
|
|
2970
|
+
}
|
|
2971
|
+
else if ("content" in item && item.type === "tool-result") {
|
|
2972
|
+
// Only use content if it's not the type discriminator
|
|
2973
|
+
toolResult = item.content;
|
|
2974
|
+
}
|
|
2975
|
+
else if ("data" in item) {
|
|
2976
|
+
toolResult = item.data;
|
|
2977
|
+
}
|
|
2978
|
+
else {
|
|
2979
|
+
toolResult = undefined;
|
|
2980
|
+
}
|
|
2981
|
+
console.log(` Tool result name: ${item.toolName}`);
|
|
2982
|
+
console.log(" Tool result raw item:", JSON.stringify(item, null, 2));
|
|
2983
|
+
console.log(" Tool result value:", toolResult);
|
|
2984
|
+
console.log(" Tool args:", args);
|
|
2985
|
+
const safeToolResult = redactToolResult(toolResult).result;
|
|
2986
|
+
executedToolResults.push({
|
|
2987
|
+
name: item.toolName,
|
|
2988
|
+
args: args,
|
|
2989
|
+
result: safeToolResult,
|
|
2990
|
+
toolCallId: item.toolCallId,
|
|
2991
|
+
lifecycleStepIndex: index,
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
if ((item.type === "tool-suspended" ||
|
|
2995
|
+
item.type === "tool_suspended") &&
|
|
2996
|
+
item.toolName) {
|
|
2997
|
+
let suspendPayload;
|
|
2998
|
+
if ("suspendPayload" in item &&
|
|
2999
|
+
item.suspendPayload &&
|
|
3000
|
+
typeof item.suspendPayload === "object") {
|
|
3001
|
+
suspendPayload = item.suspendPayload;
|
|
3002
|
+
}
|
|
3003
|
+
else if ("result" in item &&
|
|
3004
|
+
item.result &&
|
|
3005
|
+
typeof item.result === "object") {
|
|
3006
|
+
suspendPayload = item.result;
|
|
3007
|
+
}
|
|
3008
|
+
else if ("output" in item &&
|
|
3009
|
+
item.output &&
|
|
3010
|
+
typeof item.output === "object") {
|
|
3011
|
+
suspendPayload = item.output;
|
|
3012
|
+
}
|
|
3013
|
+
else if ("data" in item &&
|
|
3014
|
+
item.data &&
|
|
3015
|
+
typeof item.data === "object") {
|
|
3016
|
+
suspendPayload = item.data;
|
|
3017
|
+
}
|
|
3018
|
+
suspendedTools.push({
|
|
3019
|
+
name: item.toolName,
|
|
3020
|
+
toolCallId: item.toolCallId,
|
|
3021
|
+
suspendPayload,
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
3027
|
+
// Only collect PENDING tool calls from the LAST step
|
|
3028
|
+
// If the agent executed tools in previous steps, we don't want to send them back to the client
|
|
3029
|
+
const lastStep = result.steps[result.steps.length - 1];
|
|
3030
|
+
// Tool calls are in the content array
|
|
3031
|
+
const lastStepCallCounts = new Map();
|
|
3032
|
+
const lastStepCallKey = (name, args, toolCallId) => toolCallId
|
|
3033
|
+
? `id:${toolCallId}:${getToolCallSignature(name, args)}`
|
|
3034
|
+
: getToolCallSignature(name, args);
|
|
3035
|
+
if (lastStep.content && Array.isArray(lastStep.content)) {
|
|
3036
|
+
lastStep.content.forEach((item) => {
|
|
3037
|
+
if (item.type === "tool-call" && item.toolName) {
|
|
3038
|
+
console.log(` Tool call: ${item.toolName}`, item.input || item.args);
|
|
3039
|
+
const args = sanitizeToolArgsForWorkspace(item.toolName, item.input || item.args, isWebWorkspace);
|
|
3040
|
+
const key = lastStepCallKey(item.toolName, args, item.toolCallId);
|
|
3041
|
+
lastStepCallCounts.set(key, (lastStepCallCounts.get(key) || 0) + 1);
|
|
3042
|
+
toolCalls.push({
|
|
3043
|
+
name: item.toolName,
|
|
3044
|
+
args: args,
|
|
3045
|
+
toolCallId: item.toolCallId,
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
// Fallback: Check toolCalls property. Track multiplicity per key so
|
|
3051
|
+
// repeated anonymous invocations are preserved while entries already
|
|
3052
|
+
// collected from `content` above are not double-counted.
|
|
3053
|
+
if (lastStep.toolCalls && lastStep.toolCalls.length > 0) {
|
|
3054
|
+
const fallbackCallCounts = new Map();
|
|
3055
|
+
lastStep.toolCalls.forEach((call) => {
|
|
3056
|
+
// Only add if it has a valid toolName
|
|
3057
|
+
if (call && call.toolName) {
|
|
3058
|
+
const args = sanitizeToolArgsForWorkspace(call.toolName, call.args, isWebWorkspace);
|
|
3059
|
+
const key = lastStepCallKey(call.toolName, args, call.toolCallId);
|
|
3060
|
+
const occurrence = (fallbackCallCounts.get(key) || 0) + 1;
|
|
3061
|
+
fallbackCallCounts.set(key, occurrence);
|
|
3062
|
+
if (occurrence <= (lastStepCallCounts.get(key) || 0)) {
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
console.log(" Tool call (fallback):", call.toolName, call.args);
|
|
3066
|
+
toolCalls.push({
|
|
3067
|
+
name: call.toolName,
|
|
3068
|
+
args: args,
|
|
3069
|
+
toolCallId: call.toolCallId,
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
// Fallback to top-level toolCalls if no steps
|
|
3076
|
+
if (toolCalls.length === 0 &&
|
|
3077
|
+
result.toolCalls &&
|
|
3078
|
+
result.toolCalls.length > 0) {
|
|
3079
|
+
result.toolCalls.forEach((call) => {
|
|
3080
|
+
// Only add if it has a valid toolName
|
|
3081
|
+
if (call && call.toolName) {
|
|
3082
|
+
console.log("Direct tool call:", call.toolName, call.args);
|
|
3083
|
+
const args = sanitizeToolArgsForWorkspace(call.toolName, call.args, isWebWorkspace);
|
|
3084
|
+
toolCalls.push({
|
|
3085
|
+
name: call.toolName,
|
|
3086
|
+
args: args,
|
|
3087
|
+
toolCallId: call.toolCallId,
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
const normalizedLifecycle = normalizeToolLifecycle(toolCalls, executedToolResults);
|
|
3093
|
+
let lifecycleToolCallsForCounting = toolCalls;
|
|
3094
|
+
let lifecycleResultsForCounting = executedToolResults;
|
|
3095
|
+
let normalizedToolCalls = normalizedLifecycle.pendingToolCalls.map((call) => ({
|
|
3096
|
+
name: call.name,
|
|
3097
|
+
args: call.args,
|
|
3098
|
+
toolCallId: call.toolCallId,
|
|
3099
|
+
status: "pending",
|
|
3100
|
+
}));
|
|
3101
|
+
const normalizedExecutedToolResults = normalizedLifecycle.executedToolResults.map((toolResult) => ({
|
|
3102
|
+
name: toolResult.name,
|
|
3103
|
+
args: toolResult.args,
|
|
3104
|
+
result: toolResult.result,
|
|
3105
|
+
toolCallId: toolResult.toolCallId,
|
|
3106
|
+
status: resolveToolExecutionStatus(toolResult.result),
|
|
3107
|
+
}));
|
|
3108
|
+
const replayToolActions = buildOrderedPersistedToolActions(result.steps, normalizedToolCalls, normalizedExecutedToolResults, (toolName, args) => sanitizeToolArgsForWorkspace(toolName, args, isWebWorkspace));
|
|
3109
|
+
const persistedReplayActions = new Set();
|
|
3110
|
+
const persistReplayToolActions = async (actions) => {
|
|
3111
|
+
for (const action of actions) {
|
|
3112
|
+
if (persistedReplayActions.has(action))
|
|
3113
|
+
continue;
|
|
3114
|
+
persistedReplayActions.add(action);
|
|
3115
|
+
await persistToolEvent(action.eventType, {
|
|
3116
|
+
name: action.name,
|
|
3117
|
+
toolName: action.name,
|
|
3118
|
+
args: action.args,
|
|
3119
|
+
...(action.eventType === "tool_result"
|
|
3120
|
+
? { result: action.result }
|
|
3121
|
+
: {}),
|
|
3122
|
+
toolCallId: action.toolCallId,
|
|
3123
|
+
status: action.status,
|
|
3124
|
+
});
|
|
3125
|
+
}
|
|
3126
|
+
};
|
|
3127
|
+
await persistReplayToolActions(replayToolActions);
|
|
3128
|
+
const uniqueThoughtSteps = Array.from(new Set(thoughtSteps
|
|
3129
|
+
.map((t) => t.trim())
|
|
3130
|
+
.filter((t) => t.length > 0 && t !== responseText.trim())));
|
|
3131
|
+
console.log("Final tool calls count:", normalizedToolCalls.length);
|
|
3132
|
+
console.log("Executed tool results count:", normalizedExecutedToolResults.length);
|
|
3133
|
+
console.log("Thought steps count:", uniqueThoughtSteps.length);
|
|
3134
|
+
// Check if any tool results are pending user confirmation
|
|
3135
|
+
let pendingConfirmations = normalizedExecutedToolResults.filter((toolResult) => resolveToolExecutionStatus(toolResult.result) ===
|
|
3136
|
+
"pending_confirmation");
|
|
3137
|
+
let stepsCount = result.steps?.length || 0;
|
|
3138
|
+
let effectiveMaxSteps = maxSteps;
|
|
3139
|
+
let autoFixAttempted = false;
|
|
3140
|
+
let autoFixFailureCount = 0;
|
|
3141
|
+
let reflectionAttemptCount = 0;
|
|
3142
|
+
let reflectionStopReason = "none";
|
|
3143
|
+
let backendSynthesisPerformed = false;
|
|
3144
|
+
const countResultToolCalls = (generationResult) => generationResult.steps?.reduce((total, step) => total + (Array.isArray(step.toolCalls) ? step.toolCalls.length : 0), 0) ??
|
|
3145
|
+
generationResult.toolCalls?.length ??
|
|
3146
|
+
0;
|
|
3147
|
+
let cumulativeToolCallsUsed = Math.max(toolCallBudget.admitted, countUniqueToolCalls(lifecycleToolCallsForCounting, lifecycleResultsForCounting), countResultToolCalls(result));
|
|
3148
|
+
// Bounded reflection loop: target failed validation/tool execution with explicit retry context.
|
|
3149
|
+
if (isAutoFixValidationEnabled() &&
|
|
3150
|
+
pendingConfirmations.length === 0 &&
|
|
3151
|
+
normalizedToolCalls.length === 0) {
|
|
3152
|
+
let reflectionAttempt = 0;
|
|
3153
|
+
const reflectionCap = modelProfile.reflectionMaxAttempts;
|
|
3154
|
+
const noProgressRepeatThreshold = getReflectionNoProgressRepeatThreshold();
|
|
3155
|
+
let previousFailureSignature = null;
|
|
3156
|
+
let repeatedFailureSignatures = 0;
|
|
3157
|
+
while (reflectionAttempt < reflectionCap) {
|
|
3158
|
+
if (await isCancelled()) {
|
|
3159
|
+
await transitionToCancelled("reflection_loop");
|
|
3160
|
+
return res.status(409).json({
|
|
3161
|
+
success: false,
|
|
3162
|
+
runId: resolvedRunId,
|
|
3163
|
+
lifecycleState,
|
|
3164
|
+
stopReason,
|
|
3165
|
+
error: "Run was cancelled",
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
const validationFailures = extractValidationFailures(normalizedExecutedToolResults);
|
|
3169
|
+
const toolFailures = extractToolExecutionFailures(normalizedExecutedToolResults);
|
|
3170
|
+
const detectedFailureCount = validationFailures.length + toolFailures.length;
|
|
3171
|
+
autoFixFailureCount = Math.max(autoFixFailureCount, detectedFailureCount);
|
|
3172
|
+
if (detectedFailureCount === 0) {
|
|
3173
|
+
reflectionStopReason = "resolved";
|
|
3174
|
+
break;
|
|
3175
|
+
}
|
|
3176
|
+
const remainingToolCalls = maxToolCalls - cumulativeToolCallsUsed;
|
|
3177
|
+
if (remainingToolCalls <= 0) {
|
|
3178
|
+
reflectionStopReason = "max_attempts";
|
|
3179
|
+
break;
|
|
3180
|
+
}
|
|
3181
|
+
const failureSignature = buildFailureSignature(validationFailures, toolFailures);
|
|
3182
|
+
if (previousFailureSignature === failureSignature) {
|
|
3183
|
+
repeatedFailureSignatures += 1;
|
|
3184
|
+
}
|
|
3185
|
+
else {
|
|
3186
|
+
repeatedFailureSignatures = 0;
|
|
3187
|
+
}
|
|
3188
|
+
previousFailureSignature = failureSignature;
|
|
3189
|
+
if (repeatedFailureSignatures >= noProgressRepeatThreshold) {
|
|
3190
|
+
reflectionStopReason = "no_progress";
|
|
3191
|
+
console.warn(`[agent] reflection halted due to repeated failure signature (${repeatedFailureSignatures}/${noProgressRepeatThreshold})`);
|
|
3192
|
+
break;
|
|
3193
|
+
}
|
|
3194
|
+
reflectionAttempt += 1;
|
|
3195
|
+
reflectionAttemptCount = reflectionAttempt;
|
|
3196
|
+
autoFixAttempted = true;
|
|
3197
|
+
const validationFailureReport = validationFailures
|
|
3198
|
+
.map((failure, index) => {
|
|
3199
|
+
const chunks = [
|
|
3200
|
+
`${index + 1}. ${failure.phase.toUpperCase()} failed`,
|
|
3201
|
+
failure.command ? `Command: ${failure.command}` : undefined,
|
|
3202
|
+
failure.error ? `Error: ${failure.error}` : undefined,
|
|
3203
|
+
failure.stderr ? `stderr:\n${failure.stderr}` : undefined,
|
|
3204
|
+
failure.stdout ? `stdout:\n${failure.stdout}` : undefined,
|
|
3205
|
+
].filter(Boolean);
|
|
3206
|
+
return chunks.join("\n");
|
|
3207
|
+
})
|
|
3208
|
+
.join("\n\n");
|
|
3209
|
+
const toolFailureReport = toolFailures
|
|
3210
|
+
.map((failure, index) => {
|
|
3211
|
+
const chunks = [
|
|
3212
|
+
`${index + 1}. Tool \`${failure.toolName}\` failed`,
|
|
3213
|
+
`Status: ${failure.status}`,
|
|
3214
|
+
failure.error ? `Error: ${failure.error}` : undefined,
|
|
3215
|
+
failure.args
|
|
3216
|
+
? `Args: ${JSON.stringify(failure.args)}`
|
|
3217
|
+
: undefined,
|
|
3218
|
+
].filter(Boolean);
|
|
3219
|
+
return chunks.join("\n");
|
|
3220
|
+
})
|
|
3221
|
+
.join("\n\n");
|
|
3222
|
+
const autoFixPrompt = `${prompt}\n\nA prior attempt produced failures. Perform a targeted repair pass for attempt ${reflectionAttempt}/${reflectionCap}.\n\n${validationFailureReport
|
|
3223
|
+
? `Validation failures:\n${validationFailureReport}\n\n`
|
|
3224
|
+
: ""}${toolFailureReport
|
|
3225
|
+
? `Tool execution failures:\n${toolFailureReport}\n\n`
|
|
3226
|
+
: ""}Requirements:\n- Focus only on the listed failures.\n- If patch/tool matching failed, retry with tighter file targeting and explicit paths.\n- Keep edits minimal and reversible.\n- Stop after this repair pass.`;
|
|
3227
|
+
const autoFixOptions = {
|
|
3228
|
+
maxSteps: Math.min(modelProfile.reflectionMaxSteps, remainingToolCalls),
|
|
3229
|
+
maxOutputTokens,
|
|
3230
|
+
...(mastraMemoryScope ? { memory: mastraMemoryScope } : {}),
|
|
3231
|
+
toolChoice: "auto",
|
|
3232
|
+
toolCallConcurrency: 1,
|
|
3233
|
+
stopWhen: stopWhenToolBudgetReached,
|
|
3234
|
+
requestContext: agentRequestContext,
|
|
3235
|
+
abortSignal: turnAbortController.signal,
|
|
3236
|
+
};
|
|
3237
|
+
let autoFixResultOrCancellation;
|
|
3238
|
+
const admittedBeforeReflection = toolCallBudget.admitted;
|
|
3239
|
+
try {
|
|
3240
|
+
autoFixResultOrCancellation = await waitForResultOrCancellation(activeHostSession
|
|
3241
|
+
? generateWithSessionRetry(activeHostSession, autoFixPrompt, autoFixOptions, turnAbortController.signal, modelProfile.reflectionRetryAttempts)
|
|
3242
|
+
: generateWithRetry(agent, autoFixPrompt, autoFixOptions, turnAbortController.signal, modelProfile.reflectionRetryAttempts));
|
|
3243
|
+
}
|
|
3244
|
+
finally {
|
|
3245
|
+
agent.clearProcessedWorkspaceResults?.(workspaceMutationGenerationId);
|
|
3246
|
+
}
|
|
3247
|
+
if (isCancelledWaitResult(autoFixResultOrCancellation)) {
|
|
3248
|
+
await transitionToCancelled("during_reflection_model_request");
|
|
3249
|
+
return res.status(409).json({
|
|
3250
|
+
success: false,
|
|
3251
|
+
runId: resolvedRunId,
|
|
3252
|
+
lifecycleState,
|
|
3253
|
+
stopReason,
|
|
3254
|
+
error: "Run was cancelled",
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
const autoFixResult = autoFixResultOrCancellation;
|
|
3258
|
+
if (await isCancelled()) {
|
|
3259
|
+
await transitionToCancelled("after_reflection_response");
|
|
3260
|
+
return res.status(409).json({
|
|
3261
|
+
success: false,
|
|
3262
|
+
runId: resolvedRunId,
|
|
3263
|
+
lifecycleState,
|
|
3264
|
+
stopReason,
|
|
3265
|
+
error: "Run was cancelled",
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
usageSummary = mergeTokenUsage(usageSummary, normalizeTokenUsage(autoFixResult.usage));
|
|
3269
|
+
if (!usageSeenFromFinal && usageSummary) {
|
|
3270
|
+
usageSeenFromFinal = true;
|
|
3271
|
+
}
|
|
3272
|
+
let autoFixResponseText = autoFixResult.text || "";
|
|
3273
|
+
if (autoFixResponseText.length > 12000) {
|
|
3274
|
+
autoFixResponseText =
|
|
3275
|
+
truncateEnvironmentResponse(autoFixResponseText);
|
|
3276
|
+
}
|
|
3277
|
+
responseText = autoFixResponseText;
|
|
3278
|
+
const autoFixToolCalls = [];
|
|
3279
|
+
const autoFixExecutedToolResults = [];
|
|
3280
|
+
const autoFixToolCallArgs = new Map();
|
|
3281
|
+
const autoFixAnonymousToolCallArgs = [];
|
|
3282
|
+
const autoFixThoughtSteps = [];
|
|
3283
|
+
if (autoFixResult.steps && autoFixResult.steps.length > 0) {
|
|
3284
|
+
const lastStep = autoFixResult.steps[autoFixResult.steps.length - 1];
|
|
3285
|
+
autoFixResult.steps.forEach((step, index) => {
|
|
3286
|
+
if (!step.content || !Array.isArray(step.content))
|
|
3287
|
+
return;
|
|
3288
|
+
step.content.forEach((item) => {
|
|
3289
|
+
const maybeText = item.text;
|
|
3290
|
+
if (item.type === "text" && typeof maybeText === "string") {
|
|
3291
|
+
const text = maybeText.trim();
|
|
3292
|
+
if (text.length > 0) {
|
|
3293
|
+
autoFixThoughtSteps.push(text);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
});
|
|
3297
|
+
step.content.forEach((item) => {
|
|
3298
|
+
if (item.type === "tool-call" && item.toolName) {
|
|
3299
|
+
const args = sanitizeToolArgsForWorkspace(item.toolName, item.input || item.args, isWebWorkspace);
|
|
3300
|
+
if (item.toolCallId) {
|
|
3301
|
+
autoFixToolCallArgs.set(item.toolCallId, args);
|
|
3302
|
+
}
|
|
3303
|
+
else {
|
|
3304
|
+
autoFixAnonymousToolCallArgs.push({
|
|
3305
|
+
toolName: item.toolName,
|
|
3306
|
+
args,
|
|
3307
|
+
consumed: false,
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
});
|
|
3312
|
+
step.content.forEach((item) => {
|
|
3313
|
+
if (item.type === "tool-result" && item.toolName) {
|
|
3314
|
+
const args = resolveToolResultArgs(item.toolName, item.toolCallId, item.args || item.input, autoFixToolCallArgs, autoFixAnonymousToolCallArgs);
|
|
3315
|
+
let toolResult;
|
|
3316
|
+
if ("result" in item) {
|
|
3317
|
+
toolResult = item.result;
|
|
3318
|
+
}
|
|
3319
|
+
else if ("output" in item) {
|
|
3320
|
+
toolResult = item.output;
|
|
3321
|
+
}
|
|
3322
|
+
else if ("content" in item && item.type === "tool-result") {
|
|
3323
|
+
toolResult = item.content;
|
|
3324
|
+
}
|
|
3325
|
+
else if ("data" in item) {
|
|
3326
|
+
toolResult = item.data;
|
|
3327
|
+
}
|
|
3328
|
+
const safeToolResult = redactToolResult(toolResult).result;
|
|
3329
|
+
autoFixExecutedToolResults.push({
|
|
3330
|
+
name: item.toolName,
|
|
3331
|
+
args,
|
|
3332
|
+
result: safeToolResult,
|
|
3333
|
+
toolCallId: item.toolCallId,
|
|
3334
|
+
lifecycleStepIndex: index,
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
});
|
|
3338
|
+
});
|
|
3339
|
+
if (lastStep.content && Array.isArray(lastStep.content)) {
|
|
3340
|
+
lastStep.content.forEach((item) => {
|
|
3341
|
+
if (item.type === "tool-call" && item.toolName) {
|
|
3342
|
+
const args = sanitizeToolArgsForWorkspace(item.toolName, item.input || item.args, isWebWorkspace);
|
|
3343
|
+
autoFixToolCalls.push({
|
|
3344
|
+
name: item.toolName,
|
|
3345
|
+
args,
|
|
3346
|
+
toolCallId: item.toolCallId,
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
if (autoFixToolCalls.length === 0 &&
|
|
3353
|
+
autoFixResult.toolCalls &&
|
|
3354
|
+
autoFixResult.toolCalls.length > 0) {
|
|
3355
|
+
autoFixResult.toolCalls.forEach((call) => {
|
|
3356
|
+
if (!call || !call.toolName)
|
|
3357
|
+
return;
|
|
3358
|
+
const args = sanitizeToolArgsForWorkspace(call.toolName, call.args, isWebWorkspace);
|
|
3359
|
+
autoFixToolCalls.push({
|
|
3360
|
+
name: call.toolName,
|
|
3361
|
+
args,
|
|
3362
|
+
toolCallId: call.toolCallId,
|
|
3363
|
+
});
|
|
3364
|
+
});
|
|
3365
|
+
}
|
|
3366
|
+
const autoFixNormalizedLifecycle = normalizeToolLifecycle(autoFixToolCalls, autoFixExecutedToolResults);
|
|
3367
|
+
const autoFixNormalizedToolCalls = autoFixNormalizedLifecycle.pendingToolCalls.map((call) => ({
|
|
3368
|
+
name: call.name,
|
|
3369
|
+
args: redactToolResult(call.args).result,
|
|
3370
|
+
toolCallId: call.toolCallId,
|
|
3371
|
+
status: "pending",
|
|
3372
|
+
}));
|
|
3373
|
+
const autoFixNormalizedExecutedResults = autoFixNormalizedLifecycle.executedToolResults.map((toolResult) => ({
|
|
3374
|
+
name: toolResult.name,
|
|
3375
|
+
args: redactToolResult(toolResult.args).result,
|
|
3376
|
+
result: toolResult.result,
|
|
3377
|
+
toolCallId: toolResult.toolCallId,
|
|
3378
|
+
status: resolveToolExecutionStatus(toolResult.result),
|
|
3379
|
+
}));
|
|
3380
|
+
const reflectionToolCallsUsed = countUniqueToolCalls(autoFixToolCalls, autoFixExecutedToolResults);
|
|
3381
|
+
const admissionDelta = toolCallBudget.admitted - admittedBeforeReflection;
|
|
3382
|
+
cumulativeToolCallsUsed +=
|
|
3383
|
+
admissionDelta > 0 ? admissionDelta : reflectionToolCallsUsed;
|
|
3384
|
+
const uniqueAutoFixThoughtSteps = Array.from(new Set(autoFixThoughtSteps
|
|
3385
|
+
.map((t) => t.trim())
|
|
3386
|
+
.filter((t) => t.length > 0 && t !== responseText.trim())));
|
|
3387
|
+
const autoFixReplayActions = buildOrderedPersistedToolActions(autoFixResult.steps, autoFixNormalizedToolCalls, autoFixNormalizedExecutedResults, (toolName, args) => sanitizeToolArgsForWorkspace(toolName, args, isWebWorkspace));
|
|
3388
|
+
replayToolActions.push(...autoFixReplayActions);
|
|
3389
|
+
await persistReplayToolActions(autoFixReplayActions);
|
|
3390
|
+
normalizedToolCalls.length = 0;
|
|
3391
|
+
normalizedToolCalls.push(...autoFixNormalizedToolCalls);
|
|
3392
|
+
normalizedExecutedToolResults.length = 0;
|
|
3393
|
+
normalizedExecutedToolResults.push(...autoFixNormalizedExecutedResults);
|
|
3394
|
+
lifecycleToolCallsForCounting = autoFixToolCalls;
|
|
3395
|
+
lifecycleResultsForCounting = autoFixExecutedToolResults;
|
|
3396
|
+
uniqueThoughtSteps.length = 0;
|
|
3397
|
+
uniqueThoughtSteps.push(...uniqueAutoFixThoughtSteps);
|
|
3398
|
+
pendingConfirmations = normalizedExecutedToolResults.filter((toolResult) => resolveToolExecutionStatus(toolResult.result) ===
|
|
3399
|
+
"pending_confirmation");
|
|
3400
|
+
stepsCount = autoFixResult.steps?.length || 0;
|
|
3401
|
+
effectiveMaxSteps = autoFixOptions.maxSteps;
|
|
3402
|
+
if (pendingConfirmations.length > 0 ||
|
|
3403
|
+
normalizedToolCalls.length > 0) {
|
|
3404
|
+
break;
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
if (autoFixAttempted && reflectionStopReason === "none") {
|
|
3408
|
+
const remainingValidationFailures = extractValidationFailures(normalizedExecutedToolResults);
|
|
3409
|
+
const remainingToolFailures = extractToolExecutionFailures(normalizedExecutedToolResults);
|
|
3410
|
+
reflectionStopReason =
|
|
3411
|
+
remainingValidationFailures.length +
|
|
3412
|
+
remainingToolFailures.length ===
|
|
3413
|
+
0
|
|
3414
|
+
? "resolved"
|
|
3415
|
+
: "max_attempts";
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
const gitSafety = {
|
|
3419
|
+
enabled: gitSafetyMode !== "off",
|
|
3420
|
+
gitDetected,
|
|
3421
|
+
policyMode: gitSafetyMode,
|
|
3422
|
+
};
|
|
3423
|
+
await persistReplayToolActions(replayToolActions);
|
|
3424
|
+
logTokenUsageSource({
|
|
3425
|
+
mode: "non_stream",
|
|
3426
|
+
source: usageSeenFromFinal ? "final" : "none",
|
|
3427
|
+
usage: usageSummary,
|
|
3428
|
+
modelId,
|
|
3429
|
+
});
|
|
3430
|
+
const nonStreamUsageSource = usageSeenFromFinal
|
|
3431
|
+
? "final"
|
|
3432
|
+
: "none";
|
|
3433
|
+
const nonStreamTokenUsageDebug = buildTokenUsageDebug({
|
|
3434
|
+
mode: "non_stream",
|
|
3435
|
+
source: nonStreamUsageSource,
|
|
3436
|
+
});
|
|
3437
|
+
if (gitDetected &&
|
|
3438
|
+
gitSafetyMode !== "off" &&
|
|
3439
|
+
hasSuccessfulEditExecution(normalizedExecutedToolResults)) {
|
|
3440
|
+
gitSafety.checkpointHint =
|
|
3441
|
+
'Create a checkpoint commit: git add -A && git commit -m "checkpoint: agent changes"';
|
|
3442
|
+
gitSafety.undoHint =
|
|
3443
|
+
"Undo unstaged edits safely: git restore -- <file>; inspect with git diff first.";
|
|
3444
|
+
}
|
|
3445
|
+
if (pendingConfirmations.length > 0) {
|
|
3446
|
+
await persistLifecycle("waiting_confirmation", "awaiting_approval", "awaiting_confirmation", {
|
|
3447
|
+
pendingConfirmations: pendingConfirmations.length,
|
|
3448
|
+
});
|
|
3449
|
+
// Return special response for command confirmation
|
|
3450
|
+
return res.json({
|
|
3451
|
+
success: true,
|
|
3452
|
+
runId: resolvedRunId,
|
|
3453
|
+
lifecycleState,
|
|
3454
|
+
stopReason,
|
|
3455
|
+
requiresConfirmation: true,
|
|
3456
|
+
autoFixAttempted,
|
|
3457
|
+
autoFixFailureCount,
|
|
3458
|
+
reflectionAttemptCount,
|
|
3459
|
+
reflectionStopReason,
|
|
3460
|
+
gitSafety,
|
|
3461
|
+
usage: usageSummary,
|
|
3462
|
+
...(nonStreamTokenUsageDebug
|
|
3463
|
+
? { tokenUsageDebug: nonStreamTokenUsageDebug }
|
|
3464
|
+
: {}),
|
|
3465
|
+
pendingConfirmations: pendingConfirmations.map((tool) => {
|
|
3466
|
+
const resultPayload = getToolResultPayload(tool.result) || {};
|
|
3467
|
+
return {
|
|
3468
|
+
confirmationId: typeof resultPayload.confirmationId === "string"
|
|
3469
|
+
? resultPayload.confirmationId
|
|
3470
|
+
: undefined,
|
|
3471
|
+
command: typeof resultPayload.command === "string"
|
|
3472
|
+
? resultPayload.command
|
|
3473
|
+
: undefined,
|
|
3474
|
+
action: typeof resultPayload.action === "string"
|
|
3475
|
+
? resultPayload.action
|
|
3476
|
+
: undefined,
|
|
3477
|
+
target: typeof resultPayload.target === "string"
|
|
3478
|
+
? resultPayload.target
|
|
3479
|
+
: undefined,
|
|
3480
|
+
toolName: tool.name,
|
|
3481
|
+
toolArgs: tool.args,
|
|
3482
|
+
};
|
|
3483
|
+
}),
|
|
3484
|
+
response: "The following commands require your approval before execution:",
|
|
3485
|
+
});
|
|
3486
|
+
}
|
|
3487
|
+
if (suspendedTools.length > 0) {
|
|
3488
|
+
const primarySuspension = suspendedTools[0];
|
|
3489
|
+
const payload = primarySuspension.suspendPayload || {};
|
|
3490
|
+
const question = typeof payload.question === "string" &&
|
|
3491
|
+
payload.question.trim().length > 0
|
|
3492
|
+
? payload.question
|
|
3493
|
+
: typeof payload.prompt === "string" &&
|
|
3494
|
+
payload.prompt.trim().length > 0
|
|
3495
|
+
? payload.prompt
|
|
3496
|
+
: primarySuspension.name === "submit_plan"
|
|
3497
|
+
? "A plan requires your review. Please approve, reject, or provide feedback."
|
|
3498
|
+
: "Additional input is required to continue. Please provide your response.";
|
|
3499
|
+
await persistLifecycle("waiting_user_input", "awaiting_user_input", "awaiting_user_input", {
|
|
3500
|
+
suspendedTools: suspendedTools.length,
|
|
3501
|
+
});
|
|
3502
|
+
return res.json({
|
|
3503
|
+
success: true,
|
|
3504
|
+
runId: resolvedRunId,
|
|
3505
|
+
lifecycleState,
|
|
3506
|
+
stopReason,
|
|
3507
|
+
response: question,
|
|
3508
|
+
waitingForUserInput: true,
|
|
3509
|
+
suspendedTools,
|
|
3510
|
+
toolCalls: [],
|
|
3511
|
+
executedToolResults: normalizedExecutedToolResults,
|
|
3512
|
+
thoughtSteps: uniqueThoughtSteps,
|
|
3513
|
+
model: modelId,
|
|
3514
|
+
autoFixAttempted,
|
|
3515
|
+
autoFixFailureCount,
|
|
3516
|
+
reflectionAttemptCount,
|
|
3517
|
+
reflectionStopReason,
|
|
3518
|
+
gitSafety,
|
|
3519
|
+
usage: usageSummary,
|
|
3520
|
+
...(nonStreamTokenUsageDebug
|
|
3521
|
+
? { tokenUsageDebug: nonStreamTokenUsageDebug }
|
|
3522
|
+
: {}),
|
|
3523
|
+
maxStepsReached: false,
|
|
3524
|
+
stepsUsed: stepsCount,
|
|
3525
|
+
maxSteps: effectiveMaxSteps,
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
const toolCallsUsed = Math.max(cumulativeToolCallsUsed, countUniqueToolCalls(lifecycleToolCallsForCounting, lifecycleResultsForCounting));
|
|
3529
|
+
const endedWithoutFinalText = typeof responseText !== "string" || responseText.trim().length === 0;
|
|
3530
|
+
const hasPendingToolCalls = normalizedToolCalls.length > 0;
|
|
3531
|
+
const hasTerminalExecutedToolResults = hasTerminalToolResults(normalizedExecutedToolResults);
|
|
3532
|
+
const hasNonterminalExecutedToolResults = hasNonterminalToolResults(normalizedExecutedToolResults);
|
|
3533
|
+
const budgetLimitReached = toolCallsUsed >= maxToolCalls ||
|
|
3534
|
+
toolCallBudget.stopReason === "repeated_call";
|
|
3535
|
+
const shouldSynthesizeAfterCompletedToolWork = pendingConfirmations.length === 0 &&
|
|
3536
|
+
((hasPendingToolCalls &&
|
|
3537
|
+
!hasNonterminalExecutedToolResults &&
|
|
3538
|
+
budgetLimitReached) ||
|
|
3539
|
+
(hasTerminalExecutedToolResults &&
|
|
3540
|
+
endedWithoutFinalText &&
|
|
3541
|
+
!hasPendingToolCalls));
|
|
3542
|
+
if (shouldSynthesizeAfterCompletedToolWork) {
|
|
3543
|
+
const synthesisStopReason = budgetLimitReached
|
|
3544
|
+
? (toolCallBudget.stopReason ?? "limit")
|
|
3545
|
+
: "empty_final_response";
|
|
3546
|
+
const synthesisResultOrCancellation = await generateBackendOnlySynthesis(normalizedExecutedToolResults, synthesisStopReason);
|
|
3547
|
+
if (isCancelledWaitResult(synthesisResultOrCancellation)) {
|
|
3548
|
+
await transitionToCancelled("during_backend_synthesis");
|
|
3549
|
+
return res.status(409).json({
|
|
3550
|
+
success: false,
|
|
3551
|
+
runId: resolvedRunId,
|
|
3552
|
+
lifecycleState,
|
|
3553
|
+
stopReason,
|
|
3554
|
+
error: "Run was cancelled",
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
const synthesisResult = synthesisResultOrCancellation;
|
|
3558
|
+
const synthesizedText = typeof synthesisResult.text === "string" &&
|
|
3559
|
+
synthesisResult.text.trim().length > 0
|
|
3560
|
+
? synthesisResult.text
|
|
3561
|
+
: null;
|
|
3562
|
+
if (!synthesizedText && endedWithoutFinalText) {
|
|
3563
|
+
throw new Error("The agent completed its tool work but returned no final response.");
|
|
3564
|
+
}
|
|
3565
|
+
if (synthesizedText) {
|
|
3566
|
+
responseText = synthesizedText;
|
|
3567
|
+
normalizedToolCalls = [];
|
|
3568
|
+
backendSynthesisPerformed = true;
|
|
3569
|
+
}
|
|
3570
|
+
usageSummary = mergeTokenUsage(usageSummary, normalizeTokenUsage(synthesisResult.usage));
|
|
3571
|
+
}
|
|
3572
|
+
const maxStepsReached = !backendSynthesisPerformed &&
|
|
3573
|
+
(toolCallsUsed >= maxToolCalls ||
|
|
3574
|
+
toolCallBudget.stopReason === "repeated_call") &&
|
|
3575
|
+
(endedWithoutFinalText || toolCallsUsed > 0);
|
|
3576
|
+
let completionState = "succeeded";
|
|
3577
|
+
let completionStopReason = "completed";
|
|
3578
|
+
if (normalizedToolCalls.length > 0) {
|
|
3579
|
+
completionState = "waiting_tool";
|
|
3580
|
+
completionStopReason = maxStepsReached ? "max_steps_reached" : "none";
|
|
3581
|
+
}
|
|
3582
|
+
else if (reflectionStopReason !== "none") {
|
|
3583
|
+
completionStopReason =
|
|
3584
|
+
toRunStopReasonFromReflection(reflectionStopReason);
|
|
3585
|
+
}
|
|
3586
|
+
await persistLifecycle(completionState, completionStopReason, "response_ready", {
|
|
3587
|
+
stepsUsed: stepsCount,
|
|
3588
|
+
toolCallsUsed,
|
|
3589
|
+
maxSteps: effectiveMaxSteps,
|
|
3590
|
+
pendingToolCalls: normalizedToolCalls.length,
|
|
3591
|
+
reflectionStopReason,
|
|
3592
|
+
});
|
|
3593
|
+
console.log(`📊 Steps used: ${stepsCount}/${effectiveMaxSteps} - Max reached: ${maxStepsReached}`);
|
|
3594
|
+
// ✅ FIX: Return the correct response object
|
|
3595
|
+
res.json({
|
|
3596
|
+
success: true,
|
|
3597
|
+
runId: resolvedRunId,
|
|
3598
|
+
lifecycleState,
|
|
3599
|
+
stopReason,
|
|
3600
|
+
response: responseText, // Use result.text, not aiResponse.content
|
|
3601
|
+
toolCalls: normalizedToolCalls, // Pending tool calls that need client execution
|
|
3602
|
+
executedToolResults: normalizedExecutedToolResults, // Tools that were already executed with results
|
|
3603
|
+
suspendedTools,
|
|
3604
|
+
thoughtSteps: uniqueThoughtSteps,
|
|
3605
|
+
model: modelId,
|
|
3606
|
+
autoFixAttempted,
|
|
3607
|
+
autoFixFailureCount,
|
|
3608
|
+
reflectionAttemptCount,
|
|
3609
|
+
reflectionStopReason,
|
|
3610
|
+
gitSafety,
|
|
3611
|
+
usage: usageSummary,
|
|
3612
|
+
...(nonStreamTokenUsageDebug
|
|
3613
|
+
? { tokenUsageDebug: nonStreamTokenUsageDebug }
|
|
3614
|
+
: {}),
|
|
3615
|
+
maxStepsReached: maxStepsReached, // Indicate if max steps was reached
|
|
3616
|
+
stepsUsed: stepsCount,
|
|
3617
|
+
toolCallsUsed,
|
|
3618
|
+
maxSteps: effectiveMaxSteps,
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3621
|
+
catch (error) {
|
|
3622
|
+
const err = error;
|
|
3623
|
+
console.error("❌ === AGENT ERROR ===");
|
|
3624
|
+
console.error("Error name:", err.name);
|
|
3625
|
+
console.error("Error message:", err.message);
|
|
3626
|
+
console.error("Error stack:", err.stack);
|
|
3627
|
+
console.error("Error details:", error);
|
|
3628
|
+
const { code, message: providerMessage } = extractProviderErrorDetails(error);
|
|
3629
|
+
if (activeRunId) {
|
|
3630
|
+
await safePersistRunLifecycleEvent({
|
|
3631
|
+
runId: activeRunId,
|
|
3632
|
+
lifecycleState: "failed",
|
|
3633
|
+
stopReason: "error",
|
|
3634
|
+
eventType: "route_error",
|
|
3635
|
+
payload: {
|
|
3636
|
+
message: providerMessage,
|
|
3637
|
+
code,
|
|
3638
|
+
},
|
|
3639
|
+
});
|
|
3640
|
+
}
|
|
3641
|
+
const message = providerMessage.toLowerCase();
|
|
3642
|
+
const isInsufficientQuota = code === "insufficient_quota" || message.includes("insufficient_quota");
|
|
3643
|
+
const isRateLimitError = code === "429" ||
|
|
3644
|
+
code?.toLowerCase().includes("rate_limit") ||
|
|
3645
|
+
message.includes("rate_limit") ||
|
|
3646
|
+
message.includes("rate limit") ||
|
|
3647
|
+
message.includes("rate-limited") ||
|
|
3648
|
+
message.includes("too many requests");
|
|
3649
|
+
if (isInsufficientQuota) {
|
|
3650
|
+
return res.status(429).json({
|
|
3651
|
+
success: false,
|
|
3652
|
+
runId: activeRunId,
|
|
3653
|
+
lifecycleState: "failed",
|
|
3654
|
+
stopReason: "error",
|
|
3655
|
+
error: "API quota exceeded. Please check your plan and billing details.",
|
|
3656
|
+
errorCode: "insufficient_quota",
|
|
3657
|
+
});
|
|
3658
|
+
}
|
|
3659
|
+
if (isRateLimitError) {
|
|
3660
|
+
return res.status(429).json({
|
|
3661
|
+
success: false,
|
|
3662
|
+
runId: activeRunId,
|
|
3663
|
+
lifecycleState: "failed",
|
|
3664
|
+
stopReason: "error",
|
|
3665
|
+
error: getRateLimitErrorMessage(requestedModelId),
|
|
3666
|
+
errorCode: "rate_limit_exceeded",
|
|
3667
|
+
});
|
|
3668
|
+
}
|
|
3669
|
+
res.status(500).json({
|
|
3670
|
+
success: false,
|
|
3671
|
+
runId: activeRunId,
|
|
3672
|
+
lifecycleState: "failed",
|
|
3673
|
+
stopReason: "error",
|
|
3674
|
+
error: normalizeProviderRequestError(requestedModelId, providerMessage, code),
|
|
3675
|
+
errorDetails: process.env.NODE_ENV === "development" ? err.stack : undefined,
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
finally {
|
|
3679
|
+
try {
|
|
3680
|
+
if (activeHostSession) {
|
|
3681
|
+
await activeHostSession.disconnect();
|
|
3682
|
+
}
|
|
3683
|
+
else if (activeHostSessionManager) {
|
|
3684
|
+
await activeHostSessionManager.stopAll();
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
catch (disconnectError) {
|
|
3688
|
+
const disconnectMessage = getErrorMessage(disconnectError);
|
|
3689
|
+
console.warn("[agent] failed to clean up host session:", disconnectMessage);
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
});
|
|
3693
|
+
router.get("/runs/:runId", async (req, res) => {
|
|
3694
|
+
const runId = req.params.runId?.trim();
|
|
3695
|
+
if (!runId || !isSafeRunId(runId)) {
|
|
3696
|
+
return res.status(400).json({ success: false, error: "Invalid run id" });
|
|
3697
|
+
}
|
|
3698
|
+
const snapshot = await getRunSnapshot(runId);
|
|
3699
|
+
if (!snapshot) {
|
|
3700
|
+
return res.status(404).json({ success: false, error: "Run not found" });
|
|
3701
|
+
}
|
|
3702
|
+
const parsePayload = (raw) => {
|
|
3703
|
+
if (!raw)
|
|
3704
|
+
return null;
|
|
3705
|
+
try {
|
|
3706
|
+
const parsed = JSON.parse(raw);
|
|
3707
|
+
return parsed && typeof parsed === "object"
|
|
3708
|
+
? parsed
|
|
3709
|
+
: null;
|
|
3710
|
+
}
|
|
3711
|
+
catch {
|
|
3712
|
+
return null;
|
|
3713
|
+
}
|
|
3714
|
+
};
|
|
3715
|
+
return res.json({
|
|
3716
|
+
success: true,
|
|
3717
|
+
run: {
|
|
3718
|
+
runId: snapshot.run.run_id,
|
|
3719
|
+
lifecycleState: snapshot.run.status,
|
|
3720
|
+
stopReason: snapshot.run.stop_reason,
|
|
3721
|
+
objective: snapshot.run.objective,
|
|
3722
|
+
workspacePath: snapshot.run.workspace_path,
|
|
3723
|
+
modelId: snapshot.run.model_id,
|
|
3724
|
+
cancelRequested: Number(snapshot.run.cancel_requested || 0) === 1,
|
|
3725
|
+
cancelRequestedAt: snapshot.run.cancel_requested_at || null,
|
|
3726
|
+
createdAt: snapshot.run.created_at,
|
|
3727
|
+
updatedAt: snapshot.run.updated_at,
|
|
3728
|
+
},
|
|
3729
|
+
latestCheckpoint: snapshot.latestCheckpoint
|
|
3730
|
+
? {
|
|
3731
|
+
sequence: snapshot.latestCheckpoint.sequence,
|
|
3732
|
+
lifecycleState: snapshot.latestCheckpoint.lifecycle_state,
|
|
3733
|
+
stopReason: snapshot.latestCheckpoint.stop_reason,
|
|
3734
|
+
eventType: snapshot.latestCheckpoint.event_type,
|
|
3735
|
+
payload: normalizeToolActionPayload(snapshot.latestCheckpoint.event_type, parsePayload(snapshot.latestCheckpoint.payload_json)),
|
|
3736
|
+
createdAt: snapshot.latestCheckpoint.created_at,
|
|
3737
|
+
}
|
|
3738
|
+
: null,
|
|
3739
|
+
});
|
|
3740
|
+
});
|
|
3741
|
+
router.get("/runs/:runId/events", async (req, res) => {
|
|
3742
|
+
const runId = req.params.runId?.trim();
|
|
3743
|
+
if (!runId || !isSafeRunId(runId)) {
|
|
3744
|
+
return res.status(400).json({ success: false, error: "Invalid run id" });
|
|
3745
|
+
}
|
|
3746
|
+
const afterSequence = parsePositiveIntQuery(req.query.afterSequence, 0, 0, 10_000_000);
|
|
3747
|
+
const limit = parsePositiveIntQuery(req.query.limit, 200, 1, 500);
|
|
3748
|
+
const events = await listRunEvents(runId, afterSequence, limit);
|
|
3749
|
+
const serialized = events.map((event) => {
|
|
3750
|
+
let payload = null;
|
|
3751
|
+
if (event.payload_json) {
|
|
3752
|
+
try {
|
|
3753
|
+
const parsed = JSON.parse(event.payload_json);
|
|
3754
|
+
payload =
|
|
3755
|
+
parsed && typeof parsed === "object"
|
|
3756
|
+
? parsed
|
|
3757
|
+
: null;
|
|
3758
|
+
}
|
|
3759
|
+
catch {
|
|
3760
|
+
payload = null;
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
return {
|
|
3764
|
+
runId: event.run_id,
|
|
3765
|
+
sequence: event.sequence,
|
|
3766
|
+
lifecycleState: event.lifecycle_state,
|
|
3767
|
+
stopReason: event.stop_reason,
|
|
3768
|
+
eventType: event.event_type,
|
|
3769
|
+
payload: normalizeToolActionPayload(event.event_type, payload),
|
|
3770
|
+
createdAt: event.created_at,
|
|
3771
|
+
};
|
|
3772
|
+
});
|
|
3773
|
+
return res.json({
|
|
3774
|
+
success: true,
|
|
3775
|
+
runId,
|
|
3776
|
+
afterSequence,
|
|
3777
|
+
count: serialized.length,
|
|
3778
|
+
events: serialized,
|
|
3779
|
+
});
|
|
3780
|
+
});
|
|
3781
|
+
router.post("/runs/:runId/cancel", async (req, res) => {
|
|
3782
|
+
const runId = req.params.runId?.trim();
|
|
3783
|
+
if (!runId || !isSafeRunId(runId)) {
|
|
3784
|
+
return res.status(400).json({ success: false, error: "Invalid run id" });
|
|
3785
|
+
}
|
|
3786
|
+
const exists = await getRunSnapshot(runId);
|
|
3787
|
+
if (!exists) {
|
|
3788
|
+
return res.status(404).json({ success: false, error: "Run not found" });
|
|
3789
|
+
}
|
|
3790
|
+
const updated = await requestRunCancellation(runId);
|
|
3791
|
+
if (!updated) {
|
|
3792
|
+
return res
|
|
3793
|
+
.status(409)
|
|
3794
|
+
.json({ success: false, error: "Unable to request cancellation" });
|
|
3795
|
+
}
|
|
3796
|
+
const currentState = asRunLifecycleState(exists.run.status);
|
|
3797
|
+
await safePersistRunLifecycleEvent({
|
|
3798
|
+
runId,
|
|
3799
|
+
lifecycleState: currentState,
|
|
3800
|
+
stopReason: "cancelled",
|
|
3801
|
+
eventType: "cancel_requested",
|
|
3802
|
+
payload: {
|
|
3803
|
+
requestedAt: new Date().toISOString(),
|
|
3804
|
+
},
|
|
3805
|
+
});
|
|
3806
|
+
return res.json({
|
|
3807
|
+
success: true,
|
|
3808
|
+
runId,
|
|
3809
|
+
cancelRequested: true,
|
|
3810
|
+
lifecycleState: currentState,
|
|
3811
|
+
stopReason: "cancelled",
|
|
3812
|
+
});
|
|
3813
|
+
});
|
|
3814
|
+
// POST /api/agent/command-confirmation - Handle user approval/skip for command execution
|
|
3815
|
+
router.post("/command-confirmation", async (req, res) => {
|
|
3816
|
+
try {
|
|
3817
|
+
const { confirmationId, approved, toolArgs, workspaceRoot } = req.body;
|
|
3818
|
+
if (!confirmationId || typeof approved !== "boolean") {
|
|
3819
|
+
return res.status(400).json({
|
|
3820
|
+
error: "confirmationId and approved (boolean) are required",
|
|
3821
|
+
});
|
|
3822
|
+
}
|
|
3823
|
+
console.log(`📋 Command confirmation ${confirmationId}: ${approved ? "✅ Approved" : "❌ Skipped"}`);
|
|
3824
|
+
if (!approved) {
|
|
3825
|
+
return res.json({
|
|
3826
|
+
success: true,
|
|
3827
|
+
skipped: true,
|
|
3828
|
+
output: "Command execution skipped by user",
|
|
3829
|
+
});
|
|
3830
|
+
}
|
|
3831
|
+
// User approved - execute the command
|
|
3832
|
+
// Import executeCommand function
|
|
3833
|
+
const { executeCommand } = await import("./core/agent/tools/executeCommand.js");
|
|
3834
|
+
// Execute with approval bypass (since user already approved)
|
|
3835
|
+
const result = await executeCommand({
|
|
3836
|
+
...toolArgs,
|
|
3837
|
+
workspaceRoot: typeof workspaceRoot === "string" && workspaceRoot.trim().length > 0
|
|
3838
|
+
? workspaceRoot
|
|
3839
|
+
: typeof toolArgs?.cwd === "string" &&
|
|
3840
|
+
toolArgs.cwd.trim().length > 0
|
|
3841
|
+
? toolArgs.cwd
|
|
3842
|
+
: process.cwd(),
|
|
3843
|
+
skipConfirmation: true, // Flag to bypass confirmation check
|
|
3844
|
+
});
|
|
3845
|
+
res.json({
|
|
3846
|
+
success: true,
|
|
3847
|
+
approved: true,
|
|
3848
|
+
result,
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
catch (error) {
|
|
3852
|
+
const err = error;
|
|
3853
|
+
console.error("❌ Command execution error:", error);
|
|
3854
|
+
res.status(500).json({
|
|
3855
|
+
success: false,
|
|
3856
|
+
error: err.message || "Failed to execute command",
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
});
|
|
3860
|
+
registerFileRoutes(router, requireDesktopAuth);
|
|
3861
|
+
registerSemanticRoutes(router);
|
|
3862
|
+
registerLspDocumentRoutes(router, requireDesktopAuth);
|
|
3863
|
+
registerLspSemanticDocumentRoutes(router, requireDesktopAuth);
|
|
3864
|
+
registerLspHierarchyRoutes(router, requireDesktopAuth);
|
|
3865
|
+
registerLspResolveRoutes(router, requireDesktopAuth);
|
|
3866
|
+
registerLspPositionRoutes(router, requireDesktopAuth);
|
|
3867
|
+
registerLspQueryRoutes(router, requireDesktopAuth);
|
|
3868
|
+
// ── Desktop security helpers ────────────────────────────────────────────────
|
|
3869
|
+
/** True only for IPv4/IPv6 loopback connections. */
|
|
3870
|
+
function isLoopback(req) {
|
|
3871
|
+
const ip = req.socket.remoteAddress ?? "";
|
|
3872
|
+
return ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
|
|
3873
|
+
}
|
|
3874
|
+
function hasDesktopAuth(req) {
|
|
3875
|
+
const token = process.env.IRIS_DESKTOP_TOKEN;
|
|
3876
|
+
if (!process.env.TAURI_BUNDLED || !token) {
|
|
3877
|
+
return false;
|
|
3878
|
+
}
|
|
3879
|
+
if (!isLoopback(req)) {
|
|
3880
|
+
return false;
|
|
3881
|
+
}
|
|
3882
|
+
const raw = req.headers["x-desktop-token"];
|
|
3883
|
+
const provided = Array.isArray(raw) ? raw[0] : raw;
|
|
3884
|
+
return Boolean(provided && provided === token);
|
|
3885
|
+
}
|
|
3886
|
+
/**
|
|
3887
|
+
* Reject with 403 unless ALL of:
|
|
3888
|
+
* 1. The server was started as a Tauri desktop sidecar (TAURI_BUNDLED=1)
|
|
3889
|
+
* 2. The request originates from the loopback interface
|
|
3890
|
+
* 3. The X-Desktop-Token header matches the per-launch secret
|
|
3891
|
+
*
|
|
3892
|
+
* This gates the mutable /keys endpoint so arbitrary web pages or local
|
|
3893
|
+
* processes cannot change API keys on behalf of the user.
|
|
3894
|
+
*/
|
|
3895
|
+
export function requireDesktopAuth(req, res, next) {
|
|
3896
|
+
if (!hasDesktopAuth(req)) {
|
|
3897
|
+
return res.status(403).json({ error: "Forbidden" });
|
|
3898
|
+
}
|
|
3899
|
+
next();
|
|
3900
|
+
}
|
|
3901
|
+
// ── API Key management ─────────────────────────────────────────────
|
|
3902
|
+
// Allowed env vars that the frontend can set via this endpoint.
|
|
3903
|
+
const ALLOWED_KEY_NAMES = new Set([
|
|
3904
|
+
"OPENAI_API_KEY",
|
|
3905
|
+
"ANTHROPIC_API_KEY",
|
|
3906
|
+
"GOOGLE_GENERATIVE_AI_API_KEY",
|
|
3907
|
+
"OPENROUTER_API_KEY",
|
|
3908
|
+
"OPENROUTER_BASE_URL",
|
|
3909
|
+
"OLLAMA_BASE_URL",
|
|
3910
|
+
"OLLAMA_API_KEY",
|
|
3911
|
+
"HF_TOKEN",
|
|
3912
|
+
]);
|
|
3913
|
+
// POST /api/agent/keys - Set API keys at runtime (desktop app only)
|
|
3914
|
+
router.post("/keys", requireDesktopAuth, (req, res) => {
|
|
3915
|
+
const keys = req.body?.keys;
|
|
3916
|
+
if (!keys || typeof keys !== "object") {
|
|
3917
|
+
return res
|
|
3918
|
+
.status(400)
|
|
3919
|
+
.json({ success: false, error: "Missing keys object" });
|
|
3920
|
+
}
|
|
3921
|
+
const applied = [];
|
|
3922
|
+
for (const [name, value] of Object.entries(keys)) {
|
|
3923
|
+
if (!ALLOWED_KEY_NAMES.has(name))
|
|
3924
|
+
continue;
|
|
3925
|
+
if (typeof value === "string") {
|
|
3926
|
+
const trimmedValue = value.trim();
|
|
3927
|
+
if (trimmedValue.length > 0) {
|
|
3928
|
+
process.env[name] = trimmedValue;
|
|
3929
|
+
applied.push(name);
|
|
3930
|
+
continue;
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
if (value === "" ||
|
|
3934
|
+
value === null ||
|
|
3935
|
+
(typeof value === "string" && value.trim().length === 0)) {
|
|
3936
|
+
delete process.env[name];
|
|
3937
|
+
applied.push(name);
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
// Clear cached agents so they pick up the new keys on next request
|
|
3941
|
+
if (applied.length > 0) {
|
|
3942
|
+
agentCache.clear();
|
|
3943
|
+
}
|
|
3944
|
+
res.json({ success: true, applied });
|
|
3945
|
+
});
|
|
3946
|
+
// GET /api/agent/keys - Check which keys are configured (never returns values)
|
|
3947
|
+
router.get("/keys", requireDesktopAuth, (_req, res) => {
|
|
3948
|
+
const status = {};
|
|
3949
|
+
for (const name of ALLOWED_KEY_NAMES) {
|
|
3950
|
+
status[name] = Boolean(process.env[name]?.trim());
|
|
3951
|
+
}
|
|
3952
|
+
res.json({ success: true, keys: status });
|
|
3953
|
+
});
|
|
3954
|
+
// GET /api/agent/keys/values - Return runtime key values (desktop app only)
|
|
3955
|
+
router.get("/keys/values", requireDesktopAuth, (_req, res) => {
|
|
3956
|
+
const keys = {};
|
|
3957
|
+
for (const name of ALLOWED_KEY_NAMES) {
|
|
3958
|
+
keys[name] = process.env[name]?.trim() || "";
|
|
3959
|
+
}
|
|
3960
|
+
res.json({ success: true, keys });
|
|
3961
|
+
});
|
|
3962
|
+
router.get("/chat-sessions/:sessionId", requireDesktopAuth, async (req, res) => {
|
|
3963
|
+
const sessionId = req.params.sessionId?.trim();
|
|
3964
|
+
if (!sessionId || !isSafeSessionId(sessionId)) {
|
|
3965
|
+
return res
|
|
3966
|
+
.status(400)
|
|
3967
|
+
.json({ success: false, error: "Invalid session id" });
|
|
3968
|
+
}
|
|
3969
|
+
const sessionPath = getRemoteChatSessionPath(sessionId);
|
|
3970
|
+
try {
|
|
3971
|
+
const raw = await fs.readFile(sessionPath, "utf8");
|
|
3972
|
+
const parsed = JSON.parse(raw);
|
|
3973
|
+
if (!Array.isArray(parsed?.messages)) {
|
|
3974
|
+
return res
|
|
3975
|
+
.status(404)
|
|
3976
|
+
.json({ success: false, error: "Session not found" });
|
|
3977
|
+
}
|
|
3978
|
+
return res.json({
|
|
3979
|
+
id: sessionId,
|
|
3980
|
+
title: typeof parsed.title === "string" ? parsed.title : undefined,
|
|
3981
|
+
createdAt: typeof parsed.createdAt === "number" ? parsed.createdAt : undefined,
|
|
3982
|
+
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : undefined,
|
|
3983
|
+
messages: parsed.messages,
|
|
3984
|
+
});
|
|
3985
|
+
}
|
|
3986
|
+
catch (error) {
|
|
3987
|
+
const err = error;
|
|
3988
|
+
if (err.code === "ENOENT") {
|
|
3989
|
+
return res
|
|
3990
|
+
.status(404)
|
|
3991
|
+
.json({ success: false, error: "Session not found" });
|
|
3992
|
+
}
|
|
3993
|
+
return res
|
|
3994
|
+
.status(500)
|
|
3995
|
+
.json({ success: false, error: "Failed to read session" });
|
|
3996
|
+
}
|
|
3997
|
+
});
|
|
3998
|
+
router.put("/chat-sessions/:sessionId", requireDesktopAuth, async (req, res) => {
|
|
3999
|
+
const sessionId = req.params.sessionId?.trim();
|
|
4000
|
+
if (!sessionId || !isSafeSessionId(sessionId)) {
|
|
4001
|
+
return res
|
|
4002
|
+
.status(400)
|
|
4003
|
+
.json({ success: false, error: "Invalid session id" });
|
|
4004
|
+
}
|
|
4005
|
+
if (!Array.isArray(req.body?.messages)) {
|
|
4006
|
+
return res
|
|
4007
|
+
.status(400)
|
|
4008
|
+
.json({ success: false, error: "messages must be an array" });
|
|
4009
|
+
}
|
|
4010
|
+
const timestamp = Date.now();
|
|
4011
|
+
const payload = {
|
|
4012
|
+
id: sessionId,
|
|
4013
|
+
title: typeof req.body.title === "string" ? req.body.title : "New Chat",
|
|
4014
|
+
createdAt: typeof req.body.createdAt === "number" ? req.body.createdAt : timestamp,
|
|
4015
|
+
updatedAt: typeof req.body.updatedAt === "number" ? req.body.updatedAt : timestamp,
|
|
4016
|
+
messages: req.body.messages,
|
|
4017
|
+
};
|
|
4018
|
+
try {
|
|
4019
|
+
await fs.mkdir(REMOTE_CHAT_SESSIONS_DIR, { recursive: true });
|
|
4020
|
+
await fs.writeFile(getRemoteChatSessionPath(sessionId), JSON.stringify(payload, null, 2), "utf8");
|
|
4021
|
+
return res.json({ success: true, syncedAt: timestamp });
|
|
4022
|
+
}
|
|
4023
|
+
catch {
|
|
4024
|
+
return res
|
|
4025
|
+
.status(500)
|
|
4026
|
+
.json({ success: false, error: "Failed to store session" });
|
|
4027
|
+
}
|
|
4028
|
+
});
|
|
4029
|
+
router.delete("/chat-sessions/:sessionId", requireDesktopAuth, async (req, res) => {
|
|
4030
|
+
const sessionId = req.params.sessionId?.trim();
|
|
4031
|
+
if (!sessionId || !isSafeSessionId(sessionId)) {
|
|
4032
|
+
return res
|
|
4033
|
+
.status(400)
|
|
4034
|
+
.json({ success: false, error: "Invalid session id" });
|
|
4035
|
+
}
|
|
4036
|
+
try {
|
|
4037
|
+
const sessionPath = getRemoteChatSessionPath(sessionId);
|
|
4038
|
+
const rawSession = await fs.readFile(sessionPath, "utf8");
|
|
4039
|
+
const parsed = JSON.parse(rawSession);
|
|
4040
|
+
const runIds = extractRunIdsFromRemoteSession(parsed);
|
|
4041
|
+
if (runIds.length > 0) {
|
|
4042
|
+
await deleteRunDataBatch(runIds);
|
|
4043
|
+
}
|
|
4044
|
+
await fs.unlink(sessionPath);
|
|
4045
|
+
return res.json({ success: true });
|
|
4046
|
+
}
|
|
4047
|
+
catch (error) {
|
|
4048
|
+
const err = error;
|
|
4049
|
+
if (err.code === "ENOENT") {
|
|
4050
|
+
return res.json({ success: true });
|
|
4051
|
+
}
|
|
4052
|
+
if (error instanceof SyntaxError) {
|
|
4053
|
+
return res
|
|
4054
|
+
.status(500)
|
|
4055
|
+
.json({ success: false, error: "Failed to parse session" });
|
|
4056
|
+
}
|
|
4057
|
+
return res
|
|
4058
|
+
.status(500)
|
|
4059
|
+
.json({ success: false, error: "Failed to delete session" });
|
|
4060
|
+
}
|
|
4061
|
+
});
|
|
4062
|
+
router.post("/mcp/inspect", requireDesktopAuth, async (req, res) => {
|
|
4063
|
+
const serverList = sanitizeMcpServers([req.body?.server], 1);
|
|
4064
|
+
const server = serverList[0];
|
|
4065
|
+
if (!server) {
|
|
4066
|
+
return res
|
|
4067
|
+
.status(400)
|
|
4068
|
+
.json({ success: false, error: "Invalid MCP server configuration" });
|
|
4069
|
+
}
|
|
4070
|
+
try {
|
|
4071
|
+
const tools = await listMcpServerTools(server, process.cwd());
|
|
4072
|
+
return res.json({
|
|
4073
|
+
success: true,
|
|
4074
|
+
server: {
|
|
4075
|
+
id: server.id,
|
|
4076
|
+
name: server.name,
|
|
4077
|
+
command: server.command,
|
|
4078
|
+
},
|
|
4079
|
+
tools: tools.map((tool) => ({
|
|
4080
|
+
name: tool.name,
|
|
4081
|
+
description: tool.description || "",
|
|
4082
|
+
})),
|
|
4083
|
+
toolCount: tools.length,
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
catch (error) {
|
|
4087
|
+
const err = error;
|
|
4088
|
+
return res.status(500).json({
|
|
4089
|
+
success: false,
|
|
4090
|
+
error: err.message || "Failed to inspect MCP server",
|
|
4091
|
+
});
|
|
4092
|
+
}
|
|
4093
|
+
});
|
|
4094
|
+
router.post("/mcp/call", requireDesktopAuth, async (req, res) => {
|
|
4095
|
+
console.log("🧩 === MCP TOOL CALL RECEIVED ===");
|
|
4096
|
+
console.log("Tool name:", req.body?.toolName);
|
|
4097
|
+
console.log("Args keys:", Object.keys(req.body?.args || {}));
|
|
4098
|
+
console.log("Workspace root:", req.body?.workspaceRoot);
|
|
4099
|
+
console.log("MCP servers:", req.body?.mcpServers);
|
|
4100
|
+
const toolName = typeof req.body?.toolName === "string" ? req.body.toolName.trim() : "";
|
|
4101
|
+
const args = req.body?.args &&
|
|
4102
|
+
typeof req.body.args === "object" &&
|
|
4103
|
+
!Array.isArray(req.body.args)
|
|
4104
|
+
? req.body.args
|
|
4105
|
+
: {};
|
|
4106
|
+
const workspaceRoot = req.body?.workspaceRoot || process.cwd();
|
|
4107
|
+
const mcpServers = sanitizeMcpServers(req.body?.mcpServers);
|
|
4108
|
+
console.log("Sanitized MCP servers count:", mcpServers.length);
|
|
4109
|
+
if (!toolName.startsWith("mcp_")) {
|
|
4110
|
+
console.warn("⚠️ Invalid MCP tool name:", toolName);
|
|
4111
|
+
return res
|
|
4112
|
+
.status(400)
|
|
4113
|
+
.json({ success: false, error: "Invalid MCP tool name" });
|
|
4114
|
+
}
|
|
4115
|
+
if (mcpServers.length === 0) {
|
|
4116
|
+
console.warn("⚠️ No MCP servers configured");
|
|
4117
|
+
return res
|
|
4118
|
+
.status(400)
|
|
4119
|
+
.json({ success: false, error: "No MCP servers configured" });
|
|
4120
|
+
}
|
|
4121
|
+
try {
|
|
4122
|
+
console.log("🚀 Executing MCP tool:", toolName);
|
|
4123
|
+
const result = await executeMcpToolByKey(mcpServers, workspaceRoot, toolName, args);
|
|
4124
|
+
console.log("✅ MCP tool execution result:", result);
|
|
4125
|
+
return res.json(result);
|
|
4126
|
+
}
|
|
4127
|
+
catch (error) {
|
|
4128
|
+
const err = error;
|
|
4129
|
+
console.error("❌ MCP tool execution error:", err.message);
|
|
4130
|
+
console.error("Stack:", err.stack);
|
|
4131
|
+
return res.status(500).json({
|
|
4132
|
+
success: false,
|
|
4133
|
+
error: err.message || "Failed to execute MCP tool",
|
|
4134
|
+
});
|
|
4135
|
+
}
|
|
4136
|
+
});
|
|
4137
|
+
router.get("/skills", async (_req, res) => {
|
|
4138
|
+
try {
|
|
4139
|
+
const { getSkillsList } = await loadAgentCoreModule();
|
|
4140
|
+
const skills = await getSkillsList();
|
|
4141
|
+
res.json({ success: true, skills });
|
|
4142
|
+
}
|
|
4143
|
+
catch {
|
|
4144
|
+
res.status(500).json({ success: false, error: "Failed to load skills" });
|
|
4145
|
+
}
|
|
4146
|
+
});
|
|
4147
|
+
// GET /api/agent/tools - List all available Language Model Tools (native + MCP)
|
|
4148
|
+
router.get("/tools", async (req, res) => {
|
|
4149
|
+
try {
|
|
4150
|
+
const tools = await getAvailableTools();
|
|
4151
|
+
res.json({
|
|
4152
|
+
success: true,
|
|
4153
|
+
count: tools.length,
|
|
4154
|
+
tools: tools.map((tool) => ({
|
|
4155
|
+
name: tool.name,
|
|
4156
|
+
description: tool.description,
|
|
4157
|
+
tags: tool.tags,
|
|
4158
|
+
inputSchema: tool.inputSchema,
|
|
4159
|
+
})),
|
|
4160
|
+
});
|
|
4161
|
+
}
|
|
4162
|
+
catch (error) {
|
|
4163
|
+
const err = error;
|
|
4164
|
+
console.error("❌ Failed to list tools:", error);
|
|
4165
|
+
res.status(500).json({
|
|
4166
|
+
success: false,
|
|
4167
|
+
error: err.message || "Failed to list tools",
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
});
|
|
4171
|
+
export default router;
|