cowork-os 0.3.21 → 0.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +372 -10
- package/connectors/README.md +20 -0
- package/connectors/asana-mcp/README.md +24 -0
- package/connectors/asana-mcp/dist/index.js +427 -0
- package/connectors/asana-mcp/package.json +15 -0
- package/connectors/asana-mcp/src/index.ts +553 -0
- package/connectors/asana-mcp/tsconfig.json +13 -0
- package/connectors/hubspot-mcp/README.md +35 -0
- package/connectors/hubspot-mcp/dist/index.js +454 -0
- package/connectors/hubspot-mcp/package.json +15 -0
- package/connectors/hubspot-mcp/src/index.ts +562 -0
- package/connectors/hubspot-mcp/tsconfig.json +13 -0
- package/connectors/jira-mcp/README.md +49 -0
- package/connectors/jira-mcp/dist/index.js +588 -0
- package/connectors/jira-mcp/package.json +15 -0
- package/connectors/jira-mcp/src/index.ts +711 -0
- package/connectors/jira-mcp/tsconfig.json +13 -0
- package/connectors/linear-mcp/README.md +22 -0
- package/connectors/linear-mcp/dist/index.js +402 -0
- package/connectors/linear-mcp/package.json +15 -0
- package/connectors/linear-mcp/src/index.ts +522 -0
- package/connectors/linear-mcp/tsconfig.json +13 -0
- package/connectors/okta-mcp/README.md +24 -0
- package/connectors/okta-mcp/dist/index.js +411 -0
- package/connectors/okta-mcp/package.json +15 -0
- package/connectors/okta-mcp/src/index.ts +520 -0
- package/connectors/okta-mcp/tsconfig.json +13 -0
- package/connectors/salesforce-mcp/README.md +47 -0
- package/connectors/salesforce-mcp/dist/index.js +584 -0
- package/connectors/salesforce-mcp/package.json +15 -0
- package/connectors/salesforce-mcp/src/index.ts +722 -0
- package/connectors/salesforce-mcp/tsconfig.json +13 -0
- package/connectors/servicenow-mcp/README.md +26 -0
- package/connectors/servicenow-mcp/dist/index.js +400 -0
- package/connectors/servicenow-mcp/package.json +15 -0
- package/connectors/servicenow-mcp/src/index.ts +500 -0
- package/connectors/servicenow-mcp/tsconfig.json +13 -0
- package/connectors/templates/mcp-connector/README.md +31 -0
- package/connectors/templates/mcp-connector/package.json +15 -0
- package/connectors/templates/mcp-connector/src/index.ts +330 -0
- package/connectors/templates/mcp-connector/tsconfig.json +13 -0
- package/connectors/zendesk-mcp/README.md +40 -0
- package/connectors/zendesk-mcp/dist/index.js +431 -0
- package/connectors/zendesk-mcp/package.json +15 -0
- package/connectors/zendesk-mcp/src/index.ts +543 -0
- package/connectors/zendesk-mcp/tsconfig.json +13 -0
- package/dist/electron/electron/agent/custom-skill-loader.js +31 -1
- package/dist/electron/electron/agent/daemon.js +189 -13
- package/dist/electron/electron/agent/executor.js +895 -78
- package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
- package/dist/electron/electron/agent/llm/azure-openai-provider.js +328 -0
- package/dist/electron/electron/agent/llm/bedrock-provider.js +49 -9
- package/dist/electron/electron/agent/llm/github-copilot-provider.js +97 -0
- package/dist/electron/electron/agent/llm/groq-provider.js +33 -0
- package/dist/electron/electron/agent/llm/index.js +13 -1
- package/dist/electron/electron/agent/llm/kimi-provider.js +33 -0
- package/dist/electron/electron/agent/llm/openai-compatible-provider.js +116 -0
- package/dist/electron/electron/agent/llm/openai-compatible.js +111 -0
- package/dist/electron/electron/agent/llm/openai-oauth.js +2 -1
- package/dist/electron/electron/agent/llm/openrouter-provider.js +1 -1
- package/dist/electron/electron/agent/llm/provider-factory.js +350 -4
- package/dist/electron/electron/agent/llm/types.js +66 -1
- package/dist/electron/electron/agent/llm/xai-provider.js +33 -0
- package/dist/electron/electron/agent/search/provider-factory.js +38 -2
- package/dist/electron/electron/agent/tools/box-tools.js +231 -0
- package/dist/electron/electron/agent/tools/builtin-settings.js +28 -0
- package/dist/electron/electron/agent/tools/dropbox-tools.js +237 -0
- package/dist/electron/electron/agent/tools/file-tools.js +66 -3
- package/dist/electron/electron/agent/tools/google-drive-tools.js +227 -0
- package/dist/electron/electron/agent/tools/grep-tools.js +90 -10
- package/dist/electron/electron/agent/tools/image-tools.js +11 -1
- package/dist/electron/electron/agent/tools/notion-tools.js +312 -0
- package/dist/electron/electron/agent/tools/onedrive-tools.js +217 -0
- package/dist/electron/electron/agent/tools/registry.js +548 -10
- package/dist/electron/electron/agent/tools/search-tools.js +28 -10
- package/dist/electron/electron/agent/tools/sharepoint-tools.js +243 -0
- package/dist/electron/electron/agent/tools/shell-tools.js +12 -3
- package/dist/electron/electron/agent/tools/x-tools.js +1 -1
- package/dist/electron/electron/agents/agent-dispatch.js +63 -0
- package/dist/electron/electron/database/repositories.js +19 -5
- package/dist/electron/electron/database/schema.js +8 -0
- package/dist/electron/electron/gateway/channels/whatsapp.js +55 -0
- package/dist/electron/electron/gateway/index.js +75 -1
- package/dist/electron/electron/gateway/router.js +209 -154
- package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
- package/dist/electron/electron/ipc/handlers.js +763 -267
- package/dist/electron/electron/main.js +63 -0
- package/dist/electron/electron/mcp/oauth/connector-oauth.js +333 -0
- package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +503 -154
- package/dist/electron/electron/memory/MemoryService.js +2 -1
- package/dist/electron/electron/preload.js +78 -1
- package/dist/electron/electron/settings/appearance-manager.js +18 -1
- package/dist/electron/electron/settings/box-manager.js +54 -0
- package/dist/electron/electron/settings/dropbox-manager.js +54 -0
- package/dist/electron/electron/settings/google-drive-manager.js +54 -0
- package/dist/electron/electron/settings/notion-manager.js +56 -0
- package/dist/electron/electron/settings/onedrive-manager.js +54 -0
- package/dist/electron/electron/settings/sharepoint-manager.js +54 -0
- package/dist/electron/electron/utils/box-api.js +153 -0
- package/dist/electron/electron/utils/dropbox-api.js +144 -0
- package/dist/electron/electron/utils/env-migration.js +19 -0
- package/dist/electron/electron/utils/google-drive-api.js +152 -0
- package/dist/electron/electron/utils/notion-api.js +103 -0
- package/dist/electron/electron/utils/onedrive-api.js +113 -0
- package/dist/electron/electron/utils/sharepoint-api.js +109 -0
- package/dist/electron/electron/utils/validation.js +98 -3
- package/dist/electron/electron/utils/x-cli.js +1 -1
- package/dist/electron/shared/channelMessages.js +284 -3
- package/dist/electron/shared/llm-provider-catalog.js +198 -0
- package/dist/electron/shared/types.js +90 -1
- package/package.json +14 -3
- package/resources/skills/nano-banana-pro.json +4 -4
- package/resources/skills/openai-image-gen.json +3 -3
- package/resources/skills/scripts/gen.py +163 -0
- package/resources/skills/scripts/generate_image.py +91 -0
- package/src/electron/agent/custom-skill-loader.ts +34 -1
- package/src/electron/agent/daemon.ts +210 -14
- package/src/electron/agent/executor.ts +1124 -85
- package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
- package/src/electron/agent/llm/azure-openai-provider.ts +388 -0
- package/src/electron/agent/llm/bedrock-provider.ts +62 -9
- package/src/electron/agent/llm/github-copilot-provider.ts +117 -0
- package/src/electron/agent/llm/groq-provider.ts +39 -0
- package/src/electron/agent/llm/index.ts +6 -0
- package/src/electron/agent/llm/kimi-provider.ts +39 -0
- package/src/electron/agent/llm/openai-compatible-provider.ts +153 -0
- package/src/electron/agent/llm/openai-compatible.ts +133 -0
- package/src/electron/agent/llm/openai-oauth.ts +2 -1
- package/src/electron/agent/llm/openrouter-provider.ts +2 -1
- package/src/electron/agent/llm/provider-factory.ts +459 -6
- package/src/electron/agent/llm/types.ts +95 -1
- package/src/electron/agent/llm/xai-provider.ts +39 -0
- package/src/electron/agent/search/provider-factory.ts +43 -2
- package/src/electron/agent/tools/box-tools.ts +239 -0
- package/src/electron/agent/tools/builtin-settings.ts +36 -0
- package/src/electron/agent/tools/dropbox-tools.ts +237 -0
- package/src/electron/agent/tools/file-tools.ts +66 -3
- package/src/electron/agent/tools/gmail-tools.ts +240 -0
- package/src/electron/agent/tools/google-calendar-tools.ts +258 -0
- package/src/electron/agent/tools/google-drive-tools.ts +228 -0
- package/src/electron/agent/tools/grep-tools.ts +97 -12
- package/src/electron/agent/tools/image-tools.ts +11 -1
- package/src/electron/agent/tools/notion-tools.ts +330 -0
- package/src/electron/agent/tools/onedrive-tools.ts +217 -0
- package/src/electron/agent/tools/registry.ts +794 -10
- package/src/electron/agent/tools/search-tools.ts +29 -11
- package/src/electron/agent/tools/sharepoint-tools.ts +247 -0
- package/src/electron/agent/tools/shell-tools.ts +11 -3
- package/src/electron/agent/tools/x-tools.ts +1 -1
- package/src/electron/agents/agent-dispatch.ts +79 -0
- package/src/electron/database/SecureSettingsRepository.ts +7 -1
- package/src/electron/database/repositories.ts +58 -6
- package/src/electron/database/schema.ts +8 -0
- package/src/electron/gateway/channels/discord.ts +4 -0
- package/src/electron/gateway/channels/google-chat.ts +3 -0
- package/src/electron/gateway/channels/line.ts +3 -0
- package/src/electron/gateway/channels/matrix-client.ts +15 -0
- package/src/electron/gateway/channels/matrix.ts +31 -0
- package/src/electron/gateway/channels/mattermost.ts +3 -0
- package/src/electron/gateway/channels/signal.ts +3 -0
- package/src/electron/gateway/channels/slack.ts +9 -4
- package/src/electron/gateway/channels/teams.ts +4 -0
- package/src/electron/gateway/channels/telegram.ts +2 -0
- package/src/electron/gateway/channels/twitch.ts +2 -0
- package/src/electron/gateway/channels/types.ts +8 -0
- package/src/electron/gateway/channels/whatsapp.ts +66 -0
- package/src/electron/gateway/index.ts +95 -2
- package/src/electron/gateway/router.ts +231 -161
- package/src/electron/gateway/security.ts +21 -9
- package/src/electron/ipc/canvas-handlers.ts +10 -0
- package/src/electron/ipc/handlers.ts +848 -292
- package/src/electron/main.ts +35 -0
- package/src/electron/mcp/oauth/connector-oauth.ts +448 -0
- package/src/electron/mcp/registry/MCPRegistryManager.ts +343 -12
- package/src/electron/memory/MemoryService.ts +7 -1
- package/src/electron/preload.ts +200 -5
- package/src/electron/settings/appearance-manager.ts +20 -2
- package/src/electron/settings/box-manager.ts +58 -0
- package/src/electron/settings/dropbox-manager.ts +58 -0
- package/src/electron/settings/google-workspace-manager.ts +59 -0
- package/src/electron/settings/notion-manager.ts +60 -0
- package/src/electron/settings/onedrive-manager.ts +58 -0
- package/src/electron/settings/sharepoint-manager.ts +58 -0
- package/src/electron/utils/box-api.ts +184 -0
- package/src/electron/utils/dropbox-api.ts +171 -0
- package/src/electron/utils/env-migration.ts +22 -0
- package/src/electron/utils/gmail-api.ts +121 -0
- package/src/electron/utils/google-calendar-api.ts +115 -0
- package/src/electron/utils/google-workspace-api.ts +228 -0
- package/src/electron/utils/google-workspace-auth.ts +109 -0
- package/src/electron/utils/google-workspace-oauth.ts +232 -0
- package/src/electron/utils/notion-api.ts +126 -0
- package/src/electron/utils/onedrive-api.ts +137 -0
- package/src/electron/utils/sharepoint-api.ts +132 -0
- package/src/electron/utils/validation.ts +128 -1
- package/src/electron/utils/x-cli.ts +1 -1
- package/src/renderer/App.tsx +119 -8
- package/src/renderer/components/ActivityFeedItem.tsx +34 -17
- package/src/renderer/components/AgentWorkingStatePanel.tsx +7 -5
- package/src/renderer/components/AppearanceSettings.tsx +37 -2
- package/src/renderer/components/BlueBubblesSettings.tsx +18 -7
- package/src/renderer/components/BoxSettings.tsx +203 -0
- package/src/renderer/components/BrowserView.tsx +101 -0
- package/src/renderer/components/BuiltinToolsSettings.tsx +105 -0
- package/src/renderer/components/CanvasPreview.tsx +68 -1
- package/src/renderer/components/ConnectorEnvModal.tsx +116 -0
- package/src/renderer/components/ConnectorSetupModal.tsx +566 -0
- package/src/renderer/components/ConnectorsSettings.tsx +397 -0
- package/src/renderer/components/ControlPlaneSettings.tsx +2 -0
- package/src/renderer/components/DiscordSettings.tsx +18 -7
- package/src/renderer/components/DropboxSettings.tsx +202 -0
- package/src/renderer/components/EmailSettings.tsx +18 -7
- package/src/renderer/components/FileViewer.tsx +21 -13
- package/src/renderer/components/GoogleChatSettings.tsx +17 -7
- package/src/renderer/components/GoogleWorkspaceSettings.tsx +332 -0
- package/src/renderer/components/ImessageSettings.tsx +22 -11
- package/src/renderer/components/LineIcons.tsx +376 -0
- package/src/renderer/components/LineSettings.tsx +18 -7
- package/src/renderer/components/MCPSettings.tsx +56 -0
- package/src/renderer/components/MainContent.tsx +740 -76
- package/src/renderer/components/MatrixSettings.tsx +18 -7
- package/src/renderer/components/MattermostSettings.tsx +18 -7
- package/src/renderer/components/NodesSettings.tsx +58 -99
- package/src/renderer/components/NotificationPanel.tsx +25 -11
- package/src/renderer/components/NotionSettings.tsx +231 -0
- package/src/renderer/components/Onboarding/Onboarding.tsx +13 -1
- package/src/renderer/components/OnboardingModal.tsx +70 -1
- package/src/renderer/components/OneDriveSettings.tsx +212 -0
- package/src/renderer/components/RightPanel.tsx +141 -28
- package/src/renderer/components/ScheduledTasksSettings.tsx +10 -62
- package/src/renderer/components/SearchSettings.tsx +118 -114
- package/src/renderer/components/Settings.tsx +1425 -651
- package/src/renderer/components/SharePointSettings.tsx +224 -0
- package/src/renderer/components/Sidebar.tsx +94 -19
- package/src/renderer/components/SignalSettings.tsx +18 -7
- package/src/renderer/components/SkillHubBrowser.tsx +144 -185
- package/src/renderer/components/SlackSettings.tsx +18 -7
- package/src/renderer/components/TaskQuickActions.tsx +11 -6
- package/src/renderer/components/TaskTimeline.tsx +58 -26
- package/src/renderer/components/TeamsSettings.tsx +18 -7
- package/src/renderer/components/TelegramSettings.tsx +18 -7
- package/src/renderer/components/ThemeIcon.tsx +16 -0
- package/src/renderer/components/TwitchSettings.tsx +18 -7
- package/src/renderer/components/VoiceSettings.tsx +30 -74
- package/src/renderer/components/WhatsAppSettings.tsx +48 -37
- package/src/renderer/components/WorkingStateHistory.tsx +7 -5
- package/src/renderer/components/WorkspaceSelector.tsx +42 -13
- package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
- package/src/renderer/styles/index.css +2333 -209
- package/src/shared/channelMessages.ts +367 -4
- package/src/shared/llm-provider-catalog.ts +217 -0
- package/src/shared/types.ts +251 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Task, Workspace, Plan, PlanStep, TaskEvent, SuccessCriteria } from '../../shared/types';
|
|
1
|
+
import { Task, Workspace, Plan, PlanStep, TaskEvent, SuccessCriteria, TEMP_WORKSPACE_ID } from '../../shared/types';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { AgentDaemon } from './daemon';
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
LLMProviderFactory,
|
|
10
10
|
LLMMessage,
|
|
11
11
|
LLMToolResult,
|
|
12
|
+
LLMToolUse,
|
|
12
13
|
} from './llm';
|
|
13
14
|
import {
|
|
14
15
|
ContextManager,
|
|
@@ -21,6 +22,7 @@ import { calculateCost, formatCost } from './llm/pricing';
|
|
|
21
22
|
import { getCustomSkillLoader } from './custom-skill-loader';
|
|
22
23
|
import { MemoryService } from '../memory/MemoryService';
|
|
23
24
|
import { InputSanitizer, OutputFilter } from './security';
|
|
25
|
+
import { BuiltinToolsSettingsManager } from './tools/builtin-settings';
|
|
24
26
|
|
|
25
27
|
class AwaitingUserInputError extends Error {
|
|
26
28
|
constructor(message: string) {
|
|
@@ -35,7 +37,7 @@ const LLM_TIMEOUT_MS = 2 * 60 * 1000;
|
|
|
35
37
|
// Per-step timeout (5 minutes max per step)
|
|
36
38
|
const STEP_TIMEOUT_MS = 5 * 60 * 1000;
|
|
37
39
|
|
|
38
|
-
//
|
|
40
|
+
// Default per-tool execution timeout (overrideable per tool)
|
|
39
41
|
const TOOL_TIMEOUT_MS = 30 * 1000;
|
|
40
42
|
|
|
41
43
|
// Maximum consecutive failures for the same tool before giving up
|
|
@@ -88,6 +90,25 @@ const INPUT_DEPENDENT_ERROR_PATTERNS = [
|
|
|
88
90
|
/user denied/i, // User denied an approval request
|
|
89
91
|
];
|
|
90
92
|
|
|
93
|
+
// Keywords that imply a step wants image verification.
|
|
94
|
+
const IMAGE_VERIFICATION_KEYWORDS = [
|
|
95
|
+
'image',
|
|
96
|
+
'photo',
|
|
97
|
+
'photograph',
|
|
98
|
+
'picture',
|
|
99
|
+
'render',
|
|
100
|
+
'illustration',
|
|
101
|
+
'png',
|
|
102
|
+
'jpg',
|
|
103
|
+
'jpeg',
|
|
104
|
+
'webp',
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const IMAGE_FILE_EXTENSION_REGEX = /\.(png|jpe?g|webp|gif|bmp)$/i;
|
|
108
|
+
|
|
109
|
+
// Allow a small buffer for file timestamp granularity/clock skew.
|
|
110
|
+
const IMAGE_VERIFICATION_TIME_SKEW_MS = 1000;
|
|
111
|
+
|
|
91
112
|
/**
|
|
92
113
|
* Check if an error is non-retryable (quota/rate limit related)
|
|
93
114
|
* These errors indicate a systemic problem with the tool/API
|
|
@@ -149,30 +170,6 @@ function isAskingQuestion(text: string): boolean {
|
|
|
149
170
|
const trimmed = text.trim();
|
|
150
171
|
if (!trimmed) return false;
|
|
151
172
|
|
|
152
|
-
// Keep this lightweight and conservative: only pause on questions that
|
|
153
|
-
// clearly request input/decisions needed to proceed.
|
|
154
|
-
const blockingQuestionPatterns = [
|
|
155
|
-
// Direct requests for info or confirmation
|
|
156
|
-
/(?:^|\n)\s*(?:please\s+)?(?:provide|share|send|upload|enter|paste|specify|clarify|confirm|choose|pick|select)\b/i,
|
|
157
|
-
/(?:can|could|would)\s+you\s+(?:please\s+)?(?:provide|share|send|upload|enter|paste|specify|clarify|confirm|choose|pick|select)\b/i,
|
|
158
|
-
|
|
159
|
-
// Decision/approval questions
|
|
160
|
-
/would\s+you\s+like\s+me\s+to\b/i,
|
|
161
|
-
/would\s+you\s+prefer\b/i,
|
|
162
|
-
/should\s+i\b/i,
|
|
163
|
-
/do\s+you\s+want\s+me\s+to\b/i,
|
|
164
|
-
/do\s+you\s+prefer\b/i,
|
|
165
|
-
/is\s+it\s+(?:ok|okay|alright)\s+if\s+i\b/i,
|
|
166
|
-
|
|
167
|
-
// Clarifying questions about specifics
|
|
168
|
-
/\bwhat\s+(?:is|are|was|were|should|would|can|could|do|does|did)\s+(?:the|your|this|that)\b/i,
|
|
169
|
-
/\bwhat\s+should\s+i\b/i,
|
|
170
|
-
/\bwhich\s+(?:one|option|approach|method|file|version|environment|format|branch|repo|path)\b/i,
|
|
171
|
-
/\bwhere\s+(?:is|are|should|can|could)\b/i,
|
|
172
|
-
/\bwhen\s+(?:is|are|should|can|could)\b/i,
|
|
173
|
-
/\bhow\s+should\s+i\b/i,
|
|
174
|
-
];
|
|
175
|
-
|
|
176
173
|
const nonBlockingQuestionPatterns = [
|
|
177
174
|
// Conversational/offboarding prompts that shouldn't pause execution
|
|
178
175
|
/\bwhat\s+(?:else\s+)?can\s+i\s+help\b/i,
|
|
@@ -185,23 +182,73 @@ function isAskingQuestion(text: string): boolean {
|
|
|
185
182
|
/\bdoes\s+that\s+(?:help|make\s+sense)\b/i,
|
|
186
183
|
];
|
|
187
184
|
|
|
188
|
-
const
|
|
189
|
-
|
|
185
|
+
const maxLengthForAnalysis = 4000;
|
|
186
|
+
const sample = trimmed.slice(0, maxLengthForAnalysis);
|
|
190
187
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
188
|
+
const blockingCuePatterns = [
|
|
189
|
+
/(?:need|required)\s+(?:your|a|the)\b/i,
|
|
190
|
+
/before\s+i\s+can\s+(?:proceed|continue)\b/i,
|
|
191
|
+
/to\s+(?:proceed|continue|move\s+forward)\b/i,
|
|
192
|
+
/i\s+can(?:not|'t)\s+(?:proceed|continue)\b/i,
|
|
193
|
+
/\bawaiting\s+your\b/i,
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const explicitProceedPatterns = [
|
|
197
|
+
/\bi\s+(?:will|\'ll)\s+(?:proceed|continue|go\s+ahead|move\s+forward)\b/i,
|
|
198
|
+
/\bi\s+can\s+(?:proceed|continue|move\s+forward)\b/i,
|
|
199
|
+
/\bi\s+(?:will|\'ll)\s+assume\b/i,
|
|
200
|
+
/\bif\s+you\s+do\s+not\s+(?:respond|answer|reply)\b/i,
|
|
201
|
+
/\bif\s+you\s+don\'t\s+(?:respond|answer|reply)\b/i,
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const questionWordPatterns = [
|
|
205
|
+
/^(?:who|what|where|when|why|how|which)\b/i,
|
|
206
|
+
];
|
|
195
207
|
|
|
196
|
-
|
|
197
|
-
|
|
208
|
+
const imperativePatterns = [
|
|
209
|
+
/^(?:please\s+)?(?:provide|share|send|upload|enter|paste|specify|clarify|confirm|choose|pick|select|list|tell|give)\b/i,
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const decisionPatterns = [
|
|
213
|
+
/^(?:do\s+you\s+want|do\s+you\s+prefer|would\s+you\s+like|would\s+you\s+prefer|should\s+i|is\s+it\s+(?:ok|okay|alright)\s+if\s+i)\b/i,
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
const hasBlockingCue = blockingCuePatterns.some(pattern => pattern.test(sample));
|
|
217
|
+
const hasExplicitProceed = explicitProceedPatterns.some(pattern => pattern.test(sample));
|
|
218
|
+
if (hasBlockingCue) return true;
|
|
219
|
+
const lines = sample.split('\n').map(l => l.trim()).filter(Boolean);
|
|
220
|
+
if (lines.length === 0) return false;
|
|
221
|
+
|
|
222
|
+
const lastLine = lines[lines.length - 1] ?? sample;
|
|
198
223
|
const sentenceMatch = lastLine.match(/[^.!?]+[.!?]*$/);
|
|
199
224
|
const lastSentence = sentenceMatch ? sentenceMatch[0].trim() : lastLine;
|
|
200
|
-
|
|
201
|
-
|
|
225
|
+
const hasNonBlockingTail = nonBlockingQuestionPatterns.some(pattern => pattern.test(lastSentence));
|
|
226
|
+
|
|
227
|
+
const tailLines = lines.slice(-2);
|
|
228
|
+
let tailQuestion = false;
|
|
229
|
+
let tailImperative = false;
|
|
230
|
+
|
|
231
|
+
for (const line of tailLines) {
|
|
232
|
+
const normalized = line.replace(/^[\-\*]?\s*\d*[\).]?\s*/, '').trim();
|
|
233
|
+
if (!normalized) continue;
|
|
234
|
+
if (nonBlockingQuestionPatterns.some(pattern => pattern.test(normalized))) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (imperativePatterns.some(pattern => pattern.test(normalized)) || decisionPatterns.some(pattern => pattern.test(normalized))) {
|
|
238
|
+
tailImperative = true;
|
|
239
|
+
}
|
|
240
|
+
if (normalized.endsWith('?') || questionWordPatterns.some(pattern => pattern.test(normalized))) {
|
|
241
|
+
tailQuestion = true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (tailImperative) return true;
|
|
246
|
+
if (tailQuestion) {
|
|
247
|
+
if (hasNonBlockingTail) return false;
|
|
248
|
+
if (hasExplicitProceed) return false;
|
|
249
|
+
return true;
|
|
202
250
|
}
|
|
203
251
|
|
|
204
|
-
// Default to not pausing on generic questions.
|
|
205
252
|
return false;
|
|
206
253
|
}
|
|
207
254
|
|
|
@@ -1014,6 +1061,14 @@ export class TaskExecutor {
|
|
|
1014
1061
|
private modelKey: string;
|
|
1015
1062
|
private conversationHistory: LLMMessage[] = [];
|
|
1016
1063
|
private systemPrompt: string = '';
|
|
1064
|
+
private lastUserMessage: string;
|
|
1065
|
+
private toolResultMemory: Array<{ tool: string; summary: string; timestamp: number }> = [];
|
|
1066
|
+
private lastAssistantOutput: string | null = null;
|
|
1067
|
+
private lastNonVerificationOutput: string | null = null;
|
|
1068
|
+
private readonly toolResultMemoryLimit = 8;
|
|
1069
|
+
private readonly shouldPauseForQuestions: boolean;
|
|
1070
|
+
private dispatchedMentionedAgents = false;
|
|
1071
|
+
private lastAssistantText: string | null = null;
|
|
1017
1072
|
|
|
1018
1073
|
// Plan revision tracking to prevent infinite revision loops
|
|
1019
1074
|
private planRevisionCount: number = 0;
|
|
@@ -1040,7 +1095,10 @@ export class TaskExecutor {
|
|
|
1040
1095
|
private workspace: Workspace,
|
|
1041
1096
|
private daemon: AgentDaemon
|
|
1042
1097
|
) {
|
|
1098
|
+
this.lastUserMessage = task.prompt;
|
|
1043
1099
|
this.requiresTestRun = this.detectTestRequirement(`${task.title}\n${task.prompt}`);
|
|
1100
|
+
// Only main tasks should pause for user input. Sub/parallel tasks should complete and report back.
|
|
1101
|
+
this.shouldPauseForQuestions = !task.parentTaskId && (task.agentType ?? 'main') === 'main';
|
|
1044
1102
|
// Get base settings
|
|
1045
1103
|
const settings = LLMProviderFactory.loadSettings();
|
|
1046
1104
|
|
|
@@ -1056,13 +1114,19 @@ export class TaskExecutor {
|
|
|
1056
1114
|
const effectiveModelKey = taskModelKey || settings.modelKey;
|
|
1057
1115
|
|
|
1058
1116
|
// Get the model ID
|
|
1117
|
+
const azureDeployment = settings.azure?.deployment || settings.azure?.deployments?.[0];
|
|
1059
1118
|
this.modelId = LLMProviderFactory.getModelId(
|
|
1060
1119
|
effectiveModelKey,
|
|
1061
1120
|
settings.providerType,
|
|
1062
1121
|
settings.ollama?.model,
|
|
1063
1122
|
settings.gemini?.model,
|
|
1064
1123
|
settings.openrouter?.model,
|
|
1065
|
-
settings.openai?.model
|
|
1124
|
+
settings.openai?.model,
|
|
1125
|
+
azureDeployment,
|
|
1126
|
+
settings.groq?.model,
|
|
1127
|
+
settings.xai?.model,
|
|
1128
|
+
settings.kimi?.model,
|
|
1129
|
+
settings.customProviders
|
|
1066
1130
|
);
|
|
1067
1131
|
this.modelKey = effectiveModelKey;
|
|
1068
1132
|
|
|
@@ -1151,6 +1215,9 @@ export class TaskExecutor {
|
|
|
1151
1215
|
error.message?.includes('rate limit') ||
|
|
1152
1216
|
error.message?.includes('ECONNRESET') ||
|
|
1153
1217
|
error.message?.includes('ETIMEDOUT') ||
|
|
1218
|
+
error.message?.includes('ENOTFOUND') ||
|
|
1219
|
+
error.message?.includes('EAI_AGAIN') ||
|
|
1220
|
+
error.message?.includes('ECONNREFUSED') ||
|
|
1154
1221
|
error.message?.includes('network') ||
|
|
1155
1222
|
error.status === 429 ||
|
|
1156
1223
|
error.status === 503 ||
|
|
@@ -1220,6 +1287,23 @@ export class TaskExecutor {
|
|
|
1220
1287
|
this.globalTurnCount++; // Track global turns across all steps
|
|
1221
1288
|
}
|
|
1222
1289
|
|
|
1290
|
+
private getToolTimeoutMs(toolName: string, input: unknown): number {
|
|
1291
|
+
const settingsTimeout = BuiltinToolsSettingsManager.getToolTimeoutMs(toolName);
|
|
1292
|
+
const normalizedSettingsTimeout = settingsTimeout && settingsTimeout > 0 ? settingsTimeout : null;
|
|
1293
|
+
|
|
1294
|
+
if (toolName === 'run_command') {
|
|
1295
|
+
const inputTimeout = typeof (input as { timeout?: unknown })?.timeout === 'number'
|
|
1296
|
+
? (input as { timeout?: number }).timeout
|
|
1297
|
+
: undefined;
|
|
1298
|
+
if (typeof inputTimeout === 'number' && Number.isFinite(inputTimeout) && inputTimeout > 0) {
|
|
1299
|
+
return Math.round(inputTimeout);
|
|
1300
|
+
}
|
|
1301
|
+
return normalizedSettingsTimeout ?? TOOL_TIMEOUT_MS;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
return normalizedSettingsTimeout ?? TOOL_TIMEOUT_MS;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1223
1307
|
/**
|
|
1224
1308
|
* Check if a file operation should be blocked (redundant read or duplicate creation)
|
|
1225
1309
|
* @returns Object with blocked flag, reason, and suggestion if blocked, plus optional cached result
|
|
@@ -1372,11 +1456,108 @@ export class TaskExecutor {
|
|
|
1372
1456
|
}
|
|
1373
1457
|
}
|
|
1374
1458
|
|
|
1459
|
+
private stepRequiresImageVerification(step: PlanStep): boolean {
|
|
1460
|
+
const description = (step.description || '').toLowerCase();
|
|
1461
|
+
if (!description.includes('verify')) return false;
|
|
1462
|
+
return IMAGE_VERIFICATION_KEYWORDS.some((keyword) => description.includes(keyword));
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
private hasNewImageFromGlobResult(result: any, since: number): boolean {
|
|
1466
|
+
const matches = result?.matches;
|
|
1467
|
+
if (!Array.isArray(matches)) return false;
|
|
1468
|
+
|
|
1469
|
+
const threshold = Math.max(0, since - IMAGE_VERIFICATION_TIME_SKEW_MS);
|
|
1470
|
+
|
|
1471
|
+
for (const match of matches) {
|
|
1472
|
+
const path = typeof match === 'string' ? match : match?.path;
|
|
1473
|
+
if (!path || !IMAGE_FILE_EXTENSION_REGEX.test(path)) continue;
|
|
1474
|
+
|
|
1475
|
+
const modified = typeof match === 'object' ? match?.modified : undefined;
|
|
1476
|
+
if (!modified) continue;
|
|
1477
|
+
|
|
1478
|
+
const modifiedTime = Date.parse(modified);
|
|
1479
|
+
if (!Number.isNaN(modifiedTime) && modifiedTime >= threshold) {
|
|
1480
|
+
return true;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1375
1487
|
/**
|
|
1376
1488
|
* Infer missing parameters for tool calls (helps weaker models)
|
|
1377
1489
|
* This auto-fills parameters when the LLM fails to provide them but context is available
|
|
1378
1490
|
*/
|
|
1379
1491
|
private inferMissingParameters(toolName: string, input: any): { input: any; modified: boolean; inference?: string } {
|
|
1492
|
+
if (toolName === 'create_document') {
|
|
1493
|
+
let modified = false;
|
|
1494
|
+
let inference = '';
|
|
1495
|
+
input = input || {};
|
|
1496
|
+
|
|
1497
|
+
if (!input.filename) {
|
|
1498
|
+
if (input.path) {
|
|
1499
|
+
input.filename = path.basename(String(input.path));
|
|
1500
|
+
modified = true;
|
|
1501
|
+
inference = 'Normalized path -> filename';
|
|
1502
|
+
} else if (input.name) {
|
|
1503
|
+
input.filename = String(input.name);
|
|
1504
|
+
modified = true;
|
|
1505
|
+
inference = 'Normalized name -> filename';
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
if (!input.format) {
|
|
1510
|
+
const ext = input.filename ? path.extname(String(input.filename)).toLowerCase() : '';
|
|
1511
|
+
if (ext === '.pdf') {
|
|
1512
|
+
input.format = 'pdf';
|
|
1513
|
+
modified = true;
|
|
1514
|
+
inference = `${inference ? `${inference}; ` : ''}Inferred format="pdf" from filename`;
|
|
1515
|
+
} else if (ext === '.docx') {
|
|
1516
|
+
input.format = 'docx';
|
|
1517
|
+
modified = true;
|
|
1518
|
+
inference = `${inference ? `${inference}; ` : ''}Inferred format="docx" from filename`;
|
|
1519
|
+
} else {
|
|
1520
|
+
input.format = 'docx';
|
|
1521
|
+
modified = true;
|
|
1522
|
+
inference = `${inference ? `${inference}; ` : ''}Defaulted format="docx"`;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
if (!input.content) {
|
|
1527
|
+
const fallback = this.getContentFallback();
|
|
1528
|
+
if (fallback) {
|
|
1529
|
+
input.content = fallback;
|
|
1530
|
+
modified = true;
|
|
1531
|
+
inference = `${inference ? `${inference}; ` : ''}Inferred content from latest assistant output`;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
return { input, modified, inference: modified ? inference : undefined };
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
if (toolName === 'write_file') {
|
|
1539
|
+
let modified = false;
|
|
1540
|
+
let inference = '';
|
|
1541
|
+
input = input || {};
|
|
1542
|
+
|
|
1543
|
+
if (!input.path && input.filename) {
|
|
1544
|
+
input.path = String(input.filename);
|
|
1545
|
+
modified = true;
|
|
1546
|
+
inference = 'Normalized filename -> path';
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
if (!input.content) {
|
|
1550
|
+
const fallback = this.getContentFallback();
|
|
1551
|
+
if (fallback) {
|
|
1552
|
+
input.content = fallback;
|
|
1553
|
+
modified = true;
|
|
1554
|
+
inference = `${inference ? `${inference}; ` : ''}Inferred content from latest assistant output`;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
return { input, modified, inference: modified ? inference : undefined };
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1380
1561
|
// Handle edit_document - infer sourcePath from recently created documents
|
|
1381
1562
|
if (toolName === 'edit_document') {
|
|
1382
1563
|
let modified = false;
|
|
@@ -1458,9 +1639,101 @@ export class TaskExecutor {
|
|
|
1458
1639
|
return { input, modified, inference: modified ? inference : undefined };
|
|
1459
1640
|
}
|
|
1460
1641
|
|
|
1642
|
+
// Handle web_search - normalize region/country inputs
|
|
1643
|
+
if (toolName === 'web_search') {
|
|
1644
|
+
let modified = false;
|
|
1645
|
+
let inference = '';
|
|
1646
|
+
|
|
1647
|
+
if (!input?.region && input?.country && typeof input.country === 'string') {
|
|
1648
|
+
input.region = input.country;
|
|
1649
|
+
modified = true;
|
|
1650
|
+
inference = 'Normalized country -> region';
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
if (input?.region && typeof input.region === 'string') {
|
|
1654
|
+
const raw = input.region.trim();
|
|
1655
|
+
const upper = raw.toUpperCase();
|
|
1656
|
+
let normalized = upper;
|
|
1657
|
+
if (upper === 'UK') normalized = 'GB';
|
|
1658
|
+
if (upper === 'USA') normalized = 'US';
|
|
1659
|
+
if (normalized !== raw) {
|
|
1660
|
+
input.region = normalized;
|
|
1661
|
+
modified = true;
|
|
1662
|
+
inference = `${inference ? `${inference}; ` : ''}Normalized region "${raw}" -> "${normalized}"`;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
if (modified) {
|
|
1667
|
+
return { input, modified, inference };
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1461
1671
|
return { input, modified: false };
|
|
1462
1672
|
}
|
|
1463
1673
|
|
|
1674
|
+
private getContentFallback(): string | undefined {
|
|
1675
|
+
const candidates = [
|
|
1676
|
+
this.lastAssistantText,
|
|
1677
|
+
this.lastNonVerificationOutput,
|
|
1678
|
+
this.lastAssistantOutput,
|
|
1679
|
+
];
|
|
1680
|
+
const placeholders = new Set([
|
|
1681
|
+
'I understand. Let me continue.',
|
|
1682
|
+
]);
|
|
1683
|
+
for (const candidate of candidates) {
|
|
1684
|
+
if (!candidate) continue;
|
|
1685
|
+
const trimmed = candidate.trim();
|
|
1686
|
+
if (trimmed.length < 20) continue;
|
|
1687
|
+
if (placeholders.has(trimmed)) continue;
|
|
1688
|
+
return trimmed;
|
|
1689
|
+
}
|
|
1690
|
+
return undefined;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
private getToolInputValidationError(toolName: string, input: any): string | null {
|
|
1694
|
+
if (toolName === 'create_document') {
|
|
1695
|
+
if (!input?.filename) return 'create_document requires a filename';
|
|
1696
|
+
if (!input?.format) return 'create_document requires a format (docx or pdf)';
|
|
1697
|
+
if (!input?.content) return 'create_document requires content';
|
|
1698
|
+
}
|
|
1699
|
+
if (toolName === 'write_file') {
|
|
1700
|
+
if (!input?.path) return 'write_file requires a path';
|
|
1701
|
+
if (!input?.content) return 'write_file requires content';
|
|
1702
|
+
}
|
|
1703
|
+
return null;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
private async handleCanvasPushFallback(content: LLMToolUse, assistantText: string): Promise<void> {
|
|
1707
|
+
if (content.name !== 'canvas_push') {
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
const inputContent = content.input?.content;
|
|
1712
|
+
const hasContent = typeof inputContent === 'string' && inputContent.trim().length > 0;
|
|
1713
|
+
const filename = content.input?.filename;
|
|
1714
|
+
const isHtmlTarget = !filename || filename === 'index.html';
|
|
1715
|
+
if (hasContent || !isHtmlTarget) {
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const extracted = this.extractHtmlFromText(assistantText);
|
|
1720
|
+
const generated = extracted || await this.generateCanvasHtml(this.lastUserMessage || this.task.prompt);
|
|
1721
|
+
if (!generated) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
content.input = {
|
|
1726
|
+
...(content.input || {}),
|
|
1727
|
+
content: generated,
|
|
1728
|
+
};
|
|
1729
|
+
this.daemon.logEvent(this.task.id, 'parameter_inference', {
|
|
1730
|
+
tool: content.name,
|
|
1731
|
+
inference: extracted
|
|
1732
|
+
? 'Recovered HTML from assistant text'
|
|
1733
|
+
: 'Auto-generated HTML from latest user request',
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1464
1737
|
/**
|
|
1465
1738
|
* Get available tools, filtering out disabled ones
|
|
1466
1739
|
* This prevents the LLM from trying to use tools that have been disabled by the circuit breaker
|
|
@@ -1576,6 +1849,7 @@ export class TaskExecutor {
|
|
|
1576
1849
|
this.systemPrompt = `You are an AI assistant helping with tasks. Use the available tools to complete the work.
|
|
1577
1850
|
Current time: ${getCurrentDateTimeContext()}
|
|
1578
1851
|
Workspace: ${this.workspace.path}
|
|
1852
|
+
Workspace is temporary: ${this.workspace.isTemp ? 'true' : 'false'}
|
|
1579
1853
|
Always ask for approval before deleting files or making destructive changes.
|
|
1580
1854
|
Be concise in your responses. When reading files, only read what you need.
|
|
1581
1855
|
|
|
@@ -1891,6 +2165,9 @@ You are continuing a previous conversation. The context from the previous conver
|
|
|
1891
2165
|
|
|
1892
2166
|
// Reset tool failure tracker (tools might work on retry)
|
|
1893
2167
|
this.toolFailureTracker = new ToolFailureTracker();
|
|
2168
|
+
this.toolResultMemory = [];
|
|
2169
|
+
this.lastAssistantOutput = null;
|
|
2170
|
+
this.lastNonVerificationOutput = null;
|
|
1894
2171
|
|
|
1895
2172
|
// Add context for LLM about retry
|
|
1896
2173
|
this.conversationHistory.push({
|
|
@@ -2145,6 +2422,391 @@ You are continuing a previous conversation. The context from the previous conver
|
|
|
2145
2422
|
return { additionalContext: additionalContext || undefined, taskType };
|
|
2146
2423
|
}
|
|
2147
2424
|
|
|
2425
|
+
private classifyWorkspaceNeed(prompt: string): 'none' | 'new_ok' | 'ambiguous' | 'needs_existing' {
|
|
2426
|
+
const text = prompt.toLowerCase();
|
|
2427
|
+
|
|
2428
|
+
const newProjectPatterns = [
|
|
2429
|
+
/from\s+scratch/i,
|
|
2430
|
+
/\bnew\s+project\b/i,
|
|
2431
|
+
/\bcreate\s+(?:a|an)\s+new\b/i,
|
|
2432
|
+
/\bstart\s+(?:a|an)\s+new\b/i,
|
|
2433
|
+
/\bscaffold\b/i,
|
|
2434
|
+
/\bbootstrap\b/i,
|
|
2435
|
+
/\binitialize\b/i,
|
|
2436
|
+
/\binit\b/i,
|
|
2437
|
+
/\bgreenfield\b/i,
|
|
2438
|
+
];
|
|
2439
|
+
|
|
2440
|
+
const existingProjectPatterns = [
|
|
2441
|
+
/\bexisting\b/i,
|
|
2442
|
+
/\bcurrent\b/i,
|
|
2443
|
+
/\balready\b/i,
|
|
2444
|
+
/\bin\s+(?:this|the)\s+(?:repo|repository|project|codebase)\b/i,
|
|
2445
|
+
/\bfix\b/i,
|
|
2446
|
+
/\bbug\b/i,
|
|
2447
|
+
/\bdebug\b/i,
|
|
2448
|
+
/\brefactor\b/i,
|
|
2449
|
+
/\bupdate\b/i,
|
|
2450
|
+
/\bmodify\b/i,
|
|
2451
|
+
// Note: 'add' is intentionally omitted - it's ambiguous (could be new or existing)
|
|
2452
|
+
/\bextend\b/i,
|
|
2453
|
+
/\bmigrate\b/i,
|
|
2454
|
+
/\bpatch\b/i,
|
|
2455
|
+
];
|
|
2456
|
+
|
|
2457
|
+
const pathOrFilePatterns = [
|
|
2458
|
+
/(?:^|[\s/\\])[\w.\-\/\\]+?\.(ts|tsx|js|jsx|py|rs|go|java|kt|swift|json|yml|yaml|toml|md|sol|c|cpp|h|hpp)\b/i,
|
|
2459
|
+
/\b(?:src|app|apps|packages|programs|frontend|backend|server|client|contracts|lib|services)\//i,
|
|
2460
|
+
];
|
|
2461
|
+
|
|
2462
|
+
const codeTaskPatterns = [
|
|
2463
|
+
/\bapp\b/i,
|
|
2464
|
+
/\bdapp\b/i,
|
|
2465
|
+
/\bweb\b/i,
|
|
2466
|
+
/\bfrontend\b/i,
|
|
2467
|
+
/\bbackend\b/i,
|
|
2468
|
+
/\bapi\b/i,
|
|
2469
|
+
/\bservice\b/i,
|
|
2470
|
+
/\bprogram\b/i,
|
|
2471
|
+
/\bsmart\s+contract\b/i,
|
|
2472
|
+
/\bcontract\b/i,
|
|
2473
|
+
/\bblockchain\b/i,
|
|
2474
|
+
/\bsolana\b/i,
|
|
2475
|
+
/\breact\b/i,
|
|
2476
|
+
/\bnode\b/i,
|
|
2477
|
+
/\btypescript\b/i,
|
|
2478
|
+
/\bjavascript\b/i,
|
|
2479
|
+
/\bpython\b/i,
|
|
2480
|
+
/\brust\b/i,
|
|
2481
|
+
/\bgo\b/i,
|
|
2482
|
+
/\bjava\b/i,
|
|
2483
|
+
/\bkotlin\b/i,
|
|
2484
|
+
/\bswift\b/i,
|
|
2485
|
+
/\bdatabase\b/i,
|
|
2486
|
+
/\bschema\b/i,
|
|
2487
|
+
/\bmigration\b/i,
|
|
2488
|
+
/\brepo\b/i,
|
|
2489
|
+
/\brepository\b/i,
|
|
2490
|
+
/\bcodebase\b/i,
|
|
2491
|
+
];
|
|
2492
|
+
|
|
2493
|
+
const mentionsNew = newProjectPatterns.some(pattern => pattern.test(text));
|
|
2494
|
+
const isCodeTask = codeTaskPatterns.some(pattern => pattern.test(text));
|
|
2495
|
+
const mentionsExisting = pathOrFilePatterns.some(pattern => pattern.test(text)) ||
|
|
2496
|
+
(existingProjectPatterns.some(pattern => pattern.test(text)) && isCodeTask);
|
|
2497
|
+
|
|
2498
|
+
if (mentionsExisting) return 'needs_existing';
|
|
2499
|
+
if (mentionsNew) return 'new_ok';
|
|
2500
|
+
if (isCodeTask) return 'ambiguous';
|
|
2501
|
+
return 'none';
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
private getWorkspaceSignals(): { hasProjectMarkers: boolean; hasCodeFiles: boolean; hasAppDirs: boolean } {
|
|
2505
|
+
const projectMarkers = new Set([
|
|
2506
|
+
'package.json',
|
|
2507
|
+
'pnpm-lock.yaml',
|
|
2508
|
+
'yarn.lock',
|
|
2509
|
+
'package-lock.json',
|
|
2510
|
+
'Cargo.toml',
|
|
2511
|
+
'Anchor.toml',
|
|
2512
|
+
'pyproject.toml',
|
|
2513
|
+
'requirements.txt',
|
|
2514
|
+
'go.mod',
|
|
2515
|
+
'pom.xml',
|
|
2516
|
+
'build.gradle',
|
|
2517
|
+
'settings.gradle',
|
|
2518
|
+
'Gemfile',
|
|
2519
|
+
'composer.json',
|
|
2520
|
+
'mix.exs',
|
|
2521
|
+
'Makefile',
|
|
2522
|
+
'CMakeLists.txt',
|
|
2523
|
+
]);
|
|
2524
|
+
|
|
2525
|
+
const codeExtensions = new Set([
|
|
2526
|
+
'.ts', '.tsx', '.js', '.jsx', '.py', '.rs', '.go', '.java', '.kt', '.swift',
|
|
2527
|
+
'.cs', '.cpp', '.c', '.h', '.hpp', '.sol',
|
|
2528
|
+
]);
|
|
2529
|
+
|
|
2530
|
+
const appDirs = new Set([
|
|
2531
|
+
'src', 'app', 'apps', 'packages', 'programs', 'frontend', 'backend',
|
|
2532
|
+
'server', 'client', 'contracts', 'lib', 'services', 'web', 'api',
|
|
2533
|
+
]);
|
|
2534
|
+
|
|
2535
|
+
try {
|
|
2536
|
+
const entries = fs.readdirSync(this.workspace.path, { withFileTypes: true });
|
|
2537
|
+
let hasProjectMarkers = false;
|
|
2538
|
+
let hasCodeFiles = false;
|
|
2539
|
+
let hasAppDirs = false;
|
|
2540
|
+
|
|
2541
|
+
for (const entry of entries) {
|
|
2542
|
+
if (entry.isFile()) {
|
|
2543
|
+
if (projectMarkers.has(entry.name)) {
|
|
2544
|
+
hasProjectMarkers = true;
|
|
2545
|
+
}
|
|
2546
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
2547
|
+
if (codeExtensions.has(ext)) {
|
|
2548
|
+
hasCodeFiles = true;
|
|
2549
|
+
}
|
|
2550
|
+
} else if (entry.isDirectory()) {
|
|
2551
|
+
if (appDirs.has(entry.name)) {
|
|
2552
|
+
hasAppDirs = true;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
if (hasProjectMarkers && hasCodeFiles && hasAppDirs) break;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
return { hasProjectMarkers, hasCodeFiles, hasAppDirs };
|
|
2560
|
+
} catch {
|
|
2561
|
+
return { hasProjectMarkers: false, hasCodeFiles: false, hasAppDirs: false };
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
private pauseForUserInput(message: string, reason: string): void {
|
|
2566
|
+
this.waitingForUserInput = true;
|
|
2567
|
+
this.daemon.updateTaskStatus(this.task.id, 'paused');
|
|
2568
|
+
this.daemon.logEvent(this.task.id, 'assistant_message', { message });
|
|
2569
|
+
this.daemon.logEvent(this.task.id, 'task_paused', { message, reason });
|
|
2570
|
+
this.daemon.logEvent(this.task.id, 'progress_update', {
|
|
2571
|
+
phase: 'execution',
|
|
2572
|
+
completedSteps: this.plan?.steps.filter(s => s.status === 'completed').length ?? 0,
|
|
2573
|
+
totalSteps: this.plan?.steps.length ?? 0,
|
|
2574
|
+
progress: 0,
|
|
2575
|
+
message: 'Paused - awaiting user input',
|
|
2576
|
+
});
|
|
2577
|
+
|
|
2578
|
+
if (this.conversationHistory.length === 0) {
|
|
2579
|
+
this.conversationHistory.push({
|
|
2580
|
+
role: 'user',
|
|
2581
|
+
content: this.task.prompt,
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
this.conversationHistory.push({
|
|
2586
|
+
role: 'assistant',
|
|
2587
|
+
content: [{ type: 'text', text: message }],
|
|
2588
|
+
});
|
|
2589
|
+
this.saveConversationSnapshot();
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
private preflightWorkspaceCheck(): boolean {
|
|
2593
|
+
const workspaceNeed = this.classifyWorkspaceNeed(this.task.prompt);
|
|
2594
|
+
if (workspaceNeed === 'none') return false;
|
|
2595
|
+
|
|
2596
|
+
const signals = this.getWorkspaceSignals();
|
|
2597
|
+
const looksLikeProject = signals.hasProjectMarkers || signals.hasCodeFiles || signals.hasAppDirs;
|
|
2598
|
+
const isTemp = this.workspace.isTemp || this.workspace.id === TEMP_WORKSPACE_ID;
|
|
2599
|
+
|
|
2600
|
+
if (isTemp && !looksLikeProject) {
|
|
2601
|
+
if (workspaceNeed === 'needs_existing') {
|
|
2602
|
+
this.pauseForUserInput(
|
|
2603
|
+
'I am in the temporary workspace, but this task looks like it targets an existing project. ' +
|
|
2604
|
+
'Please select the project folder or provide its path so I can switch to it. ' +
|
|
2605
|
+
'If you want a new project created here instead, say so.',
|
|
2606
|
+
'workspace_required'
|
|
2607
|
+
);
|
|
2608
|
+
return true;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
if (workspaceNeed === 'ambiguous') {
|
|
2612
|
+
this.pauseForUserInput(
|
|
2613
|
+
'I am in the temporary workspace and this task could be a new project or changes to an existing one. ' +
|
|
2614
|
+
'Choose one:\n' +
|
|
2615
|
+
'1. Create a new project in the temporary workspace\n' +
|
|
2616
|
+
'2. Switch to an existing project folder (share the path or select a workspace)',
|
|
2617
|
+
'workspace_selection'
|
|
2618
|
+
);
|
|
2619
|
+
return true;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
if (!isTemp && workspaceNeed === 'needs_existing' && !looksLikeProject) {
|
|
2624
|
+
this.pauseForUserInput(
|
|
2625
|
+
'I am in the selected workspace, but I do not see typical project files here. ' +
|
|
2626
|
+
'If this task targets an existing project, please confirm the correct folder or provide its path. ' +
|
|
2627
|
+
'If this is a new project, tell me to scaffold it here.',
|
|
2628
|
+
'workspace_mismatch'
|
|
2629
|
+
);
|
|
2630
|
+
return true;
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
return false;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
private summarizeToolResult(toolName: string, result: any): string | null {
|
|
2637
|
+
if (!result) return null;
|
|
2638
|
+
|
|
2639
|
+
if (toolName === 'web_search') {
|
|
2640
|
+
const query = typeof result.query === 'string' ? result.query : '';
|
|
2641
|
+
const items = Array.isArray(result.results) ? result.results : [];
|
|
2642
|
+
if (items.length === 0) {
|
|
2643
|
+
return query ? `query "${query}": no results` : 'no results';
|
|
2644
|
+
}
|
|
2645
|
+
const formatted = items.slice(0, 5).map((item: any) => {
|
|
2646
|
+
const title = item?.title ? String(item.title).trim() : 'Untitled';
|
|
2647
|
+
const url = item?.url ? String(item.url) : '';
|
|
2648
|
+
let host = '';
|
|
2649
|
+
if (url) {
|
|
2650
|
+
try {
|
|
2651
|
+
host = new URL(url).hostname.replace(/^www\./, '');
|
|
2652
|
+
} catch {
|
|
2653
|
+
host = '';
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
return host ? `${title} (${host})` : title;
|
|
2657
|
+
});
|
|
2658
|
+
const prefix = query ? `query "${query}": ` : '';
|
|
2659
|
+
return `${prefix}${formatted.join(' | ')}`;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
if (toolName === 'web_fetch') {
|
|
2663
|
+
const url = typeof result.url === 'string' ? result.url : '';
|
|
2664
|
+
const content = typeof result.content === 'string' ? result.content : '';
|
|
2665
|
+
const snippet = content
|
|
2666
|
+
? content.replace(/\s+/g, ' ').slice(0, 300)
|
|
2667
|
+
: '';
|
|
2668
|
+
if (url && snippet) return `${url} — ${snippet}`;
|
|
2669
|
+
if (url) return url;
|
|
2670
|
+
if (snippet) return snippet;
|
|
2671
|
+
return null;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
if (toolName === 'search_files') {
|
|
2675
|
+
const totalFound = typeof result.totalFound === 'number' ? result.totalFound : undefined;
|
|
2676
|
+
if (totalFound !== undefined) return `matches found: ${totalFound}`;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
if (toolName === 'glob') {
|
|
2680
|
+
const totalMatches = typeof result.totalMatches === 'number' ? result.totalMatches : undefined;
|
|
2681
|
+
const pattern = typeof result.pattern === 'string' ? result.pattern : '';
|
|
2682
|
+
if (totalMatches !== undefined) {
|
|
2683
|
+
return pattern ? `pattern "${pattern}" matched ${totalMatches} item(s)` : `matched ${totalMatches} item(s)`;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
return null;
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
private recordToolResult(toolName: string, result: any): void {
|
|
2691
|
+
const summary = this.summarizeToolResult(toolName, result);
|
|
2692
|
+
if (!summary) return;
|
|
2693
|
+
this.toolResultMemory.push({ tool: toolName, summary, timestamp: Date.now() });
|
|
2694
|
+
if (this.toolResultMemory.length > this.toolResultMemoryLimit) {
|
|
2695
|
+
this.toolResultMemory.splice(0, this.toolResultMemory.length - this.toolResultMemoryLimit);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
private getRecentToolResultSummary(maxEntries = 6): string {
|
|
2700
|
+
if (this.toolResultMemory.length === 0) return '';
|
|
2701
|
+
const entries = this.toolResultMemory.slice(-maxEntries);
|
|
2702
|
+
return entries.map(entry => `- ${entry.tool}: ${entry.summary}`).join('\n');
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
private isVerificationStep(step: PlanStep): boolean {
|
|
2706
|
+
const desc = step.description.toLowerCase().trim();
|
|
2707
|
+
if (desc.startsWith('verify')) return true;
|
|
2708
|
+
if (desc.startsWith('review')) return true;
|
|
2709
|
+
return desc.includes('verify:') || desc.includes('verification') || desc.includes('verify ');
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
private isSummaryStep(step: PlanStep): boolean {
|
|
2713
|
+
const desc = step.description.toLowerCase();
|
|
2714
|
+
return desc.includes('summary') || desc.includes('summarize') || desc.includes('compile') || desc.includes('report');
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
private isLastPlanStep(step: PlanStep): boolean {
|
|
2718
|
+
if (!this.plan || this.plan.steps.length === 0) return false;
|
|
2719
|
+
const last = this.plan.steps[this.plan.steps.length - 1];
|
|
2720
|
+
return last?.id === step.id;
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
private taskLikelyNeedsWebEvidence(): boolean {
|
|
2724
|
+
const prompt = `${this.task.title}\n${this.task.prompt}`.toLowerCase();
|
|
2725
|
+
const signals = [
|
|
2726
|
+
'news',
|
|
2727
|
+
'latest',
|
|
2728
|
+
'today',
|
|
2729
|
+
'trending',
|
|
2730
|
+
'breaking',
|
|
2731
|
+
'reddit',
|
|
2732
|
+
'search',
|
|
2733
|
+
'headline',
|
|
2734
|
+
'current events',
|
|
2735
|
+
];
|
|
2736
|
+
return signals.some(signal => prompt.includes(signal));
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
private taskRequiresTodayContext(): boolean {
|
|
2740
|
+
const prompt = `${this.task.title}\n${this.task.prompt}`.toLowerCase();
|
|
2741
|
+
return prompt.includes('today');
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
private hasWebEvidence(): boolean {
|
|
2745
|
+
return this.toolResultMemory.some(entry =>
|
|
2746
|
+
entry.tool === 'web_search' || entry.tool === 'web_fetch'
|
|
2747
|
+
);
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
private normalizeToolName(name: string): { name: string; modified: boolean; original: string } {
|
|
2751
|
+
if (!name) return { name, modified: false, original: name };
|
|
2752
|
+
if (!name.includes('.')) return { name, modified: false, original: name };
|
|
2753
|
+
const [prefix, ...rest] = name.split('.');
|
|
2754
|
+
if (rest.length === 0) return { name, modified: false, original: name };
|
|
2755
|
+
if (['functions', 'tool', 'tools'].includes(prefix)) {
|
|
2756
|
+
const normalized = rest.join('.');
|
|
2757
|
+
return { name: normalized, modified: normalized !== name, original: name };
|
|
2758
|
+
}
|
|
2759
|
+
return { name, modified: false, original: name };
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
private recordAssistantOutput(messages: LLMMessage[], step: PlanStep): void {
|
|
2763
|
+
if (!messages || messages.length === 0) return;
|
|
2764
|
+
const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant');
|
|
2765
|
+
if (!lastAssistant || !lastAssistant.content) return;
|
|
2766
|
+
const text = (Array.isArray(lastAssistant.content) ? lastAssistant.content : [])
|
|
2767
|
+
.filter((item: any) => item.type === 'text' && item.text)
|
|
2768
|
+
.map((item: any) => String(item.text))
|
|
2769
|
+
.join('\n')
|
|
2770
|
+
.trim();
|
|
2771
|
+
if (!text) return;
|
|
2772
|
+
const truncated = text.length > 1500 ? `${text.slice(0, 1500)}…` : text;
|
|
2773
|
+
if (!this.isVerificationStep(step)) {
|
|
2774
|
+
this.lastAssistantOutput = truncated;
|
|
2775
|
+
this.lastNonVerificationOutput = truncated;
|
|
2776
|
+
} else {
|
|
2777
|
+
if (!this.lastAssistantOutput) {
|
|
2778
|
+
this.lastAssistantOutput = truncated;
|
|
2779
|
+
}
|
|
2780
|
+
// Preserve lastNonVerificationOutput for future steps/follow-ups.
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
private isTransientProviderError(error: any): boolean {
|
|
2785
|
+
if (!error) return false;
|
|
2786
|
+
const message = String(error.message || '').toLowerCase();
|
|
2787
|
+
const code = error.cause?.code || error.code;
|
|
2788
|
+
const retryableCodes = new Set(['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED']);
|
|
2789
|
+
if (code && retryableCodes.has(code)) return true;
|
|
2790
|
+
return (
|
|
2791
|
+
message.includes('fetch failed') ||
|
|
2792
|
+
message.includes('network') ||
|
|
2793
|
+
message.includes('timeout') ||
|
|
2794
|
+
message.includes('socket hang up')
|
|
2795
|
+
);
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
private async dispatchMentionedAgentsAfterPlanning(): Promise<void> {
|
|
2799
|
+
if (this.dispatchedMentionedAgents) return;
|
|
2800
|
+
if (!this.shouldPauseForQuestions) return;
|
|
2801
|
+
if (!this.plan) return;
|
|
2802
|
+
try {
|
|
2803
|
+
await this.daemon.dispatchMentionedAgents(this.task.id, this.plan);
|
|
2804
|
+
this.dispatchedMentionedAgents = true;
|
|
2805
|
+
} catch (error) {
|
|
2806
|
+
console.warn('[TaskExecutor] Failed to dispatch mentioned agents:', error);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2148
2810
|
/**
|
|
2149
2811
|
* Main execution loop
|
|
2150
2812
|
*/
|
|
@@ -2181,6 +2843,8 @@ You are continuing a previous conversation. The context from the previous conver
|
|
|
2181
2843
|
this.daemon.updateTaskStatus(this.task.id, 'planning');
|
|
2182
2844
|
await this.createPlan();
|
|
2183
2845
|
|
|
2846
|
+
await this.dispatchMentionedAgentsAfterPlanning();
|
|
2847
|
+
|
|
2184
2848
|
if (this.cancelled) return;
|
|
2185
2849
|
|
|
2186
2850
|
// Phase 2: Execution with Goal Mode retry loop
|
|
@@ -2260,6 +2924,13 @@ You are continuing a previous conversation. The context from the previous conver
|
|
|
2260
2924
|
return;
|
|
2261
2925
|
}
|
|
2262
2926
|
|
|
2927
|
+
if (this.isTransientProviderError(error)) {
|
|
2928
|
+
const scheduled = this.daemon.handleTransientTaskFailure(this.task.id, error.message || 'Transient LLM error');
|
|
2929
|
+
if (scheduled) {
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2263
2934
|
console.error(`Task execution failed:`, error);
|
|
2264
2935
|
// Save conversation snapshot even on failure for potential recovery
|
|
2265
2936
|
this.saveConversationSnapshot();
|
|
@@ -2295,6 +2966,7 @@ You are continuing a previous conversation. The context from the previous conver
|
|
|
2295
2966
|
|
|
2296
2967
|
Current time: ${getCurrentDateTimeContext()}
|
|
2297
2968
|
You have access to a workspace folder at: ${this.workspace.path}
|
|
2969
|
+
Workspace is temporary: ${this.workspace.isTemp ? 'true' : 'false'}
|
|
2298
2970
|
Workspace permissions: ${JSON.stringify(this.workspace.permissions)}
|
|
2299
2971
|
|
|
2300
2972
|
Available tools:
|
|
@@ -2307,6 +2979,12 @@ PLANNING RULES:
|
|
|
2307
2979
|
- DO NOT plan to create multiple versions of files - pick ONE target file.
|
|
2308
2980
|
- DO NOT plan to read the same file multiple times in different steps.
|
|
2309
2981
|
|
|
2982
|
+
WORKSPACE MODE (CRITICAL):
|
|
2983
|
+
- There are two modes: temporary workspace (no user-selected folder) and user-selected workspace.
|
|
2984
|
+
- If the workspace is temporary and the task likely targets an existing project, your FIRST step must be to ask for the correct folder or to switch workspaces.
|
|
2985
|
+
- If the task could be new or existing, ask the user to choose between scaffolding here or switching to an existing folder.
|
|
2986
|
+
- Do NOT assume a repo exists in the temporary workspace unless you find it.
|
|
2987
|
+
|
|
2310
2988
|
PATH DISCOVERY (CRITICAL):
|
|
2311
2989
|
- When users mention a folder or path (e.g., "electron/agent folder"), they may give a PARTIAL path, not the full path.
|
|
2312
2990
|
- NEVER assume a path doesn't exist just because it's not in your workspace root.
|
|
@@ -2346,6 +3024,11 @@ WEB RESEARCH & CONTENT EXTRACTION (IMPORTANT):
|
|
|
2346
3024
|
- NEVER create a plan that says "cannot be done" if alternative tools are available.
|
|
2347
3025
|
- NEVER plan to ask the user for content you can extract yourself.
|
|
2348
3026
|
|
|
3027
|
+
REDDIT POSTS (WHEN UPVOTE COUNTS REQUIRED):
|
|
3028
|
+
- Prefer web_fetch against Reddit's JSON endpoints to get reliable titles and upvote counts.
|
|
3029
|
+
- Example: https://www.reddit.com/r/<sub>/top/.json?t=day&limit=5
|
|
3030
|
+
- Use web_search only to discover the right subreddit if needed, not for score counts.
|
|
3031
|
+
|
|
2349
3032
|
TOOL SELECTION GUIDE (web tools):
|
|
2350
3033
|
- web_search: Best for research, news, finding information, exploring topics (PREFERRED for most research)
|
|
2351
3034
|
- web_fetch: Best for reading a specific known URL without interaction
|
|
@@ -2563,23 +3246,26 @@ Format your plan as a JSON object with this structure:
|
|
|
2563
3246
|
throw new Error('No plan available');
|
|
2564
3247
|
}
|
|
2565
3248
|
|
|
2566
|
-
|
|
2567
|
-
|
|
3249
|
+
if (this.preflightWorkspaceCheck()) {
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
2568
3252
|
|
|
2569
3253
|
// Emit initial progress event
|
|
2570
3254
|
this.daemon.logEvent(this.task.id, 'progress_update', {
|
|
2571
3255
|
phase: 'execution',
|
|
2572
|
-
completedSteps,
|
|
2573
|
-
totalSteps,
|
|
3256
|
+
completedSteps: this.plan.steps.filter(s => s.status === 'completed').length,
|
|
3257
|
+
totalSteps: this.plan.steps.length,
|
|
2574
3258
|
progress: 0,
|
|
2575
|
-
message: `Starting execution of ${
|
|
3259
|
+
message: `Starting execution of ${this.plan.steps.length} steps`,
|
|
2576
3260
|
});
|
|
2577
3261
|
|
|
2578
|
-
|
|
3262
|
+
let index = 0;
|
|
3263
|
+
while (index < this.plan.steps.length) {
|
|
3264
|
+
const step = this.plan.steps[index];
|
|
2579
3265
|
if (this.cancelled) break;
|
|
2580
3266
|
|
|
2581
3267
|
if (step.status === 'completed') {
|
|
2582
|
-
|
|
3268
|
+
index++;
|
|
2583
3269
|
continue;
|
|
2584
3270
|
}
|
|
2585
3271
|
|
|
@@ -2588,6 +3274,9 @@ Format your plan as a JSON object with this structure:
|
|
|
2588
3274
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
2589
3275
|
}
|
|
2590
3276
|
|
|
3277
|
+
const completedSteps = this.plan.steps.filter(s => s.status === 'completed').length;
|
|
3278
|
+
const totalSteps = this.plan.steps.length;
|
|
3279
|
+
|
|
2591
3280
|
// Emit step starting progress
|
|
2592
3281
|
this.daemon.logEvent(this.task.id, 'progress_update', {
|
|
2593
3282
|
phase: 'execution',
|
|
@@ -2645,22 +3334,34 @@ Format your plan as a JSON object with this structure:
|
|
|
2645
3334
|
message: `Step timed out after ${STEP_TIMEOUT_MS / 1000}s`,
|
|
2646
3335
|
});
|
|
2647
3336
|
// Continue with next step instead of failing entire task
|
|
2648
|
-
|
|
3337
|
+
const updatedIndex = this.plan.steps.findIndex(s => s.id === step.id);
|
|
3338
|
+
if (updatedIndex === -1) {
|
|
3339
|
+
index = Math.min(index + 1, this.plan.steps.length);
|
|
3340
|
+
} else {
|
|
3341
|
+
index = updatedIndex + 1;
|
|
3342
|
+
}
|
|
2649
3343
|
continue;
|
|
2650
3344
|
}
|
|
2651
3345
|
throw error;
|
|
2652
3346
|
}
|
|
2653
3347
|
|
|
2654
|
-
|
|
3348
|
+
const updatedIndex = this.plan.steps.findIndex(s => s.id === step.id);
|
|
3349
|
+
if (updatedIndex === -1) {
|
|
3350
|
+
index = Math.min(index + 1, this.plan.steps.length);
|
|
3351
|
+
} else {
|
|
3352
|
+
index = updatedIndex + 1;
|
|
3353
|
+
}
|
|
3354
|
+
const completedAfterStep = this.plan.steps.filter(s => s.status === 'completed').length;
|
|
3355
|
+
const totalAfterStep = this.plan.steps.length;
|
|
2655
3356
|
|
|
2656
3357
|
// Emit step completed progress
|
|
2657
3358
|
this.daemon.logEvent(this.task.id, 'progress_update', {
|
|
2658
3359
|
phase: 'execution',
|
|
2659
3360
|
currentStep: step.id,
|
|
2660
|
-
completedSteps,
|
|
2661
|
-
totalSteps,
|
|
2662
|
-
progress: Math.round((
|
|
2663
|
-
message: `Completed step ${
|
|
3361
|
+
completedSteps: completedAfterStep,
|
|
3362
|
+
totalSteps: totalAfterStep,
|
|
3363
|
+
progress: totalAfterStep > 0 ? Math.round((completedAfterStep / totalAfterStep) * 100) : 100,
|
|
3364
|
+
message: `Completed step ${step.id}: ${step.description}`,
|
|
2664
3365
|
});
|
|
2665
3366
|
}
|
|
2666
3367
|
|
|
@@ -2676,11 +3377,13 @@ Format your plan as a JSON object with this structure:
|
|
|
2676
3377
|
// If critical steps failed (not just verification), this should be marked
|
|
2677
3378
|
const criticalFailures = failedSteps.filter(s => !s.description.toLowerCase().includes('verify'));
|
|
2678
3379
|
if (criticalFailures.length > 0) {
|
|
3380
|
+
const totalSteps = this.plan.steps.length;
|
|
3381
|
+
const progress = totalSteps > 0 ? Math.round((successfulSteps.length / totalSteps) * 100) : 0;
|
|
2679
3382
|
this.daemon.logEvent(this.task.id, 'progress_update', {
|
|
2680
3383
|
phase: 'execution',
|
|
2681
3384
|
completedSteps: successfulSteps.length,
|
|
2682
3385
|
totalSteps,
|
|
2683
|
-
progress
|
|
3386
|
+
progress,
|
|
2684
3387
|
message: `Completed with ${criticalFailures.length} failed step(s)`,
|
|
2685
3388
|
hasFailures: true,
|
|
2686
3389
|
});
|
|
@@ -2692,8 +3395,8 @@ Format your plan as a JSON object with this structure:
|
|
|
2692
3395
|
// Emit completion progress (only if no critical failures)
|
|
2693
3396
|
this.daemon.logEvent(this.task.id, 'progress_update', {
|
|
2694
3397
|
phase: 'execution',
|
|
2695
|
-
completedSteps,
|
|
2696
|
-
totalSteps,
|
|
3398
|
+
completedSteps: successfulSteps.length,
|
|
3399
|
+
totalSteps: this.plan.steps.length,
|
|
2697
3400
|
progress: 100,
|
|
2698
3401
|
message: 'All steps completed',
|
|
2699
3402
|
});
|
|
@@ -2761,6 +3464,11 @@ IMPORTANT INSTRUCTIONS:
|
|
|
2761
3464
|
- The delete_file tool has a built-in approval mechanism that will prompt the user. Just call the tool directly.
|
|
2762
3465
|
- Do NOT ask "Should I proceed?" or wait for permission in text - the tools handle approvals automatically.
|
|
2763
3466
|
|
|
3467
|
+
USER INPUT GATE (CRITICAL):
|
|
3468
|
+
- If you ask the user for required information or a decision, STOP and wait.
|
|
3469
|
+
- Do NOT continue executing steps or call tools after asking such questions.
|
|
3470
|
+
- If safe defaults exist, state the assumption and proceed without asking.
|
|
3471
|
+
|
|
2764
3472
|
PATH DISCOVERY (CRITICAL):
|
|
2765
3473
|
- When a task mentions a folder or path (e.g., "electron/agent folder"), users often give PARTIAL paths.
|
|
2766
3474
|
- NEVER conclude a path doesn't exist without SEARCHING for it first.
|
|
@@ -2850,6 +3558,11 @@ RESEARCH WORKFLOW:
|
|
|
2850
3558
|
- Only fall back to browser_navigate if web_fetch fails (e.g., JavaScript-required content)
|
|
2851
3559
|
- Many sites (X/Twitter, Reddit logged-in content, LinkedIn) require authentication - web_search can still find public discussions
|
|
2852
3560
|
|
|
3561
|
+
REDDIT POSTS (WHEN UPVOTE COUNTS REQUIRED):
|
|
3562
|
+
- Prefer web_fetch against Reddit's JSON endpoints to get reliable titles and upvote counts.
|
|
3563
|
+
- Example: https://www.reddit.com/r/<sub>/top/.json?t=day&limit=5
|
|
3564
|
+
- Use web_search only to discover the right subreddit if needed, not for score counts.
|
|
3565
|
+
|
|
2853
3566
|
BROWSER TOOLS (when needed):
|
|
2854
3567
|
- Treat browser_navigate + browser_get_content as ONE ATOMIC OPERATION
|
|
2855
3568
|
- For dynamic content, use browser_wait then browser_get_content
|
|
@@ -2908,12 +3621,48 @@ SCHEDULING & REMINDERS:
|
|
|
2908
3621
|
stepContext += `\n\nDo NOT repeat work from previous steps. Focus only on: ${step.description}`;
|
|
2909
3622
|
}
|
|
2910
3623
|
|
|
3624
|
+
const isVerifyStep = this.isVerificationStep(step);
|
|
3625
|
+
const isSummaryStep = this.isSummaryStep(step);
|
|
3626
|
+
const isLastStep = this.isLastPlanStep(step);
|
|
3627
|
+
|
|
2911
3628
|
// Add accumulated knowledge from previous steps (discovered files, directories, etc.)
|
|
2912
3629
|
const knowledgeSummary = this.fileOperationTracker.getKnowledgeSummary();
|
|
2913
3630
|
if (knowledgeSummary) {
|
|
2914
3631
|
stepContext += `\n\nKNOWLEDGE FROM PREVIOUS STEPS (use this instead of re-reading/re-listing):\n${knowledgeSummary}`;
|
|
2915
3632
|
}
|
|
2916
3633
|
|
|
3634
|
+
const toolResultSummary = this.getRecentToolResultSummary();
|
|
3635
|
+
if (toolResultSummary) {
|
|
3636
|
+
stepContext += `\n\nRECENT TOOL RESULTS (from previous steps; do not look in the filesystem for these):\n${toolResultSummary}`;
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
const shouldIncludePreviousOutput = !isVerifyStep || !this.lastNonVerificationOutput;
|
|
3640
|
+
if (this.lastAssistantOutput && shouldIncludePreviousOutput) {
|
|
3641
|
+
stepContext += `\n\nPREVIOUS STEP OUTPUT:\n${this.lastAssistantOutput}`;
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
if (isVerifyStep) {
|
|
3645
|
+
stepContext += `\n\nVERIFICATION MODE:\n- This is a verification step. Keep the response brief (1-3 sentences).\n- Do NOT output a checklist. Do NOT restate the full deliverable.\n- If the deliverable has NOT been provided earlier, provide it now, then add a one-sentence verification note.\n`;
|
|
3646
|
+
if (isLastStep) {
|
|
3647
|
+
stepContext += `- This is the FINAL step. Include a very short recap (2-4 sentences) of the deliverable before the verification note so the last message still answers the user.\n`;
|
|
3648
|
+
}
|
|
3649
|
+
if (this.lastNonVerificationOutput) {
|
|
3650
|
+
stepContext += `\n\nMOST RECENT DELIVERABLE (use this for verification):\n${this.lastNonVerificationOutput}`;
|
|
3651
|
+
} else if (this.lastAssistantOutput) {
|
|
3652
|
+
stepContext += `\n\nMOST RECENT DELIVERABLE (use this for verification):\n${this.lastAssistantOutput}`;
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
if (isSummaryStep) {
|
|
3657
|
+
stepContext += `\n\nDELIVERABLE RULES:\n- If you write a file, you MUST also provide the key summary in your response.\n- Do not defer the answer to a verification step.\n`;
|
|
3658
|
+
if (this.taskLikelyNeedsWebEvidence() && !this.hasWebEvidence()) {
|
|
3659
|
+
stepContext += `\n\nEVIDENCE REQUIRED:\n- No web evidence has been gathered yet. Use web_search/web_fetch now before summarizing.\n- If you find no results, say so explicitly instead of guessing.\n`;
|
|
3660
|
+
}
|
|
3661
|
+
if (this.taskRequiresTodayContext()) {
|
|
3662
|
+
stepContext += `\n\nDATE REQUIREMENT:\n- This task explicitly asks for “today.” Only present items as “today” if you can confirm the date from sources.\n- If you cannot confirm any items from today, state that clearly, then optionally list the most recent items as “recent (not today)”.\n`;
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
|
|
2917
3666
|
// Start fresh messages for this step
|
|
2918
3667
|
let messages: LLMMessage[] = [
|
|
2919
3668
|
{
|
|
@@ -2929,8 +3678,18 @@ SCHEDULING & REMINDERS:
|
|
|
2929
3678
|
let lastFailureReason = ''; // Track the reason for failure
|
|
2930
3679
|
let hadToolError = false;
|
|
2931
3680
|
let hadToolSuccessAfterError = false;
|
|
3681
|
+
let hadAnyToolSuccess = false;
|
|
3682
|
+
const toolErrors = new Set<string>();
|
|
2932
3683
|
let lastToolErrorReason = '';
|
|
2933
3684
|
let awaitingUserInput = false;
|
|
3685
|
+
let hadRunCommandFailure = false;
|
|
3686
|
+
let hadToolSuccessAfterRunCommandFailure = false;
|
|
3687
|
+
const expectsImageVerification = this.stepRequiresImageVerification(step);
|
|
3688
|
+
const imageVerificationSince =
|
|
3689
|
+
typeof this.task.createdAt === 'number'
|
|
3690
|
+
? this.task.createdAt
|
|
3691
|
+
: (step.startedAt ?? Date.now());
|
|
3692
|
+
let foundNewImage = false;
|
|
2934
3693
|
const maxIterations = 5; // Reduced from 10 to prevent excessive iterations per step
|
|
2935
3694
|
const maxEmptyResponses = 3;
|
|
2936
3695
|
|
|
@@ -2954,6 +3713,8 @@ SCHEDULING & REMINDERS:
|
|
|
2954
3713
|
// Compact messages if context is getting too large
|
|
2955
3714
|
messages = this.contextManager.compactMessages(messages, systemPromptTokens);
|
|
2956
3715
|
|
|
3716
|
+
const availableTools = this.getAvailableTools();
|
|
3717
|
+
|
|
2957
3718
|
// Use retry wrapper for resilient API calls
|
|
2958
3719
|
const response = await this.callLLMWithRetry(
|
|
2959
3720
|
() => withTimeout(
|
|
@@ -2961,7 +3722,7 @@ SCHEDULING & REMINDERS:
|
|
|
2961
3722
|
model: this.modelId,
|
|
2962
3723
|
maxTokens: 4096,
|
|
2963
3724
|
system: this.systemPrompt,
|
|
2964
|
-
tools:
|
|
3725
|
+
tools: availableTools,
|
|
2965
3726
|
messages,
|
|
2966
3727
|
signal: this.abortController.signal,
|
|
2967
3728
|
}),
|
|
@@ -2984,6 +3745,16 @@ SCHEDULING & REMINDERS:
|
|
|
2984
3745
|
|
|
2985
3746
|
// Log any text responses from the assistant and check if asking a question
|
|
2986
3747
|
let assistantAskedQuestion = false;
|
|
3748
|
+
const assistantText = (response.content || [])
|
|
3749
|
+
.filter((item: any) => item.type === 'text' && item.text)
|
|
3750
|
+
.map((item: any) => item.text)
|
|
3751
|
+
.join('\n');
|
|
3752
|
+
if (assistantText && assistantText.trim().length > 0) {
|
|
3753
|
+
this.lastAssistantText = assistantText.trim();
|
|
3754
|
+
}
|
|
3755
|
+
if (assistantText && assistantText.trim().length > 0) {
|
|
3756
|
+
this.lastAssistantText = assistantText.trim();
|
|
3757
|
+
}
|
|
2987
3758
|
if (response.content) {
|
|
2988
3759
|
for (const content of response.content) {
|
|
2989
3760
|
if (content.type === 'text' && content.text) {
|
|
@@ -3031,9 +3802,21 @@ SCHEDULING & REMINDERS:
|
|
|
3031
3802
|
const toolResults: LLMToolResult[] = [];
|
|
3032
3803
|
let hasDisabledToolAttempt = false;
|
|
3033
3804
|
let hasDuplicateToolAttempt = false;
|
|
3805
|
+
let hasUnavailableToolAttempt = false;
|
|
3806
|
+
const availableToolNames = new Set(availableTools.map(tool => tool.name));
|
|
3034
3807
|
|
|
3035
3808
|
for (const content of response.content || []) {
|
|
3036
3809
|
if (content.type === 'tool_use') {
|
|
3810
|
+
// Normalize tool names like "functions.web_fetch" -> "web_fetch"
|
|
3811
|
+
const normalizedTool = this.normalizeToolName(content.name);
|
|
3812
|
+
if (normalizedTool.modified) {
|
|
3813
|
+
this.daemon.logEvent(this.task.id, 'parameter_inference', {
|
|
3814
|
+
tool: content.name,
|
|
3815
|
+
inference: `Normalized tool name "${normalizedTool.original}" -> "${normalizedTool.name}"`,
|
|
3816
|
+
});
|
|
3817
|
+
content.name = normalizedTool.name;
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3037
3820
|
// Check if this tool is disabled (circuit breaker tripped)
|
|
3038
3821
|
if (this.toolFailureTracker.isDisabled(content.name)) {
|
|
3039
3822
|
const lastError = this.toolFailureTracker.getLastError(content.name);
|
|
@@ -3056,6 +3839,60 @@ SCHEDULING & REMINDERS:
|
|
|
3056
3839
|
continue;
|
|
3057
3840
|
}
|
|
3058
3841
|
|
|
3842
|
+
// Validate tool availability before attempting any inference
|
|
3843
|
+
if (!availableToolNames.has(content.name)) {
|
|
3844
|
+
console.log(`[TaskExecutor] Tool not available in this context: ${content.name}`);
|
|
3845
|
+
this.daemon.logEvent(this.task.id, 'tool_error', {
|
|
3846
|
+
tool: content.name,
|
|
3847
|
+
error: 'Tool not available in current context or permissions',
|
|
3848
|
+
blocked: true,
|
|
3849
|
+
});
|
|
3850
|
+
toolResults.push({
|
|
3851
|
+
type: 'tool_result',
|
|
3852
|
+
tool_use_id: content.id,
|
|
3853
|
+
content: JSON.stringify({
|
|
3854
|
+
error: `Tool "${content.name}" is not available in this context. Please choose a different tool or check permissions/integrations.`,
|
|
3855
|
+
unavailable: true,
|
|
3856
|
+
}),
|
|
3857
|
+
is_error: true,
|
|
3858
|
+
});
|
|
3859
|
+
hasUnavailableToolAttempt = true;
|
|
3860
|
+
continue;
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
// Infer missing parameters for weaker models (normalize inputs before deduplication)
|
|
3864
|
+
const inference = this.inferMissingParameters(content.name, content.input);
|
|
3865
|
+
if (inference.modified) {
|
|
3866
|
+
content.input = inference.input;
|
|
3867
|
+
this.daemon.logEvent(this.task.id, 'parameter_inference', {
|
|
3868
|
+
tool: content.name,
|
|
3869
|
+
inference: inference.inference,
|
|
3870
|
+
});
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
// If canvas_push is missing content, try extracting HTML from assistant text or auto-generate
|
|
3874
|
+
await this.handleCanvasPushFallback(content, assistantText);
|
|
3875
|
+
|
|
3876
|
+
const validationError = this.getToolInputValidationError(content.name, content.input);
|
|
3877
|
+
if (validationError) {
|
|
3878
|
+
this.daemon.logEvent(this.task.id, 'tool_warning', {
|
|
3879
|
+
tool: content.name,
|
|
3880
|
+
error: validationError,
|
|
3881
|
+
input: content.input,
|
|
3882
|
+
});
|
|
3883
|
+
toolResults.push({
|
|
3884
|
+
type: 'tool_result',
|
|
3885
|
+
tool_use_id: content.id,
|
|
3886
|
+
content: JSON.stringify({
|
|
3887
|
+
error: validationError,
|
|
3888
|
+
suggestion: 'Include all required fields in the tool call (e.g., content for create_document/write_file).',
|
|
3889
|
+
invalid_input: true,
|
|
3890
|
+
}),
|
|
3891
|
+
is_error: true,
|
|
3892
|
+
});
|
|
3893
|
+
continue;
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3059
3896
|
// Check for duplicate tool calls (prevents stuck loops)
|
|
3060
3897
|
const duplicateCheck = this.toolCallDeduplicator.checkDuplicate(content.name, content.input);
|
|
3061
3898
|
if (duplicateCheck.isDuplicate) {
|
|
@@ -3129,16 +3966,6 @@ SCHEDULING & REMINDERS:
|
|
|
3129
3966
|
continue;
|
|
3130
3967
|
}
|
|
3131
3968
|
|
|
3132
|
-
// Infer missing parameters for weaker models
|
|
3133
|
-
const inference = this.inferMissingParameters(content.name, content.input);
|
|
3134
|
-
if (inference.modified) {
|
|
3135
|
-
content.input = inference.input;
|
|
3136
|
-
this.daemon.logEvent(this.task.id, 'parameter_inference', {
|
|
3137
|
-
tool: content.name,
|
|
3138
|
-
inference: inference.inference,
|
|
3139
|
-
});
|
|
3140
|
-
}
|
|
3141
|
-
|
|
3142
3969
|
this.daemon.logEvent(this.task.id, 'tool_call', {
|
|
3143
3970
|
tool: content.name,
|
|
3144
3971
|
input: content.input,
|
|
@@ -3146,15 +3973,42 @@ SCHEDULING & REMINDERS:
|
|
|
3146
3973
|
|
|
3147
3974
|
try {
|
|
3148
3975
|
// Execute tool with timeout to prevent hanging
|
|
3149
|
-
const
|
|
3976
|
+
const toolTimeoutMs = this.getToolTimeoutMs(content.name, content.input);
|
|
3977
|
+
let result = await withTimeout(
|
|
3150
3978
|
this.toolRegistry.executeTool(
|
|
3151
3979
|
content.name,
|
|
3152
3980
|
content.input as any
|
|
3153
3981
|
),
|
|
3154
|
-
|
|
3982
|
+
toolTimeoutMs,
|
|
3155
3983
|
`Tool ${content.name}`
|
|
3156
3984
|
);
|
|
3157
3985
|
|
|
3986
|
+
// Fallback: retry grep without glob if the glob produced an invalid regex
|
|
3987
|
+
if (content.name === 'grep' && result && result.success === false && content.input?.glob) {
|
|
3988
|
+
const errorText = String(result.error || '');
|
|
3989
|
+
if (/invalid regex pattern|nothing to repeat/i.test(errorText)) {
|
|
3990
|
+
this.daemon.logEvent(this.task.id, 'tool_fallback', {
|
|
3991
|
+
tool: 'grep',
|
|
3992
|
+
reason: 'invalid_glob_regex',
|
|
3993
|
+
originalGlob: content.input.glob,
|
|
3994
|
+
});
|
|
3995
|
+
const fallbackInput = { ...content.input };
|
|
3996
|
+
delete (fallbackInput as any).glob;
|
|
3997
|
+
try {
|
|
3998
|
+
const fallbackResult = await withTimeout(
|
|
3999
|
+
this.toolRegistry.executeTool('grep', fallbackInput as any),
|
|
4000
|
+
toolTimeoutMs,
|
|
4001
|
+
'Tool grep (fallback)'
|
|
4002
|
+
);
|
|
4003
|
+
if (fallbackResult && fallbackResult.success !== false) {
|
|
4004
|
+
result = fallbackResult;
|
|
4005
|
+
}
|
|
4006
|
+
} catch {
|
|
4007
|
+
// Keep original error if fallback fails
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
|
|
3158
4012
|
// Tool succeeded - reset failure counter
|
|
3159
4013
|
this.toolFailureTracker.recordSuccess(content.name);
|
|
3160
4014
|
|
|
@@ -3165,7 +4019,25 @@ SCHEDULING & REMINDERS:
|
|
|
3165
4019
|
// Record file operation for tracking
|
|
3166
4020
|
this.recordFileOperation(content.name, content.input, result);
|
|
3167
4021
|
this.recordCommandExecution(content.name, content.input, result);
|
|
3168
|
-
|
|
4022
|
+
|
|
4023
|
+
const toolSucceeded = !(result && result.success === false);
|
|
4024
|
+
|
|
4025
|
+
if (toolSucceeded) {
|
|
4026
|
+
hadAnyToolSuccess = true;
|
|
4027
|
+
this.recordToolResult(content.name, result);
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
if (content.name === 'run_command' && !toolSucceeded) {
|
|
4031
|
+
hadRunCommandFailure = true;
|
|
4032
|
+
} else if (hadRunCommandFailure && toolSucceeded) {
|
|
4033
|
+
hadToolSuccessAfterRunCommandFailure = true;
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
if (expectsImageVerification && content.name === 'glob' && !foundNewImage) {
|
|
4037
|
+
if (this.hasNewImageFromGlobResult(result, imageVerificationSince)) {
|
|
4038
|
+
foundNewImage = true;
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
3169
4041
|
|
|
3170
4042
|
// Check if the result indicates an error (some tools return error in result)
|
|
3171
4043
|
if (result && result.success === false) {
|
|
@@ -3174,6 +4046,7 @@ SCHEDULING & REMINDERS:
|
|
|
3174
4046
|
|| (typeof result.exitCode === 'number' ? `exit code ${result.exitCode}` : undefined)
|
|
3175
4047
|
|| 'unknown error';
|
|
3176
4048
|
hadToolError = true;
|
|
4049
|
+
toolErrors.add(content.name);
|
|
3177
4050
|
lastToolErrorReason = `Tool ${content.name} failed: ${reason}`;
|
|
3178
4051
|
// Check if this is a non-retryable error
|
|
3179
4052
|
const shouldDisable = this.toolFailureTracker.recordFailure(content.name, result.error || reason);
|
|
@@ -3233,7 +4106,11 @@ SCHEDULING & REMINDERS:
|
|
|
3233
4106
|
console.error(`Tool execution failed:`, error);
|
|
3234
4107
|
|
|
3235
4108
|
hadToolError = true;
|
|
4109
|
+
toolErrors.add(content.name);
|
|
3236
4110
|
lastToolErrorReason = `Tool ${content.name} failed: ${error.message}`;
|
|
4111
|
+
if (content.name === 'run_command') {
|
|
4112
|
+
hadRunCommandFailure = true;
|
|
4113
|
+
}
|
|
3237
4114
|
|
|
3238
4115
|
// Track the failure
|
|
3239
4116
|
const shouldDisable = this.toolFailureTracker.recordFailure(content.name, error.message);
|
|
@@ -3266,7 +4143,7 @@ SCHEDULING & REMINDERS:
|
|
|
3266
4143
|
// If all tool attempts were for disabled or duplicate tools, don't continue looping
|
|
3267
4144
|
// This prevents infinite retry loops
|
|
3268
4145
|
const allToolsFailed = toolResults.every(r => r.is_error);
|
|
3269
|
-
if ((hasDisabledToolAttempt || hasDuplicateToolAttempt) && allToolsFailed) {
|
|
4146
|
+
if ((hasDisabledToolAttempt || hasDuplicateToolAttempt || hasUnavailableToolAttempt) && allToolsFailed) {
|
|
3270
4147
|
console.log('[TaskExecutor] All tool calls failed, were disabled, or duplicates - stopping iteration');
|
|
3271
4148
|
if (hasDuplicateToolAttempt) {
|
|
3272
4149
|
// Duplicate detection triggered - step is likely complete
|
|
@@ -3282,8 +4159,8 @@ SCHEDULING & REMINDERS:
|
|
|
3282
4159
|
}
|
|
3283
4160
|
}
|
|
3284
4161
|
|
|
3285
|
-
// If assistant asked a question
|
|
3286
|
-
if (assistantAskedQuestion &&
|
|
4162
|
+
// If assistant asked a blocking question, stop and wait for user
|
|
4163
|
+
if (assistantAskedQuestion && this.shouldPauseForQuestions) {
|
|
3287
4164
|
console.log('[TaskExecutor] Assistant asked a question, pausing for user input');
|
|
3288
4165
|
awaitingUserInput = true;
|
|
3289
4166
|
continueLoop = false;
|
|
@@ -3291,14 +4168,34 @@ SCHEDULING & REMINDERS:
|
|
|
3291
4168
|
}
|
|
3292
4169
|
|
|
3293
4170
|
if (hadToolError && !hadToolSuccessAfterError) {
|
|
4171
|
+
const nonCriticalErrorTools = new Set(['web_search', 'web_fetch']);
|
|
4172
|
+
const onlyNonCriticalErrors = toolErrors.size > 0 && Array.from(toolErrors).every(t => nonCriticalErrorTools.has(t));
|
|
4173
|
+
if (!(hadAnyToolSuccess && onlyNonCriticalErrors)) {
|
|
4174
|
+
stepFailed = true;
|
|
4175
|
+
if (!lastFailureReason) {
|
|
4176
|
+
lastFailureReason = lastToolErrorReason || 'One or more tools failed without recovery.';
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
if (hadRunCommandFailure && !hadToolSuccessAfterRunCommandFailure) {
|
|
3294
4182
|
stepFailed = true;
|
|
3295
4183
|
if (!lastFailureReason) {
|
|
3296
|
-
lastFailureReason =
|
|
4184
|
+
lastFailureReason = 'run_command failed and no subsequent tool succeeded.';
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
if (expectsImageVerification && !foundNewImage) {
|
|
4189
|
+
stepFailed = true;
|
|
4190
|
+
if (!lastFailureReason) {
|
|
4191
|
+
lastFailureReason = 'Verification failed: no newly generated image was found.';
|
|
3297
4192
|
}
|
|
3298
4193
|
}
|
|
3299
4194
|
|
|
3300
4195
|
// Step completed or failed
|
|
3301
4196
|
|
|
4197
|
+
this.recordAssistantOutput(messages, step);
|
|
4198
|
+
|
|
3302
4199
|
// Save conversation history for follow-up messages
|
|
3303
4200
|
this.conversationHistory = messages;
|
|
3304
4201
|
|
|
@@ -3383,6 +4280,60 @@ SCHEDULING & REMINDERS:
|
|
|
3383
4280
|
}
|
|
3384
4281
|
}
|
|
3385
4282
|
|
|
4283
|
+
private extractHtmlFromText(text: string): string | null {
|
|
4284
|
+
if (!text) return null;
|
|
4285
|
+
const fenceMatch = text.match(/```html([\s\S]*?)```/i);
|
|
4286
|
+
const raw = fenceMatch ? fenceMatch[1].trim() : text;
|
|
4287
|
+
const doctypeIndex = raw.indexOf('<!DOCTYPE html');
|
|
4288
|
+
if (doctypeIndex >= 0) {
|
|
4289
|
+
const endIndex = raw.lastIndexOf('</html>');
|
|
4290
|
+
if (endIndex > doctypeIndex) {
|
|
4291
|
+
return raw.slice(doctypeIndex, endIndex + '</html>'.length).trim();
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
const htmlIndex = raw.indexOf('<html');
|
|
4295
|
+
if (htmlIndex >= 0) {
|
|
4296
|
+
const endIndex = raw.lastIndexOf('</html>');
|
|
4297
|
+
if (endIndex > htmlIndex) {
|
|
4298
|
+
return raw.slice(htmlIndex, endIndex + '</html>'.length).trim();
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
return null;
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
private async generateCanvasHtml(prompt: string): Promise<string | null> {
|
|
4305
|
+
const system = [
|
|
4306
|
+
'You generate a single self-contained HTML document for an in-app canvas.',
|
|
4307
|
+
'Output ONLY the HTML document (no markdown, no commentary).',
|
|
4308
|
+
'Use inline CSS and JS. Do not reference external assets or remote URLs.',
|
|
4309
|
+
'Keep it reasonably compact and interactive where appropriate.',
|
|
4310
|
+
].join(' ');
|
|
4311
|
+
|
|
4312
|
+
try {
|
|
4313
|
+
const response = await this.provider.createMessage({
|
|
4314
|
+
model: this.modelId,
|
|
4315
|
+
maxTokens: 1800,
|
|
4316
|
+
system,
|
|
4317
|
+
messages: [
|
|
4318
|
+
{
|
|
4319
|
+
role: 'user',
|
|
4320
|
+
content: `Build an interactive HTML demo for this request:\n${prompt}`,
|
|
4321
|
+
},
|
|
4322
|
+
],
|
|
4323
|
+
});
|
|
4324
|
+
|
|
4325
|
+
const text = (response.content || [])
|
|
4326
|
+
.filter((c) => c.type === 'text')
|
|
4327
|
+
.map((c) => c.text)
|
|
4328
|
+
.join('\n');
|
|
4329
|
+
|
|
4330
|
+
return this.extractHtmlFromText(text);
|
|
4331
|
+
} catch (error) {
|
|
4332
|
+
console.error('[TaskExecutor] Failed to auto-generate canvas HTML:', error);
|
|
4333
|
+
return null;
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
|
|
3386
4337
|
/**
|
|
3387
4338
|
* Send a follow-up message to continue the conversation
|
|
3388
4339
|
*/
|
|
@@ -3391,8 +4342,13 @@ SCHEDULING & REMINDERS:
|
|
|
3391
4342
|
const shouldResumeAfterFollowup = previousStatus === 'paused' || this.waitingForUserInput;
|
|
3392
4343
|
const shouldStartNewCanvasSession = ['completed', 'failed', 'cancelled'].includes(previousStatus);
|
|
3393
4344
|
let resumeAttempted = false;
|
|
4345
|
+
let pausedForUserInput = false;
|
|
3394
4346
|
this.waitingForUserInput = false;
|
|
3395
4347
|
this.paused = false;
|
|
4348
|
+
this.lastUserMessage = message;
|
|
4349
|
+
if (shouldResumeAfterFollowup) {
|
|
4350
|
+
this.task.prompt = `${this.task.prompt}\n\nUSER UPDATE:\n${message}`;
|
|
4351
|
+
}
|
|
3396
4352
|
this.toolRegistry.setCanvasSessionCutoff(shouldStartNewCanvasSession ? Date.now() : null);
|
|
3397
4353
|
this.daemon.updateTaskStatus(this.task.id, 'executing');
|
|
3398
4354
|
this.daemon.logEvent(this.task.id, 'executing', { message: 'Processing follow-up message' });
|
|
@@ -3444,6 +4400,11 @@ IMPORTANT INSTRUCTIONS:
|
|
|
3444
4400
|
- The delete_file tool has a built-in approval mechanism that will prompt the user. Just call the tool directly.
|
|
3445
4401
|
- Do NOT ask "Should I proceed?" or wait for permission in text - the tools handle approvals automatically.
|
|
3446
4402
|
|
|
4403
|
+
USER INPUT GATE (CRITICAL):
|
|
4404
|
+
- If you ask the user for required information or a decision, STOP and wait.
|
|
4405
|
+
- Do NOT continue executing steps or call tools after asking such questions.
|
|
4406
|
+
- If safe defaults exist, state the assumption and proceed without asking.
|
|
4407
|
+
|
|
3447
4408
|
PATH DISCOVERY (CRITICAL):
|
|
3448
4409
|
- When a task mentions a folder or path (e.g., "electron/agent folder"), users often give PARTIAL paths.
|
|
3449
4410
|
- NEVER conclude a path doesn't exist without SEARCHING for it first.
|
|
@@ -3602,6 +4563,9 @@ SCHEDULING & REMINDERS:
|
|
|
3602
4563
|
// Compact messages if context is getting too large
|
|
3603
4564
|
messages = this.contextManager.compactMessages(messages, systemPromptTokens);
|
|
3604
4565
|
|
|
4566
|
+
const availableTools = this.getAvailableTools();
|
|
4567
|
+
const availableToolNames = new Set(availableTools.map(tool => tool.name));
|
|
4568
|
+
|
|
3605
4569
|
// Use retry wrapper for resilient API calls
|
|
3606
4570
|
const response = await this.callLLMWithRetry(
|
|
3607
4571
|
() => withTimeout(
|
|
@@ -3609,7 +4573,7 @@ SCHEDULING & REMINDERS:
|
|
|
3609
4573
|
model: this.modelId,
|
|
3610
4574
|
maxTokens: 4096,
|
|
3611
4575
|
system: this.systemPrompt,
|
|
3612
|
-
tools:
|
|
4576
|
+
tools: availableTools,
|
|
3613
4577
|
messages,
|
|
3614
4578
|
signal: this.abortController.signal,
|
|
3615
4579
|
}),
|
|
@@ -3630,6 +4594,10 @@ SCHEDULING & REMINDERS:
|
|
|
3630
4594
|
// Log any text responses from the assistant and check if asking a question
|
|
3631
4595
|
let assistantAskedQuestion = false;
|
|
3632
4596
|
let hasTextInThisResponse = false;
|
|
4597
|
+
const assistantText = (response.content || [])
|
|
4598
|
+
.filter((item: any) => item.type === 'text' && item.text)
|
|
4599
|
+
.map((item: any) => item.text)
|
|
4600
|
+
.join('\n');
|
|
3633
4601
|
if (response.content) {
|
|
3634
4602
|
for (const content of response.content) {
|
|
3635
4603
|
if (content.type === 'text' && content.text && content.text.trim().length > 0) {
|
|
@@ -3679,9 +4647,20 @@ SCHEDULING & REMINDERS:
|
|
|
3679
4647
|
const toolResults: LLMToolResult[] = [];
|
|
3680
4648
|
let hasDisabledToolAttempt = false;
|
|
3681
4649
|
let hasDuplicateToolAttempt = false;
|
|
4650
|
+
let hasUnavailableToolAttempt = false;
|
|
3682
4651
|
|
|
3683
4652
|
for (const content of response.content || []) {
|
|
3684
4653
|
if (content.type === 'tool_use') {
|
|
4654
|
+
// Normalize tool names like "functions.web_fetch" -> "web_fetch"
|
|
4655
|
+
const normalizedTool = this.normalizeToolName(content.name);
|
|
4656
|
+
if (normalizedTool.modified) {
|
|
4657
|
+
this.daemon.logEvent(this.task.id, 'parameter_inference', {
|
|
4658
|
+
tool: content.name,
|
|
4659
|
+
inference: `Normalized tool name "${normalizedTool.original}" -> "${normalizedTool.name}"`,
|
|
4660
|
+
});
|
|
4661
|
+
content.name = normalizedTool.name;
|
|
4662
|
+
}
|
|
4663
|
+
|
|
3685
4664
|
// Check if this tool is disabled (circuit breaker tripped)
|
|
3686
4665
|
if (this.toolFailureTracker.isDisabled(content.name)) {
|
|
3687
4666
|
const lastError = this.toolFailureTracker.getLastError(content.name);
|
|
@@ -3704,6 +4683,60 @@ SCHEDULING & REMINDERS:
|
|
|
3704
4683
|
continue;
|
|
3705
4684
|
}
|
|
3706
4685
|
|
|
4686
|
+
// Validate tool availability before attempting any inference
|
|
4687
|
+
if (!availableToolNames.has(content.name)) {
|
|
4688
|
+
console.log(`[TaskExecutor] Tool not available in this context: ${content.name}`);
|
|
4689
|
+
this.daemon.logEvent(this.task.id, 'tool_error', {
|
|
4690
|
+
tool: content.name,
|
|
4691
|
+
error: 'Tool not available in current context or permissions',
|
|
4692
|
+
blocked: true,
|
|
4693
|
+
});
|
|
4694
|
+
toolResults.push({
|
|
4695
|
+
type: 'tool_result',
|
|
4696
|
+
tool_use_id: content.id,
|
|
4697
|
+
content: JSON.stringify({
|
|
4698
|
+
error: `Tool "${content.name}" is not available in this context. Please choose a different tool or check permissions/integrations.`,
|
|
4699
|
+
unavailable: true,
|
|
4700
|
+
}),
|
|
4701
|
+
is_error: true,
|
|
4702
|
+
});
|
|
4703
|
+
hasUnavailableToolAttempt = true;
|
|
4704
|
+
continue;
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
// Infer missing parameters for weaker models (normalize inputs before deduplication)
|
|
4708
|
+
const inference = this.inferMissingParameters(content.name, content.input);
|
|
4709
|
+
if (inference.modified) {
|
|
4710
|
+
content.input = inference.input;
|
|
4711
|
+
this.daemon.logEvent(this.task.id, 'parameter_inference', {
|
|
4712
|
+
tool: content.name,
|
|
4713
|
+
inference: inference.inference,
|
|
4714
|
+
});
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
// If canvas_push is missing content, try extracting HTML from assistant text or auto-generate
|
|
4718
|
+
await this.handleCanvasPushFallback(content, assistantText);
|
|
4719
|
+
|
|
4720
|
+
const validationError = this.getToolInputValidationError(content.name, content.input);
|
|
4721
|
+
if (validationError) {
|
|
4722
|
+
this.daemon.logEvent(this.task.id, 'tool_warning', {
|
|
4723
|
+
tool: content.name,
|
|
4724
|
+
error: validationError,
|
|
4725
|
+
input: content.input,
|
|
4726
|
+
});
|
|
4727
|
+
toolResults.push({
|
|
4728
|
+
type: 'tool_result',
|
|
4729
|
+
tool_use_id: content.id,
|
|
4730
|
+
content: JSON.stringify({
|
|
4731
|
+
error: validationError,
|
|
4732
|
+
suggestion: 'Include all required fields in the tool call (e.g., content for create_document/write_file).',
|
|
4733
|
+
invalid_input: true,
|
|
4734
|
+
}),
|
|
4735
|
+
is_error: true,
|
|
4736
|
+
});
|
|
4737
|
+
continue;
|
|
4738
|
+
}
|
|
4739
|
+
|
|
3707
4740
|
// Check for duplicate tool calls (prevents stuck loops)
|
|
3708
4741
|
const duplicateCheck = this.toolCallDeduplicator.checkDuplicate(content.name, content.input);
|
|
3709
4742
|
if (duplicateCheck.isDuplicate) {
|
|
@@ -3775,16 +4808,6 @@ SCHEDULING & REMINDERS:
|
|
|
3775
4808
|
continue;
|
|
3776
4809
|
}
|
|
3777
4810
|
|
|
3778
|
-
// Infer missing parameters for weaker models
|
|
3779
|
-
const inference = this.inferMissingParameters(content.name, content.input);
|
|
3780
|
-
if (inference.modified) {
|
|
3781
|
-
content.input = inference.input;
|
|
3782
|
-
this.daemon.logEvent(this.task.id, 'parameter_inference', {
|
|
3783
|
-
tool: content.name,
|
|
3784
|
-
inference: inference.inference,
|
|
3785
|
-
});
|
|
3786
|
-
}
|
|
3787
|
-
|
|
3788
4811
|
this.daemon.logEvent(this.task.id, 'tool_call', {
|
|
3789
4812
|
tool: content.name,
|
|
3790
4813
|
input: content.input,
|
|
@@ -3792,12 +4815,13 @@ SCHEDULING & REMINDERS:
|
|
|
3792
4815
|
|
|
3793
4816
|
try {
|
|
3794
4817
|
// Execute tool with timeout to prevent hanging
|
|
4818
|
+
const toolTimeoutMs = this.getToolTimeoutMs(content.name, content.input);
|
|
3795
4819
|
const result = await withTimeout(
|
|
3796
4820
|
this.toolRegistry.executeTool(
|
|
3797
4821
|
content.name,
|
|
3798
4822
|
content.input as any
|
|
3799
4823
|
),
|
|
3800
|
-
|
|
4824
|
+
toolTimeoutMs,
|
|
3801
4825
|
`Tool ${content.name}`
|
|
3802
4826
|
);
|
|
3803
4827
|
|
|
@@ -3873,7 +4897,7 @@ SCHEDULING & REMINDERS:
|
|
|
3873
4897
|
|
|
3874
4898
|
// If all tool attempts were for disabled or duplicate tools, don't continue looping
|
|
3875
4899
|
const allToolsFailed = toolResults.every(r => r.is_error);
|
|
3876
|
-
if ((hasDisabledToolAttempt || hasDuplicateToolAttempt) && allToolsFailed) {
|
|
4900
|
+
if ((hasDisabledToolAttempt || hasDuplicateToolAttempt || hasUnavailableToolAttempt) && allToolsFailed) {
|
|
3877
4901
|
console.log('[TaskExecutor] All tool calls failed, were disabled, or duplicates - stopping iteration');
|
|
3878
4902
|
continueLoop = false;
|
|
3879
4903
|
} else {
|
|
@@ -3881,6 +4905,13 @@ SCHEDULING & REMINDERS:
|
|
|
3881
4905
|
}
|
|
3882
4906
|
}
|
|
3883
4907
|
|
|
4908
|
+
if (assistantAskedQuestion && shouldResumeAfterFollowup && this.shouldPauseForQuestions) {
|
|
4909
|
+
console.log('[TaskExecutor] Assistant asked a question during follow-up, pausing for user input');
|
|
4910
|
+
this.waitingForUserInput = true;
|
|
4911
|
+
pausedForUserInput = true;
|
|
4912
|
+
continueLoop = false;
|
|
4913
|
+
}
|
|
4914
|
+
|
|
3884
4915
|
// Check if agent wants to end but hasn't provided a text response yet
|
|
3885
4916
|
// If tools were called but no summary was given, request one
|
|
3886
4917
|
if (wantsToEnd && !hasTextInThisResponse && hadToolCalls && !hasProvidedTextResponse) {
|
|
@@ -3911,6 +4942,14 @@ SCHEDULING & REMINDERS:
|
|
|
3911
4942
|
message: 'Follow-up message processed',
|
|
3912
4943
|
});
|
|
3913
4944
|
|
|
4945
|
+
if (pausedForUserInput) {
|
|
4946
|
+
this.daemon.updateTaskStatus(this.task.id, 'paused');
|
|
4947
|
+
this.daemon.logEvent(this.task.id, 'task_paused', {
|
|
4948
|
+
message: 'Paused - awaiting user input',
|
|
4949
|
+
});
|
|
4950
|
+
return;
|
|
4951
|
+
}
|
|
4952
|
+
|
|
3914
4953
|
if (shouldResumeAfterFollowup && this.plan) {
|
|
3915
4954
|
resumeAttempted = true;
|
|
3916
4955
|
await this.resumeAfterPause();
|