@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,238 @@
|
|
|
1
|
+
import { TokenLimiterProcessor } from "@mastra/core/processors";
|
|
2
|
+
const EMPTY_BUDGET_ERROR_MARKER = "No messages fit within the remaining token budget";
|
|
3
|
+
const EMERGENCY_TRUNCATION_NOTICE = "[conversation truncated to fit token budget for this step]";
|
|
4
|
+
const EMERGENCY_MAX_CONTENT_CHARS = 1_200;
|
|
5
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
6
|
+
const asString = (value) => typeof value === "string" ? value : "";
|
|
7
|
+
const truncateText = (value, maxChars = EMERGENCY_MAX_CONTENT_CHARS) => {
|
|
8
|
+
if (value.length <= maxChars) {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
const head = value.slice(0, Math.max(0, maxChars - EMERGENCY_TRUNCATION_NOTICE.length - 2));
|
|
12
|
+
return `${head}\n${EMERGENCY_TRUNCATION_NOTICE}`;
|
|
13
|
+
};
|
|
14
|
+
const getMessageParts = (message) => {
|
|
15
|
+
if (Array.isArray(message.content)) {
|
|
16
|
+
return message.content;
|
|
17
|
+
}
|
|
18
|
+
if (isRecord(message.content) && Array.isArray(message.content.parts)) {
|
|
19
|
+
return message.content.parts;
|
|
20
|
+
}
|
|
21
|
+
return [];
|
|
22
|
+
};
|
|
23
|
+
const getMessageId = (message) => {
|
|
24
|
+
const id = message.id;
|
|
25
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
26
|
+
};
|
|
27
|
+
const getMessageRole = (message) => {
|
|
28
|
+
const role = message.role;
|
|
29
|
+
return typeof role === "string" ? role : "";
|
|
30
|
+
};
|
|
31
|
+
const compactMessageContent = (message) => {
|
|
32
|
+
const content = message.content;
|
|
33
|
+
if (typeof content === "string") {
|
|
34
|
+
message.content = truncateText(content);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(content)) {
|
|
38
|
+
const textParts = content
|
|
39
|
+
.filter((part) => isRecord(part) && part.type === "text")
|
|
40
|
+
.map((part) => asString(part.text).trim())
|
|
41
|
+
.filter((text) => text.length > 0);
|
|
42
|
+
const merged = textParts.join("\n\n").trim();
|
|
43
|
+
message.content = truncateText(merged || EMERGENCY_TRUNCATION_NOTICE);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (isRecord(content) && Array.isArray(content.parts)) {
|
|
47
|
+
const textParts = content.parts
|
|
48
|
+
.filter((part) => isRecord(part) && part.type === "text")
|
|
49
|
+
.map((part) => asString(part.text).trim())
|
|
50
|
+
.filter((text) => text.length > 0);
|
|
51
|
+
const merged = textParts.join("\n\n").trim();
|
|
52
|
+
message.content = truncateText(merged || EMERGENCY_TRUNCATION_NOTICE);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
message.content = EMERGENCY_TRUNCATION_NOTICE;
|
|
56
|
+
};
|
|
57
|
+
const applyEmergencyBudgetFallback = async (messageList) => {
|
|
58
|
+
const messages = messageList?.get?.all?.db?.();
|
|
59
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const records = messages.filter((msg) => isRecord(msg));
|
|
63
|
+
if (records.length === 0) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
let latestUserMessage = null;
|
|
67
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
68
|
+
if (getMessageRole(records[index]) === "user") {
|
|
69
|
+
latestUserMessage = records[index];
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const lastMessage = records[records.length - 1] || null;
|
|
74
|
+
const keepSet = new Set();
|
|
75
|
+
if (latestUserMessage)
|
|
76
|
+
keepSet.add(latestUserMessage);
|
|
77
|
+
if (lastMessage)
|
|
78
|
+
keepSet.add(lastMessage);
|
|
79
|
+
if (keepSet.size === 0 && records[0])
|
|
80
|
+
keepSet.add(records[0]);
|
|
81
|
+
const removeIds = records
|
|
82
|
+
.filter((message) => !keepSet.has(message))
|
|
83
|
+
.map((message) => getMessageId(message))
|
|
84
|
+
.filter((id) => Boolean(id));
|
|
85
|
+
if (removeIds.length > 0 && typeof messageList.removeByIds === "function") {
|
|
86
|
+
await messageList.removeByIds(removeIds);
|
|
87
|
+
}
|
|
88
|
+
keepSet.forEach((message) => compactMessageContent(message));
|
|
89
|
+
return removeIds.length > 0 || keepSet.size > 0;
|
|
90
|
+
};
|
|
91
|
+
const isNoMessagesFitBudgetError = (error) => {
|
|
92
|
+
if (!(error instanceof Error)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return error.message.includes(EMPTY_BUDGET_ERROR_MARKER);
|
|
96
|
+
};
|
|
97
|
+
const isImagePart = (part) => {
|
|
98
|
+
if (part.type === "image" || part.type === "image-data" || part.type === "image-url") {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (part.type === "media") {
|
|
102
|
+
const mediaType = typeof part.mediaType === "string" ? part.mediaType : "";
|
|
103
|
+
return mediaType.startsWith("image/");
|
|
104
|
+
}
|
|
105
|
+
const mediaType = typeof part.mediaType === "string"
|
|
106
|
+
? part.mediaType
|
|
107
|
+
: typeof part.mimeType === "string"
|
|
108
|
+
? part.mimeType
|
|
109
|
+
: "";
|
|
110
|
+
return part.type === "file" && mediaType.startsWith("image/");
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Tool-result parts nest their payload inside `output.value` (AI SDK v2
|
|
114
|
+
* `LanguageModelV2ToolResultOutput` with `type: 'content'`), e.g. an MCP
|
|
115
|
+
* screenshot tool returns `{ type: 'tool-result', output: { type: 'content',
|
|
116
|
+
* value: [{ type: 'media', data, mediaType }] } }`. Images here are *not*
|
|
117
|
+
* top-level content parts, so callers must also inspect this nested array to
|
|
118
|
+
* find/redact images embedded in tool results.
|
|
119
|
+
*/
|
|
120
|
+
const getToolResultOutputValueParts = (part) => {
|
|
121
|
+
if (part.type !== "tool-result") {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const output = part.output;
|
|
125
|
+
if (!isRecord(output) || output.type !== "content" || !Array.isArray(output.value)) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
return output.value.filter(isRecord);
|
|
129
|
+
};
|
|
130
|
+
const redactPartPayload = (part, restorations) => {
|
|
131
|
+
for (const key of ["image", "data", "url"]) {
|
|
132
|
+
if (!(key in part)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const payload = part[key];
|
|
136
|
+
part[key] = "[multimodal image]";
|
|
137
|
+
restorations.push(() => {
|
|
138
|
+
part[key] = payload;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
export const redactMultimodalPayloads = (messages) => {
|
|
143
|
+
const restorations = [];
|
|
144
|
+
for (const message of messages) {
|
|
145
|
+
if (!isRecord(message)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
for (const part of getMessageParts(message)) {
|
|
149
|
+
if (!isRecord(part)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (isImagePart(part)) {
|
|
153
|
+
redactPartPayload(part, restorations);
|
|
154
|
+
}
|
|
155
|
+
for (const nested of getToolResultOutputValueParts(part)) {
|
|
156
|
+
if (isImagePart(nested)) {
|
|
157
|
+
redactPartPayload(nested, restorations);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return () => {
|
|
163
|
+
for (const restore of restorations) {
|
|
164
|
+
restore();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
const IMAGE_UNSUPPORTED_PLACEHOLDER_TEXT = "[Image omitted: the selected model does not support image input]";
|
|
169
|
+
/**
|
|
170
|
+
* Permanently converts image content parts (e.g. tool-result screenshots or
|
|
171
|
+
* user-attached images) into a text placeholder. Used when the target model
|
|
172
|
+
* does not support image input, to avoid provider-level 400/404 errors such
|
|
173
|
+
* as OpenRouter's "No endpoints found that support image input".
|
|
174
|
+
*/
|
|
175
|
+
const stripUnsupportedImageParts = (messages) => {
|
|
176
|
+
for (const message of messages) {
|
|
177
|
+
if (!isRecord(message)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const parts = getMessageParts(message);
|
|
181
|
+
if (parts.length === 0) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
185
|
+
const part = parts[i];
|
|
186
|
+
if (!isRecord(part)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (isImagePart(part)) {
|
|
190
|
+
parts[i] = { type: "text", text: IMAGE_UNSUPPORTED_PLACEHOLDER_TEXT };
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Tool-result parts carry images nested inside `output.value[]`; strip
|
|
194
|
+
// those in place so the surrounding tool-result part (and its other
|
|
195
|
+
// text entries) are preserved.
|
|
196
|
+
const nestedParts = getToolResultOutputValueParts(part);
|
|
197
|
+
if (nestedParts.length === 0) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
for (let j = 0; j < nestedParts.length; j += 1) {
|
|
201
|
+
if (isImagePart(nestedParts[j])) {
|
|
202
|
+
nestedParts[j] = { type: "text", text: IMAGE_UNSUPPORTED_PLACEHOLDER_TEXT };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
part.output.value = nestedParts;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
export class MultimodalTokenLimiterProcessor extends TokenLimiterProcessor {
|
|
210
|
+
supportsVision;
|
|
211
|
+
constructor(limit, supportsVision = true) {
|
|
212
|
+
super(limit);
|
|
213
|
+
this.supportsVision = supportsVision;
|
|
214
|
+
}
|
|
215
|
+
async processInputStep(args) {
|
|
216
|
+
const messages = args.messageList.get.all.db();
|
|
217
|
+
if (!this.supportsVision) {
|
|
218
|
+
stripUnsupportedImageParts(messages);
|
|
219
|
+
}
|
|
220
|
+
const restore = redactMultimodalPayloads(messages);
|
|
221
|
+
try {
|
|
222
|
+
await super.processInputStep(args);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (!isNoMessagesFitBudgetError(error)) {
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
const didApplyFallback = await applyEmergencyBudgetFallback(args.messageList);
|
|
229
|
+
if (!didApplyFallback) {
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
await super.processInputStep(args);
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
restore();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type OpenRouterModelSettings = {
|
|
2
|
+
reasoning?: {
|
|
3
|
+
max_tokens: number;
|
|
4
|
+
};
|
|
5
|
+
provider: {
|
|
6
|
+
order?: string[];
|
|
7
|
+
allow_fallbacks?: boolean;
|
|
8
|
+
require_parameters?: boolean;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export declare const resolveOpenRouterModelSettings: (modelId: string) => OpenRouterModelSettings | undefined;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const resolveOpenRouterModelSettings = (modelId) => {
|
|
2
|
+
const id = modelId.toLowerCase();
|
|
3
|
+
if (id === "moonshotai/kimi-k3") {
|
|
4
|
+
return {
|
|
5
|
+
reasoning: {
|
|
6
|
+
max_tokens: 2048,
|
|
7
|
+
},
|
|
8
|
+
provider: {
|
|
9
|
+
order: ["moonshotai", "baseten", "wafer", "morph", "fireworks"],
|
|
10
|
+
allow_fallbacks: true,
|
|
11
|
+
require_parameters: true,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (id === "z-ai/glm-5.2") {
|
|
16
|
+
return {
|
|
17
|
+
provider: {
|
|
18
|
+
order: ["z-ai"],
|
|
19
|
+
allow_fallbacks: false,
|
|
20
|
+
require_parameters: true,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (id === "deepseek/deepseek-v4-flash" || id === "deepseek/deepseek-chat") {
|
|
25
|
+
return {
|
|
26
|
+
provider: {
|
|
27
|
+
order: ["deepseek", "together", "fireworks", "lepton"],
|
|
28
|
+
allow_fallbacks: true,
|
|
29
|
+
require_parameters: true,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const buildRecoveryQueries: (inputPath: string, basePath: string) => string[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
export const buildRecoveryQueries = (inputPath, basePath) => {
|
|
3
|
+
const normalizedInput = inputPath.replace(/\\/g, "/");
|
|
4
|
+
const queries = new Set();
|
|
5
|
+
queries.add(inputPath);
|
|
6
|
+
const baseName = path.basename(normalizedInput);
|
|
7
|
+
if (baseName) {
|
|
8
|
+
queries.add(baseName);
|
|
9
|
+
}
|
|
10
|
+
if (path.isAbsolute(inputPath)) {
|
|
11
|
+
const resolved = path.resolve(inputPath).replace(/\\/g, "/");
|
|
12
|
+
const workspaceName = path.basename(basePath.replace(/\\/g, "/"));
|
|
13
|
+
const workspaceAnchor = `/${workspaceName}/`;
|
|
14
|
+
const anchorIndex = resolved.indexOf(workspaceAnchor);
|
|
15
|
+
if (anchorIndex >= 0) {
|
|
16
|
+
const afterAnchor = resolved.slice(anchorIndex + workspaceAnchor.length);
|
|
17
|
+
if (afterAnchor) {
|
|
18
|
+
queries.add(afterAnchor);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const segments = resolved.split("/").filter(Boolean);
|
|
22
|
+
for (let depth = 2; depth <= 4; depth++) {
|
|
23
|
+
if (segments.length >= depth) {
|
|
24
|
+
queries.add(segments.slice(-depth).join("/"));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return Array.from(queries).filter(Boolean);
|
|
29
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const findFilesViaRepoMap: (workspaceRoot: string, query: string, limit?: number) => Promise<string[]>;
|
|
2
|
+
export declare const rankFileCandidates: (workspaceRoot: string, query: string, candidates: string[]) => Promise<string[]>;
|
|
3
|
+
export declare const clearRepoMapCache: () => void;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { glob } from "glob";
|
|
4
|
+
const REPO_MAP_TTL_MS = 30_000;
|
|
5
|
+
const MAX_INDEX_FILES = 2_000;
|
|
6
|
+
const MAX_FILE_SIZE_BYTES = 512 * 1024;
|
|
7
|
+
const cache = new Map();
|
|
8
|
+
const SOURCE_PATTERNS = [
|
|
9
|
+
"src/**/*.{ts,tsx,js,jsx,mjs,cjs,json,md,css,scss,yml,yaml}",
|
|
10
|
+
"api/**/*.{ts,tsx,js,jsx,mjs,cjs,json,md,yml,yaml}",
|
|
11
|
+
"tests/**/*.{ts,tsx,js,jsx,mjs,cjs,json,md}",
|
|
12
|
+
"*.{ts,tsx,js,jsx,mjs,cjs,json,md,yml,yaml}",
|
|
13
|
+
".*",
|
|
14
|
+
];
|
|
15
|
+
const SYMBOL_PATTERNS = [
|
|
16
|
+
/\b(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\b/g,
|
|
17
|
+
/\b(?:export\s+)?class\s+([A-Za-z_$][\w$]*)\b/g,
|
|
18
|
+
/\b(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)\b/g,
|
|
19
|
+
/\b(?:export\s+)?type\s+([A-Za-z_$][\w$]*)\b/g,
|
|
20
|
+
/\b(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g,
|
|
21
|
+
/\b(?:export\s+)?enum\s+([A-Za-z_$][\w$]*)\b/g,
|
|
22
|
+
];
|
|
23
|
+
const tokenizeQuery = (value) => value
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9_./-]+/g, " ")
|
|
26
|
+
.split(/[\s/._-]+/)
|
|
27
|
+
.map((token) => token.trim())
|
|
28
|
+
.filter((token) => token.length >= 2);
|
|
29
|
+
const normalizeRelativeInputPath = (value) => value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\//, "");
|
|
30
|
+
const unique = (items) => Array.from(new Set(items));
|
|
31
|
+
const extractSymbols = (content) => {
|
|
32
|
+
const symbols = new Set();
|
|
33
|
+
for (const pattern of SYMBOL_PATTERNS) {
|
|
34
|
+
pattern.lastIndex = 0;
|
|
35
|
+
let match = pattern.exec(content);
|
|
36
|
+
while (match) {
|
|
37
|
+
const candidate = match[1]?.trim();
|
|
38
|
+
if (candidate) {
|
|
39
|
+
symbols.add(candidate);
|
|
40
|
+
}
|
|
41
|
+
match = pattern.exec(content);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return Array.from(symbols);
|
|
45
|
+
};
|
|
46
|
+
const buildRepoMap = async (workspaceRoot) => {
|
|
47
|
+
const discovered = await glob(SOURCE_PATTERNS, {
|
|
48
|
+
cwd: workspaceRoot,
|
|
49
|
+
absolute: true,
|
|
50
|
+
nodir: true,
|
|
51
|
+
dot: true,
|
|
52
|
+
follow: false,
|
|
53
|
+
ignore: [
|
|
54
|
+
"**/node_modules/**",
|
|
55
|
+
"**/dist/**",
|
|
56
|
+
"**/build/**",
|
|
57
|
+
"**/.git/**",
|
|
58
|
+
"**/coverage/**",
|
|
59
|
+
"**/target/**",
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
const selectedFiles = unique(discovered).slice(0, MAX_INDEX_FILES);
|
|
63
|
+
const entries = [];
|
|
64
|
+
for (const absolutePath of selectedFiles) {
|
|
65
|
+
try {
|
|
66
|
+
const stats = await fs.stat(absolutePath);
|
|
67
|
+
if (!stats.isFile() || stats.size > MAX_FILE_SIZE_BYTES) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const content = await fs.readFile(absolutePath, "utf8");
|
|
71
|
+
const relativePath = path.relative(workspaceRoot, absolutePath);
|
|
72
|
+
entries.push({
|
|
73
|
+
absolutePath,
|
|
74
|
+
relativePath,
|
|
75
|
+
relativePathLower: relativePath.replace(/\\/g, "/").toLowerCase(),
|
|
76
|
+
baseNameLower: path.basename(absolutePath).toLowerCase(),
|
|
77
|
+
symbols: extractSymbols(content),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Ignore unreadable files and continue indexing.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return entries;
|
|
85
|
+
};
|
|
86
|
+
const getRepoMapEntries = async (workspaceRoot) => {
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const cached = cache.get(workspaceRoot);
|
|
89
|
+
if (cached && now - cached.builtAt < REPO_MAP_TTL_MS) {
|
|
90
|
+
return cached.entries;
|
|
91
|
+
}
|
|
92
|
+
const entries = await buildRepoMap(workspaceRoot);
|
|
93
|
+
cache.set(workspaceRoot, {
|
|
94
|
+
builtAt: now,
|
|
95
|
+
entries,
|
|
96
|
+
});
|
|
97
|
+
return entries;
|
|
98
|
+
};
|
|
99
|
+
const scoreEntry = (entry, query, normalizedQuery, tokens) => {
|
|
100
|
+
let score = 0;
|
|
101
|
+
const queryLower = query.toLowerCase();
|
|
102
|
+
const rel = entry.relativePathLower;
|
|
103
|
+
if (normalizedQuery && rel === normalizedQuery.toLowerCase()) {
|
|
104
|
+
score += 1000;
|
|
105
|
+
}
|
|
106
|
+
if (normalizedQuery && rel.endsWith(`/${normalizedQuery.toLowerCase()}`)) {
|
|
107
|
+
score += 400;
|
|
108
|
+
}
|
|
109
|
+
const queryBase = path.basename(normalizedQuery || query).toLowerCase();
|
|
110
|
+
if (queryBase && entry.baseNameLower === queryBase) {
|
|
111
|
+
score += 180;
|
|
112
|
+
}
|
|
113
|
+
if (queryLower && entry.baseNameLower.includes(queryLower)) {
|
|
114
|
+
score += 80;
|
|
115
|
+
}
|
|
116
|
+
for (const token of tokens) {
|
|
117
|
+
if (entry.baseNameLower.includes(token)) {
|
|
118
|
+
score += 24;
|
|
119
|
+
}
|
|
120
|
+
if (rel.includes(token)) {
|
|
121
|
+
score += 10;
|
|
122
|
+
}
|
|
123
|
+
for (const symbol of entry.symbols) {
|
|
124
|
+
const symbolLower = symbol.toLowerCase();
|
|
125
|
+
if (symbolLower === token) {
|
|
126
|
+
score += 50;
|
|
127
|
+
}
|
|
128
|
+
else if (symbolLower.includes(token)) {
|
|
129
|
+
score += 20;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return score;
|
|
134
|
+
};
|
|
135
|
+
export const findFilesViaRepoMap = async (workspaceRoot, query, limit = 10) => {
|
|
136
|
+
const normalizedQuery = normalizeRelativeInputPath(query);
|
|
137
|
+
const tokens = tokenizeQuery(query);
|
|
138
|
+
if (!normalizedQuery && tokens.length === 0) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
const entries = await getRepoMapEntries(workspaceRoot);
|
|
142
|
+
const ranked = entries
|
|
143
|
+
.map((entry) => ({
|
|
144
|
+
entry,
|
|
145
|
+
score: scoreEntry(entry, query, normalizedQuery, tokens),
|
|
146
|
+
}))
|
|
147
|
+
.filter((candidate) => candidate.score > 0)
|
|
148
|
+
.sort((a, b) => b.score - a.score)
|
|
149
|
+
.slice(0, Math.max(1, limit));
|
|
150
|
+
return ranked.map((candidate) => candidate.entry.absolutePath);
|
|
151
|
+
};
|
|
152
|
+
export const rankFileCandidates = async (workspaceRoot, query, candidates) => {
|
|
153
|
+
if (candidates.length <= 1) {
|
|
154
|
+
return candidates;
|
|
155
|
+
}
|
|
156
|
+
const normalizedQuery = normalizeRelativeInputPath(query);
|
|
157
|
+
const tokens = tokenizeQuery(query);
|
|
158
|
+
const entries = await getRepoMapEntries(workspaceRoot);
|
|
159
|
+
const entryByPath = new Map(entries.map((entry) => [entry.absolutePath, entry]));
|
|
160
|
+
const scored = candidates.map((absolutePath) => {
|
|
161
|
+
const entry = entryByPath.get(absolutePath);
|
|
162
|
+
if (entry) {
|
|
163
|
+
return {
|
|
164
|
+
absolutePath,
|
|
165
|
+
score: scoreEntry(entry, query, normalizedQuery, tokens),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const relativePathLower = path
|
|
169
|
+
.relative(workspaceRoot, absolutePath)
|
|
170
|
+
.replace(/\\/g, "/")
|
|
171
|
+
.toLowerCase();
|
|
172
|
+
const baseNameLower = path.basename(absolutePath).toLowerCase();
|
|
173
|
+
let score = 0;
|
|
174
|
+
if (normalizedQuery && relativePathLower === normalizedQuery.toLowerCase()) {
|
|
175
|
+
score += 1000;
|
|
176
|
+
}
|
|
177
|
+
if (normalizedQuery && relativePathLower.endsWith(`/${normalizedQuery.toLowerCase()}`)) {
|
|
178
|
+
score += 400;
|
|
179
|
+
}
|
|
180
|
+
const queryBase = path.basename(normalizedQuery || query).toLowerCase();
|
|
181
|
+
if (queryBase && baseNameLower === queryBase) {
|
|
182
|
+
score += 180;
|
|
183
|
+
}
|
|
184
|
+
for (const token of tokens) {
|
|
185
|
+
if (baseNameLower.includes(token))
|
|
186
|
+
score += 24;
|
|
187
|
+
if (relativePathLower.includes(token))
|
|
188
|
+
score += 10;
|
|
189
|
+
}
|
|
190
|
+
return { absolutePath, score };
|
|
191
|
+
});
|
|
192
|
+
scored.sort((a, b) => b.score - a.score);
|
|
193
|
+
return scored.map((candidate) => candidate.absolutePath);
|
|
194
|
+
};
|
|
195
|
+
export const clearRepoMapCache = () => {
|
|
196
|
+
cache.clear();
|
|
197
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type SkillEntry = {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
location: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Merge skills discovered across multiple directories.
|
|
8
|
+
* Later directories have higher precedence and override earlier ones on name conflict.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadSkillsFromDirectories(skillDirs: string[]): Promise<SkillEntry[]>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const shouldLogSkillDiscoveryWarnings = process.env.IRIS_VERBOSE_SKILL_DISCOVERY === "true";
|
|
4
|
+
const logSkillDiscoveryWarning = (message) => {
|
|
5
|
+
if (!shouldLogSkillDiscoveryWarnings)
|
|
6
|
+
return;
|
|
7
|
+
console.warn(message);
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Merge skills discovered across multiple directories.
|
|
11
|
+
* Later directories have higher precedence and override earlier ones on name conflict.
|
|
12
|
+
*/
|
|
13
|
+
export async function loadSkillsFromDirectories(skillDirs) {
|
|
14
|
+
const skillsByName = new Map();
|
|
15
|
+
for (const skillsDir of skillDirs) {
|
|
16
|
+
try {
|
|
17
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (!entry.isDirectory())
|
|
20
|
+
continue;
|
|
21
|
+
const skillMdPath = path.join(skillsDir, entry.name, "SKILL.md");
|
|
22
|
+
try {
|
|
23
|
+
const content = await fs.readFile(skillMdPath, "utf8");
|
|
24
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
25
|
+
const descMatch = content.match(/^description:\s*(.+)$/m);
|
|
26
|
+
const name = nameMatch ? nameMatch[1].trim() : entry.name;
|
|
27
|
+
const description = descMatch
|
|
28
|
+
? descMatch[1].trim()
|
|
29
|
+
: "No description available";
|
|
30
|
+
const location = `skills/${entry.name}/SKILL.md`;
|
|
31
|
+
if (skillsByName.has(name)) {
|
|
32
|
+
logSkillDiscoveryWarning(`[agent] overriding duplicated skill '${name}' from higher-precedence directory: ${skillsDir}`);
|
|
33
|
+
}
|
|
34
|
+
skillsByName.set(name, {
|
|
35
|
+
name,
|
|
36
|
+
description,
|
|
37
|
+
location: location.replace(/\\/g, "/"),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// skip skill if SKILL.md is unreadable
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// candidate directory does not exist in this runtime mode; try next
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return Array.from(skillsByName.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ToolCallBudget = {
|
|
2
|
+
limit: number;
|
|
3
|
+
admitted: number;
|
|
4
|
+
consecutiveSignature?: string;
|
|
5
|
+
consecutiveCount?: number;
|
|
6
|
+
stopReason?: "limit" | "repeated_call";
|
|
7
|
+
};
|
|
8
|
+
export declare const enforceToolCallBudget: <TTool>(tool: TTool, toolName?: string) => TTool;
|
|
9
|
+
export declare const enforceToolCallBudgetForTools: <TTools extends Record<string, unknown>>(tools: TTools) => TTools;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const stableStringify = (value) => {
|
|
2
|
+
if (value === null || typeof value !== "object")
|
|
3
|
+
return JSON.stringify(value);
|
|
4
|
+
if (Array.isArray(value))
|
|
5
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
6
|
+
const record = value;
|
|
7
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
8
|
+
};
|
|
9
|
+
const MAX_CONSECUTIVE_IDENTICAL_CALLS = 3;
|
|
10
|
+
const admitToolCall = (toolName, input, context) => {
|
|
11
|
+
const budget = context?.requestContext?.get("toolCallBudget");
|
|
12
|
+
if (!budget) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (budget.admitted >= budget.limit) {
|
|
16
|
+
budget.stopReason = "limit";
|
|
17
|
+
throw new Error(`Tool call budget exhausted (${budget.limit})`);
|
|
18
|
+
}
|
|
19
|
+
budget.admitted += 1;
|
|
20
|
+
const signature = `${toolName}:${stableStringify(input)}`;
|
|
21
|
+
if (budget.consecutiveSignature === signature) {
|
|
22
|
+
budget.consecutiveCount = (budget.consecutiveCount || 1) + 1;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
budget.consecutiveSignature = signature;
|
|
26
|
+
budget.consecutiveCount = 1;
|
|
27
|
+
}
|
|
28
|
+
if ((budget.consecutiveCount || 0) >= MAX_CONSECUTIVE_IDENTICAL_CALLS) {
|
|
29
|
+
budget.stopReason = "repeated_call";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export const enforceToolCallBudget = (tool, toolName = "tool") => {
|
|
33
|
+
if (typeof tool !== "object" || tool === null) {
|
|
34
|
+
return tool;
|
|
35
|
+
}
|
|
36
|
+
const executableTool = tool;
|
|
37
|
+
if (typeof executableTool.execute !== "function") {
|
|
38
|
+
return tool;
|
|
39
|
+
}
|
|
40
|
+
const execute = executableTool.execute;
|
|
41
|
+
const budgetedExecute = async (input, context) => {
|
|
42
|
+
admitToolCall(toolName, input, context);
|
|
43
|
+
return execute.call(tool, input, context);
|
|
44
|
+
};
|
|
45
|
+
return new Proxy(tool, {
|
|
46
|
+
get(target, property) {
|
|
47
|
+
if (property === "execute") {
|
|
48
|
+
return budgetedExecute;
|
|
49
|
+
}
|
|
50
|
+
const value = Reflect.get(target, property, target);
|
|
51
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
export const enforceToolCallBudgetForTools = (tools) => Object.fromEntries(Object.entries(tools).map(([name, tool]) => [name, enforceToolCallBudget(tool, name)]));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type ToolArgs = Record<string, unknown>;
|
|
2
|
+
export type PendingToolCall = {
|
|
3
|
+
name: string;
|
|
4
|
+
args: ToolArgs;
|
|
5
|
+
toolCallId?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ExecutedToolResult = {
|
|
8
|
+
name: string;
|
|
9
|
+
args: ToolArgs;
|
|
10
|
+
result: unknown;
|
|
11
|
+
toolCallId?: string;
|
|
12
|
+
lifecycleStepIndex?: number;
|
|
13
|
+
};
|
|
14
|
+
export type ToolExecutionStatus = "pending" | "in_progress" | "completed" | "failed" | "pending_confirmation" | "unknown";
|
|
15
|
+
export declare const resolveToolExecutionStatus: (result: unknown) => ToolExecutionStatus;
|
|
16
|
+
export declare const getToolCallSignature: (name: string, args: ToolArgs) => string;
|
|
17
|
+
export declare const countUniqueToolCalls: (pendingToolCalls: Array<{
|
|
18
|
+
name: string;
|
|
19
|
+
args: ToolArgs;
|
|
20
|
+
toolCallId?: string;
|
|
21
|
+
}>, executedToolResults: Array<{
|
|
22
|
+
name: string;
|
|
23
|
+
args: ToolArgs;
|
|
24
|
+
toolCallId?: string;
|
|
25
|
+
}>) => number;
|
|
26
|
+
export declare const normalizeToolLifecycle: (pendingToolCalls: PendingToolCall[], executedToolResults: ExecutedToolResult[]) => {
|
|
27
|
+
pendingToolCalls: PendingToolCall[];
|
|
28
|
+
executedToolResults: ExecutedToolResult[];
|
|
29
|
+
};
|
|
30
|
+
export {};
|