cowork-os 0.3.21 → 0.3.23
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 +293 -6
- 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/daemon.js +25 -0
- package/dist/electron/electron/agent/executor.js +181 -26
- package/dist/electron/electron/agent/llm/anthropic-compatible-provider.js +177 -0
- 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 +11 -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 +318 -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/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/google-drive-tools.js +227 -0
- 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 +541 -0
- 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/gateway/index.js +1 -0
- package/dist/electron/electron/gateway/router.js +123 -143
- package/dist/electron/electron/ipc/canvas-handlers.js +5 -0
- package/dist/electron/electron/ipc/handlers.js +627 -158
- 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 +1 -1
- package/dist/electron/electron/preload.js +74 -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 +82 -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 +88 -1
- package/package.json +12 -2
- package/src/electron/agent/executor.ts +205 -28
- package/src/electron/agent/llm/anthropic-compatible-provider.ts +214 -0
- 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 +5 -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 +414 -6
- package/src/electron/agent/llm/types.ts +90 -1
- package/src/electron/agent/llm/xai-provider.ts +39 -0
- package/src/electron/agent/tools/box-tools.ts +239 -0
- package/src/electron/agent/tools/builtin-settings.ts +34 -0
- package/src/electron/agent/tools/dropbox-tools.ts +237 -0
- package/src/electron/agent/tools/google-drive-tools.ts +228 -0
- 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 +565 -0
- 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/database/SecureSettingsRepository.ts +7 -1
- package/src/electron/gateway/index.ts +1 -0
- package/src/electron/gateway/router.ts +134 -149
- package/src/electron/ipc/canvas-handlers.ts +10 -0
- package/src/electron/ipc/handlers.ts +673 -153
- 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 +5 -1
- package/src/electron/preload.ts +167 -4
- package/src/electron/settings/box-manager.ts +58 -0
- package/src/electron/settings/dropbox-manager.ts +58 -0
- package/src/electron/settings/google-drive-manager.ts +58 -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/google-drive-api.ts +183 -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 +102 -1
- package/src/electron/utils/x-cli.ts +1 -1
- package/src/renderer/App.tsx +20 -2
- 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/DropboxSettings.tsx +202 -0
- package/src/renderer/components/GoogleDriveSettings.tsx +201 -0
- package/src/renderer/components/MCPSettings.tsx +56 -0
- package/src/renderer/components/MainContent.tsx +270 -34
- 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/Settings.tsx +611 -8
- package/src/renderer/components/SharePointSettings.tsx +224 -0
- package/src/renderer/components/Sidebar.tsx +25 -9
- package/src/renderer/hooks/useOnboardingFlow.ts +21 -0
- package/src/renderer/styles/index.css +438 -25
- package/src/shared/channelMessages.ts +367 -4
- package/src/shared/llm-provider-catalog.ts +217 -0
- package/src/shared/types.ts +226 -1
|
@@ -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
|
|
@@ -1014,6 +1016,7 @@ export class TaskExecutor {
|
|
|
1014
1016
|
private modelKey: string;
|
|
1015
1017
|
private conversationHistory: LLMMessage[] = [];
|
|
1016
1018
|
private systemPrompt: string = '';
|
|
1019
|
+
private lastUserMessage: string;
|
|
1017
1020
|
|
|
1018
1021
|
// Plan revision tracking to prevent infinite revision loops
|
|
1019
1022
|
private planRevisionCount: number = 0;
|
|
@@ -1040,6 +1043,7 @@ export class TaskExecutor {
|
|
|
1040
1043
|
private workspace: Workspace,
|
|
1041
1044
|
private daemon: AgentDaemon
|
|
1042
1045
|
) {
|
|
1046
|
+
this.lastUserMessage = task.prompt;
|
|
1043
1047
|
this.requiresTestRun = this.detectTestRequirement(`${task.title}\n${task.prompt}`);
|
|
1044
1048
|
// Get base settings
|
|
1045
1049
|
const settings = LLMProviderFactory.loadSettings();
|
|
@@ -1062,7 +1066,11 @@ export class TaskExecutor {
|
|
|
1062
1066
|
settings.ollama?.model,
|
|
1063
1067
|
settings.gemini?.model,
|
|
1064
1068
|
settings.openrouter?.model,
|
|
1065
|
-
settings.openai?.model
|
|
1069
|
+
settings.openai?.model,
|
|
1070
|
+
settings.groq?.model,
|
|
1071
|
+
settings.xai?.model,
|
|
1072
|
+
settings.kimi?.model,
|
|
1073
|
+
settings.customProviders
|
|
1066
1074
|
);
|
|
1067
1075
|
this.modelKey = effectiveModelKey;
|
|
1068
1076
|
|
|
@@ -1220,6 +1228,23 @@ export class TaskExecutor {
|
|
|
1220
1228
|
this.globalTurnCount++; // Track global turns across all steps
|
|
1221
1229
|
}
|
|
1222
1230
|
|
|
1231
|
+
private getToolTimeoutMs(toolName: string, input: unknown): number {
|
|
1232
|
+
const settingsTimeout = BuiltinToolsSettingsManager.getToolTimeoutMs(toolName);
|
|
1233
|
+
const normalizedSettingsTimeout = settingsTimeout && settingsTimeout > 0 ? settingsTimeout : null;
|
|
1234
|
+
|
|
1235
|
+
if (toolName === 'run_command') {
|
|
1236
|
+
const inputTimeout = typeof (input as { timeout?: unknown })?.timeout === 'number'
|
|
1237
|
+
? (input as { timeout?: number }).timeout
|
|
1238
|
+
: undefined;
|
|
1239
|
+
if (typeof inputTimeout === 'number' && Number.isFinite(inputTimeout) && inputTimeout > 0) {
|
|
1240
|
+
return Math.round(inputTimeout);
|
|
1241
|
+
}
|
|
1242
|
+
return normalizedSettingsTimeout ?? TOOL_TIMEOUT_MS;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
return normalizedSettingsTimeout ?? TOOL_TIMEOUT_MS;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1223
1248
|
/**
|
|
1224
1249
|
* Check if a file operation should be blocked (redundant read or duplicate creation)
|
|
1225
1250
|
* @returns Object with blocked flag, reason, and suggestion if blocked, plus optional cached result
|
|
@@ -1461,6 +1486,37 @@ export class TaskExecutor {
|
|
|
1461
1486
|
return { input, modified: false };
|
|
1462
1487
|
}
|
|
1463
1488
|
|
|
1489
|
+
private async handleCanvasPushFallback(content: LLMToolUse, assistantText: string): Promise<void> {
|
|
1490
|
+
if (content.name !== 'canvas_push') {
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
const inputContent = content.input?.content;
|
|
1495
|
+
const hasContent = typeof inputContent === 'string' && inputContent.trim().length > 0;
|
|
1496
|
+
const filename = content.input?.filename;
|
|
1497
|
+
const isHtmlTarget = !filename || filename === 'index.html';
|
|
1498
|
+
if (hasContent || !isHtmlTarget) {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const extracted = this.extractHtmlFromText(assistantText);
|
|
1503
|
+
const generated = extracted || await this.generateCanvasHtml(this.lastUserMessage || this.task.prompt);
|
|
1504
|
+
if (!generated) {
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
content.input = {
|
|
1509
|
+
...(content.input || {}),
|
|
1510
|
+
content: generated,
|
|
1511
|
+
};
|
|
1512
|
+
this.daemon.logEvent(this.task.id, 'parameter_inference', {
|
|
1513
|
+
tool: content.name,
|
|
1514
|
+
inference: extracted
|
|
1515
|
+
? 'Recovered HTML from assistant text'
|
|
1516
|
+
: 'Auto-generated HTML from latest user request',
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1464
1520
|
/**
|
|
1465
1521
|
* Get available tools, filtering out disabled ones
|
|
1466
1522
|
* This prevents the LLM from trying to use tools that have been disabled by the circuit breaker
|
|
@@ -2954,6 +3010,8 @@ SCHEDULING & REMINDERS:
|
|
|
2954
3010
|
// Compact messages if context is getting too large
|
|
2955
3011
|
messages = this.contextManager.compactMessages(messages, systemPromptTokens);
|
|
2956
3012
|
|
|
3013
|
+
const availableTools = this.getAvailableTools();
|
|
3014
|
+
|
|
2957
3015
|
// Use retry wrapper for resilient API calls
|
|
2958
3016
|
const response = await this.callLLMWithRetry(
|
|
2959
3017
|
() => withTimeout(
|
|
@@ -2961,7 +3019,7 @@ SCHEDULING & REMINDERS:
|
|
|
2961
3019
|
model: this.modelId,
|
|
2962
3020
|
maxTokens: 4096,
|
|
2963
3021
|
system: this.systemPrompt,
|
|
2964
|
-
tools:
|
|
3022
|
+
tools: availableTools,
|
|
2965
3023
|
messages,
|
|
2966
3024
|
signal: this.abortController.signal,
|
|
2967
3025
|
}),
|
|
@@ -2984,6 +3042,10 @@ SCHEDULING & REMINDERS:
|
|
|
2984
3042
|
|
|
2985
3043
|
// Log any text responses from the assistant and check if asking a question
|
|
2986
3044
|
let assistantAskedQuestion = false;
|
|
3045
|
+
const assistantText = (response.content || [])
|
|
3046
|
+
.filter((item: any) => item.type === 'text' && item.text)
|
|
3047
|
+
.map((item: any) => item.text)
|
|
3048
|
+
.join('\n');
|
|
2987
3049
|
if (response.content) {
|
|
2988
3050
|
for (const content of response.content) {
|
|
2989
3051
|
if (content.type === 'text' && content.text) {
|
|
@@ -3031,6 +3093,8 @@ SCHEDULING & REMINDERS:
|
|
|
3031
3093
|
const toolResults: LLMToolResult[] = [];
|
|
3032
3094
|
let hasDisabledToolAttempt = false;
|
|
3033
3095
|
let hasDuplicateToolAttempt = false;
|
|
3096
|
+
let hasUnavailableToolAttempt = false;
|
|
3097
|
+
const availableToolNames = new Set(availableTools.map(tool => tool.name));
|
|
3034
3098
|
|
|
3035
3099
|
for (const content of response.content || []) {
|
|
3036
3100
|
if (content.type === 'tool_use') {
|
|
@@ -3056,6 +3120,40 @@ SCHEDULING & REMINDERS:
|
|
|
3056
3120
|
continue;
|
|
3057
3121
|
}
|
|
3058
3122
|
|
|
3123
|
+
// Validate tool availability before attempting any inference
|
|
3124
|
+
if (!availableToolNames.has(content.name)) {
|
|
3125
|
+
console.log(`[TaskExecutor] Tool not available in this context: ${content.name}`);
|
|
3126
|
+
this.daemon.logEvent(this.task.id, 'tool_error', {
|
|
3127
|
+
tool: content.name,
|
|
3128
|
+
error: 'Tool not available in current context or permissions',
|
|
3129
|
+
blocked: true,
|
|
3130
|
+
});
|
|
3131
|
+
toolResults.push({
|
|
3132
|
+
type: 'tool_result',
|
|
3133
|
+
tool_use_id: content.id,
|
|
3134
|
+
content: JSON.stringify({
|
|
3135
|
+
error: `Tool "${content.name}" is not available in this context. Please choose a different tool or check permissions/integrations.`,
|
|
3136
|
+
unavailable: true,
|
|
3137
|
+
}),
|
|
3138
|
+
is_error: true,
|
|
3139
|
+
});
|
|
3140
|
+
hasUnavailableToolAttempt = true;
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
// Infer missing parameters for weaker models (normalize inputs before deduplication)
|
|
3145
|
+
const inference = this.inferMissingParameters(content.name, content.input);
|
|
3146
|
+
if (inference.modified) {
|
|
3147
|
+
content.input = inference.input;
|
|
3148
|
+
this.daemon.logEvent(this.task.id, 'parameter_inference', {
|
|
3149
|
+
tool: content.name,
|
|
3150
|
+
inference: inference.inference,
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
// If canvas_push is missing content, try extracting HTML from assistant text or auto-generate
|
|
3155
|
+
await this.handleCanvasPushFallback(content, assistantText);
|
|
3156
|
+
|
|
3059
3157
|
// Check for duplicate tool calls (prevents stuck loops)
|
|
3060
3158
|
const duplicateCheck = this.toolCallDeduplicator.checkDuplicate(content.name, content.input);
|
|
3061
3159
|
if (duplicateCheck.isDuplicate) {
|
|
@@ -3129,16 +3227,6 @@ SCHEDULING & REMINDERS:
|
|
|
3129
3227
|
continue;
|
|
3130
3228
|
}
|
|
3131
3229
|
|
|
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
3230
|
this.daemon.logEvent(this.task.id, 'tool_call', {
|
|
3143
3231
|
tool: content.name,
|
|
3144
3232
|
input: content.input,
|
|
@@ -3146,12 +3234,13 @@ SCHEDULING & REMINDERS:
|
|
|
3146
3234
|
|
|
3147
3235
|
try {
|
|
3148
3236
|
// Execute tool with timeout to prevent hanging
|
|
3237
|
+
const toolTimeoutMs = this.getToolTimeoutMs(content.name, content.input);
|
|
3149
3238
|
const result = await withTimeout(
|
|
3150
3239
|
this.toolRegistry.executeTool(
|
|
3151
3240
|
content.name,
|
|
3152
3241
|
content.input as any
|
|
3153
3242
|
),
|
|
3154
|
-
|
|
3243
|
+
toolTimeoutMs,
|
|
3155
3244
|
`Tool ${content.name}`
|
|
3156
3245
|
);
|
|
3157
3246
|
|
|
@@ -3266,7 +3355,7 @@ SCHEDULING & REMINDERS:
|
|
|
3266
3355
|
// If all tool attempts were for disabled or duplicate tools, don't continue looping
|
|
3267
3356
|
// This prevents infinite retry loops
|
|
3268
3357
|
const allToolsFailed = toolResults.every(r => r.is_error);
|
|
3269
|
-
if ((hasDisabledToolAttempt || hasDuplicateToolAttempt) && allToolsFailed) {
|
|
3358
|
+
if ((hasDisabledToolAttempt || hasDuplicateToolAttempt || hasUnavailableToolAttempt) && allToolsFailed) {
|
|
3270
3359
|
console.log('[TaskExecutor] All tool calls failed, were disabled, or duplicates - stopping iteration');
|
|
3271
3360
|
if (hasDuplicateToolAttempt) {
|
|
3272
3361
|
// Duplicate detection triggered - step is likely complete
|
|
@@ -3383,6 +3472,60 @@ SCHEDULING & REMINDERS:
|
|
|
3383
3472
|
}
|
|
3384
3473
|
}
|
|
3385
3474
|
|
|
3475
|
+
private extractHtmlFromText(text: string): string | null {
|
|
3476
|
+
if (!text) return null;
|
|
3477
|
+
const fenceMatch = text.match(/```html([\s\S]*?)```/i);
|
|
3478
|
+
const raw = fenceMatch ? fenceMatch[1].trim() : text;
|
|
3479
|
+
const doctypeIndex = raw.indexOf('<!DOCTYPE html');
|
|
3480
|
+
if (doctypeIndex >= 0) {
|
|
3481
|
+
const endIndex = raw.lastIndexOf('</html>');
|
|
3482
|
+
if (endIndex > doctypeIndex) {
|
|
3483
|
+
return raw.slice(doctypeIndex, endIndex + '</html>'.length).trim();
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
const htmlIndex = raw.indexOf('<html');
|
|
3487
|
+
if (htmlIndex >= 0) {
|
|
3488
|
+
const endIndex = raw.lastIndexOf('</html>');
|
|
3489
|
+
if (endIndex > htmlIndex) {
|
|
3490
|
+
return raw.slice(htmlIndex, endIndex + '</html>'.length).trim();
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
return null;
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
private async generateCanvasHtml(prompt: string): Promise<string | null> {
|
|
3497
|
+
const system = [
|
|
3498
|
+
'You generate a single self-contained HTML document for an in-app canvas.',
|
|
3499
|
+
'Output ONLY the HTML document (no markdown, no commentary).',
|
|
3500
|
+
'Use inline CSS and JS. Do not reference external assets or remote URLs.',
|
|
3501
|
+
'Keep it reasonably compact and interactive where appropriate.',
|
|
3502
|
+
].join(' ');
|
|
3503
|
+
|
|
3504
|
+
try {
|
|
3505
|
+
const response = await this.provider.createMessage({
|
|
3506
|
+
model: this.modelId,
|
|
3507
|
+
maxTokens: 1800,
|
|
3508
|
+
system,
|
|
3509
|
+
messages: [
|
|
3510
|
+
{
|
|
3511
|
+
role: 'user',
|
|
3512
|
+
content: `Build an interactive HTML demo for this request:\n${prompt}`,
|
|
3513
|
+
},
|
|
3514
|
+
],
|
|
3515
|
+
});
|
|
3516
|
+
|
|
3517
|
+
const text = (response.content || [])
|
|
3518
|
+
.filter((c) => c.type === 'text')
|
|
3519
|
+
.map((c) => c.text)
|
|
3520
|
+
.join('\n');
|
|
3521
|
+
|
|
3522
|
+
return this.extractHtmlFromText(text);
|
|
3523
|
+
} catch (error) {
|
|
3524
|
+
console.error('[TaskExecutor] Failed to auto-generate canvas HTML:', error);
|
|
3525
|
+
return null;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3386
3529
|
/**
|
|
3387
3530
|
* Send a follow-up message to continue the conversation
|
|
3388
3531
|
*/
|
|
@@ -3393,6 +3536,7 @@ SCHEDULING & REMINDERS:
|
|
|
3393
3536
|
let resumeAttempted = false;
|
|
3394
3537
|
this.waitingForUserInput = false;
|
|
3395
3538
|
this.paused = false;
|
|
3539
|
+
this.lastUserMessage = message;
|
|
3396
3540
|
this.toolRegistry.setCanvasSessionCutoff(shouldStartNewCanvasSession ? Date.now() : null);
|
|
3397
3541
|
this.daemon.updateTaskStatus(this.task.id, 'executing');
|
|
3398
3542
|
this.daemon.logEvent(this.task.id, 'executing', { message: 'Processing follow-up message' });
|
|
@@ -3602,6 +3746,9 @@ SCHEDULING & REMINDERS:
|
|
|
3602
3746
|
// Compact messages if context is getting too large
|
|
3603
3747
|
messages = this.contextManager.compactMessages(messages, systemPromptTokens);
|
|
3604
3748
|
|
|
3749
|
+
const availableTools = this.getAvailableTools();
|
|
3750
|
+
const availableToolNames = new Set(availableTools.map(tool => tool.name));
|
|
3751
|
+
|
|
3605
3752
|
// Use retry wrapper for resilient API calls
|
|
3606
3753
|
const response = await this.callLLMWithRetry(
|
|
3607
3754
|
() => withTimeout(
|
|
@@ -3609,7 +3756,7 @@ SCHEDULING & REMINDERS:
|
|
|
3609
3756
|
model: this.modelId,
|
|
3610
3757
|
maxTokens: 4096,
|
|
3611
3758
|
system: this.systemPrompt,
|
|
3612
|
-
tools:
|
|
3759
|
+
tools: availableTools,
|
|
3613
3760
|
messages,
|
|
3614
3761
|
signal: this.abortController.signal,
|
|
3615
3762
|
}),
|
|
@@ -3630,6 +3777,10 @@ SCHEDULING & REMINDERS:
|
|
|
3630
3777
|
// Log any text responses from the assistant and check if asking a question
|
|
3631
3778
|
let assistantAskedQuestion = false;
|
|
3632
3779
|
let hasTextInThisResponse = false;
|
|
3780
|
+
const assistantText = (response.content || [])
|
|
3781
|
+
.filter((item: any) => item.type === 'text' && item.text)
|
|
3782
|
+
.map((item: any) => item.text)
|
|
3783
|
+
.join('\n');
|
|
3633
3784
|
if (response.content) {
|
|
3634
3785
|
for (const content of response.content) {
|
|
3635
3786
|
if (content.type === 'text' && content.text && content.text.trim().length > 0) {
|
|
@@ -3679,6 +3830,7 @@ SCHEDULING & REMINDERS:
|
|
|
3679
3830
|
const toolResults: LLMToolResult[] = [];
|
|
3680
3831
|
let hasDisabledToolAttempt = false;
|
|
3681
3832
|
let hasDuplicateToolAttempt = false;
|
|
3833
|
+
let hasUnavailableToolAttempt = false;
|
|
3682
3834
|
|
|
3683
3835
|
for (const content of response.content || []) {
|
|
3684
3836
|
if (content.type === 'tool_use') {
|
|
@@ -3704,6 +3856,40 @@ SCHEDULING & REMINDERS:
|
|
|
3704
3856
|
continue;
|
|
3705
3857
|
}
|
|
3706
3858
|
|
|
3859
|
+
// Validate tool availability before attempting any inference
|
|
3860
|
+
if (!availableToolNames.has(content.name)) {
|
|
3861
|
+
console.log(`[TaskExecutor] Tool not available in this context: ${content.name}`);
|
|
3862
|
+
this.daemon.logEvent(this.task.id, 'tool_error', {
|
|
3863
|
+
tool: content.name,
|
|
3864
|
+
error: 'Tool not available in current context or permissions',
|
|
3865
|
+
blocked: true,
|
|
3866
|
+
});
|
|
3867
|
+
toolResults.push({
|
|
3868
|
+
type: 'tool_result',
|
|
3869
|
+
tool_use_id: content.id,
|
|
3870
|
+
content: JSON.stringify({
|
|
3871
|
+
error: `Tool "${content.name}" is not available in this context. Please choose a different tool or check permissions/integrations.`,
|
|
3872
|
+
unavailable: true,
|
|
3873
|
+
}),
|
|
3874
|
+
is_error: true,
|
|
3875
|
+
});
|
|
3876
|
+
hasUnavailableToolAttempt = true;
|
|
3877
|
+
continue;
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
// Infer missing parameters for weaker models (normalize inputs before deduplication)
|
|
3881
|
+
const inference = this.inferMissingParameters(content.name, content.input);
|
|
3882
|
+
if (inference.modified) {
|
|
3883
|
+
content.input = inference.input;
|
|
3884
|
+
this.daemon.logEvent(this.task.id, 'parameter_inference', {
|
|
3885
|
+
tool: content.name,
|
|
3886
|
+
inference: inference.inference,
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
// If canvas_push is missing content, try extracting HTML from assistant text or auto-generate
|
|
3891
|
+
await this.handleCanvasPushFallback(content, assistantText);
|
|
3892
|
+
|
|
3707
3893
|
// Check for duplicate tool calls (prevents stuck loops)
|
|
3708
3894
|
const duplicateCheck = this.toolCallDeduplicator.checkDuplicate(content.name, content.input);
|
|
3709
3895
|
if (duplicateCheck.isDuplicate) {
|
|
@@ -3775,16 +3961,6 @@ SCHEDULING & REMINDERS:
|
|
|
3775
3961
|
continue;
|
|
3776
3962
|
}
|
|
3777
3963
|
|
|
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
3964
|
this.daemon.logEvent(this.task.id, 'tool_call', {
|
|
3789
3965
|
tool: content.name,
|
|
3790
3966
|
input: content.input,
|
|
@@ -3792,12 +3968,13 @@ SCHEDULING & REMINDERS:
|
|
|
3792
3968
|
|
|
3793
3969
|
try {
|
|
3794
3970
|
// Execute tool with timeout to prevent hanging
|
|
3971
|
+
const toolTimeoutMs = this.getToolTimeoutMs(content.name, content.input);
|
|
3795
3972
|
const result = await withTimeout(
|
|
3796
3973
|
this.toolRegistry.executeTool(
|
|
3797
3974
|
content.name,
|
|
3798
3975
|
content.input as any
|
|
3799
3976
|
),
|
|
3800
|
-
|
|
3977
|
+
toolTimeoutMs,
|
|
3801
3978
|
`Tool ${content.name}`
|
|
3802
3979
|
);
|
|
3803
3980
|
|
|
@@ -3873,7 +4050,7 @@ SCHEDULING & REMINDERS:
|
|
|
3873
4050
|
|
|
3874
4051
|
// If all tool attempts were for disabled or duplicate tools, don't continue looping
|
|
3875
4052
|
const allToolsFailed = toolResults.every(r => r.is_error);
|
|
3876
|
-
if ((hasDisabledToolAttempt || hasDuplicateToolAttempt) && allToolsFailed) {
|
|
4053
|
+
if ((hasDisabledToolAttempt || hasDuplicateToolAttempt || hasUnavailableToolAttempt) && allToolsFailed) {
|
|
3877
4054
|
console.log('[TaskExecutor] All tool calls failed, were disabled, or duplicates - stopping iteration');
|
|
3878
4055
|
continueLoop = false;
|
|
3879
4056
|
} else {
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LLMProvider,
|
|
3
|
+
LLMProviderType,
|
|
4
|
+
LLMRequest,
|
|
5
|
+
LLMResponse,
|
|
6
|
+
LLMContent,
|
|
7
|
+
LLMMessage,
|
|
8
|
+
LLMTool,
|
|
9
|
+
} from './types';
|
|
10
|
+
|
|
11
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
12
|
+
|
|
13
|
+
function joinUrl(baseUrl: string, path: string): string {
|
|
14
|
+
const trimmedBase = baseUrl.replace(/\/+$/, '');
|
|
15
|
+
const trimmedPath = path.startsWith('/') ? path : `/${path}`;
|
|
16
|
+
return `${trimmedBase}${trimmedPath}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AnthropicCompatibleProviderOptions {
|
|
20
|
+
type: LLMProviderType;
|
|
21
|
+
providerName: string;
|
|
22
|
+
apiKey: string;
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
defaultModel: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class AnthropicCompatibleProvider implements LLMProvider {
|
|
28
|
+
readonly type: LLMProviderType;
|
|
29
|
+
private apiKey: string;
|
|
30
|
+
private baseUrl: string;
|
|
31
|
+
private defaultModel: string;
|
|
32
|
+
private providerName: string;
|
|
33
|
+
|
|
34
|
+
constructor(options: AnthropicCompatibleProviderOptions) {
|
|
35
|
+
this.type = options.type;
|
|
36
|
+
this.apiKey = options.apiKey;
|
|
37
|
+
this.baseUrl = options.baseUrl;
|
|
38
|
+
this.defaultModel = options.defaultModel;
|
|
39
|
+
this.providerName = options.providerName;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async createMessage(request: LLMRequest): Promise<LLMResponse> {
|
|
43
|
+
const messages = this.convertMessages(request.messages);
|
|
44
|
+
const tools = request.tools ? this.convertTools(request.tools) : undefined;
|
|
45
|
+
const model = request.model || this.defaultModel;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
console.log(`[${this.providerName}] Calling API with model: ${model}`);
|
|
49
|
+
const response = await fetch(joinUrl(this.baseUrl, '/messages'), {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'x-api-key': this.apiKey,
|
|
54
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
model,
|
|
58
|
+
max_tokens: request.maxTokens,
|
|
59
|
+
system: request.system,
|
|
60
|
+
messages,
|
|
61
|
+
...(tools && { tools }),
|
|
62
|
+
}),
|
|
63
|
+
signal: request.signal,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
|
|
68
|
+
throw new Error(
|
|
69
|
+
`${this.providerName} API error: ${response.status} ${response.statusText}` +
|
|
70
|
+
(errorData.error?.message ? ` - ${errorData.error.message}` : '')
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const data = await response.json() as any;
|
|
75
|
+
return this.convertResponse(data);
|
|
76
|
+
} catch (error: any) {
|
|
77
|
+
if (error.name === 'AbortError' || error.message?.includes('aborted')) {
|
|
78
|
+
console.log(`[${this.providerName}] Request aborted`);
|
|
79
|
+
throw new Error('Request cancelled');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.error(`[${this.providerName}] API error:`, {
|
|
83
|
+
message: error.message,
|
|
84
|
+
status: error.status,
|
|
85
|
+
});
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async testConnection(): Promise<{ success: boolean; error?: string }> {
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetch(joinUrl(this.baseUrl, '/messages'), {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
'x-api-key': this.apiKey,
|
|
97
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
98
|
+
},
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
model: this.defaultModel,
|
|
101
|
+
max_tokens: 10,
|
|
102
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const errorData = await response.json().catch(() => ({})) as { error?: { message?: string } };
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { success: true };
|
|
115
|
+
} catch (error: any) {
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
error: error.message || `Failed to connect to ${this.providerName} API`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private convertMessages(messages: LLMMessage[]): Array<{ role: string; content: any }> {
|
|
124
|
+
return messages.map((msg) => {
|
|
125
|
+
if (typeof msg.content === 'string') {
|
|
126
|
+
return {
|
|
127
|
+
role: msg.role,
|
|
128
|
+
content: msg.content,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const content = msg.content.map((item) => {
|
|
133
|
+
if (item.type === 'tool_result') {
|
|
134
|
+
return {
|
|
135
|
+
type: 'tool_result' as const,
|
|
136
|
+
tool_use_id: item.tool_use_id,
|
|
137
|
+
content: item.content,
|
|
138
|
+
...(item.is_error && { is_error: true }),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (item.type === 'tool_use') {
|
|
142
|
+
return {
|
|
143
|
+
type: 'tool_use' as const,
|
|
144
|
+
id: item.id,
|
|
145
|
+
name: item.name,
|
|
146
|
+
input: item.input,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
type: 'text' as const,
|
|
151
|
+
text: item.text,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
role: msg.role,
|
|
157
|
+
content,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private convertTools(tools: LLMTool[]): Array<{ name: string; description: string; input_schema: any }> {
|
|
163
|
+
return tools.map((tool) => ({
|
|
164
|
+
name: tool.name,
|
|
165
|
+
description: tool.description,
|
|
166
|
+
input_schema: tool.input_schema,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private convertResponse(response: any): LLMResponse {
|
|
171
|
+
const content: LLMContent[] = (response.content || [])
|
|
172
|
+
.filter((block: any) => block.type === 'text' || block.type === 'tool_use')
|
|
173
|
+
.map((block: any) => {
|
|
174
|
+
if (block.type === 'tool_use') {
|
|
175
|
+
return {
|
|
176
|
+
type: 'tool_use' as const,
|
|
177
|
+
id: block.id,
|
|
178
|
+
name: block.name,
|
|
179
|
+
input: block.input as Record<string, any>,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
type: 'text' as const,
|
|
184
|
+
text: block.text || '',
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
content: content.length > 0 ? content : [{ type: 'text', text: '' }],
|
|
190
|
+
stopReason: this.mapStopReason(response.stop_reason),
|
|
191
|
+
usage: response.usage
|
|
192
|
+
? {
|
|
193
|
+
inputTokens: response.usage.input_tokens || 0,
|
|
194
|
+
outputTokens: response.usage.output_tokens || 0,
|
|
195
|
+
}
|
|
196
|
+
: undefined,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private mapStopReason(reason?: string): LLMResponse['stopReason'] {
|
|
201
|
+
switch (reason) {
|
|
202
|
+
case 'end_turn':
|
|
203
|
+
return 'end_turn';
|
|
204
|
+
case 'tool_use':
|
|
205
|
+
return 'tool_use';
|
|
206
|
+
case 'max_tokens':
|
|
207
|
+
return 'max_tokens';
|
|
208
|
+
case 'stop_sequence':
|
|
209
|
+
return 'stop_sequence';
|
|
210
|
+
default:
|
|
211
|
+
return 'end_turn';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|