neoagent 2.3.1-beta.27 → 2.3.1-beta.28
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/package.json +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/services/ai/tools.js +71 -3
- package/server/services/integrations/figma/provider.js +1 -0
- package/server/services/integrations/google/provider.js +1 -0
- package/server/services/integrations/home_assistant/provider.js +1 -0
- package/server/services/integrations/manager.js +23 -5
- package/server/services/integrations/microsoft/provider.js +1 -0
- package/server/services/integrations/oauth_provider.js +1 -0
- package/server/services/integrations/spotify/provider.js +1 -0
- package/server/services/tasks/runtime.js +1 -1
package/package.json
CHANGED
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"59aa584fdf100e6c78c785d8a5b565d1de4b48
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "2330022069" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -192,6 +192,54 @@ function isProactiveTrigger(triggerSource) {
|
|
|
192
192
|
return triggerSource === 'schedule' || triggerSource === 'tasks';
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
function normalizeSendMessagePurpose(value) {
|
|
196
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
197
|
+
if (normalized === 'final_result' || normalized === 'blocker' || normalized === 'no_response') {
|
|
198
|
+
return normalized;
|
|
199
|
+
}
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function validateProactiveSendMessageArgs({ purpose, normalizedMessage }) {
|
|
204
|
+
const normalizedPurpose = normalizeSendMessagePurpose(purpose);
|
|
205
|
+
if (!normalizedPurpose) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
error: 'Background send_message requires purpose=final_result, blocker, or no_response.',
|
|
209
|
+
reason: 'Background send_message requires purpose=final_result, blocker, or no_response.',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (normalizedPurpose === 'no_response') {
|
|
214
|
+
if (normalizedMessage !== '[NO RESPONSE]') {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
error: 'purpose=no_response requires content "[NO RESPONSE]".',
|
|
218
|
+
reason: 'purpose=no_response requires content "[NO RESPONSE]".',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
skipped: true,
|
|
224
|
+
suppressed: true,
|
|
225
|
+
reason: 'no_response',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (normalizedMessage === '[NO RESPONSE]') {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
error: `purpose=${normalizedPurpose} cannot use content "[NO RESPONSE]".`,
|
|
233
|
+
reason: `purpose=${normalizedPurpose} cannot use content "[NO RESPONSE]".`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
ok: true,
|
|
239
|
+
purpose: normalizedPurpose,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
195
243
|
function getRunState(engine, runId) {
|
|
196
244
|
if (!engine || !runId) return null;
|
|
197
245
|
return engine.activeRuns.get(runId) || null;
|
|
@@ -742,14 +790,15 @@ function getAvailableTools(app, options = {}) {
|
|
|
742
790
|
},
|
|
743
791
|
{
|
|
744
792
|
name: 'send_message',
|
|
745
|
-
description: `Send a message on a connected messaging platform. Supports WhatsApp (text/media), Telnyx Voice (phone calls — TTS), Discord, Telegram, Slack, Google Chat, Microsoft Teams, Matrix, Signal, iMessage/BlueBubbles, IRC, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, WeChat, WebChat, and configurable webhook bridges. ${buildSendMessageFormattingReference()} For WhatsApp: use media_path to attach files. Use content "[NO RESPONSE]" only when the user explicitly asked for silence or no reply.`,
|
|
793
|
+
description: `Send a message on a connected messaging platform. Supports WhatsApp (text/media), Telnyx Voice (phone calls — TTS), Discord, Telegram, Slack, Google Chat, Microsoft Teams, Matrix, Signal, iMessage/BlueBubbles, IRC, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, WeChat, WebChat, and configurable webhook bridges. ${buildSendMessageFormattingReference()} For WhatsApp: use media_path to attach files. Use content "[NO RESPONSE]" only when the user explicitly asked for silence or no reply. For background task or schedule runs, set purpose to final_result, blocker, or no_response.`,
|
|
746
794
|
parameters: {
|
|
747
795
|
type: 'object',
|
|
748
796
|
properties: {
|
|
749
797
|
platform: { type: 'string', description: 'Platform name, for example whatsapp, telnyx, discord, telegram, slack, google_chat, teams, matrix, signal, imessage, bluebubbles, irc, line, mattermost, or webchat' },
|
|
750
798
|
to: { type: 'string', description: 'Recipient/chat ID for the connected platform, such as a WhatsApp chat ID, Telnyx call_control_id, Slack channel ID, Matrix room ID, Discord channel snowflake / "dm_<userId>", Telegram "dm_<userId>" / raw group chat ID, IRC channel, or webhook target' },
|
|
751
799
|
content: { type: 'string', description: 'Message text. Write one compact natural chat reply; the runtime adapts final formatting for the destination platform.' },
|
|
752
|
-
media_path: { type: 'string', description: 'WhatsApp only: absolute path to a local file to attach. Leave empty for text-only or Telnyx.' }
|
|
800
|
+
media_path: { type: 'string', description: 'WhatsApp only: absolute path to a local file to attach. Leave empty for text-only or Telnyx.' },
|
|
801
|
+
purpose: { type: 'string', enum: ['final_result', 'blocker', 'no_response'], description: 'For background task or schedule runs, required intent for this outbound message. Use final_result for a concrete useful outcome, blocker for a real issue the user should know about, or no_response to intentionally send nothing.' }
|
|
753
802
|
},
|
|
754
803
|
required: ['platform', 'to', 'content']
|
|
755
804
|
}
|
|
@@ -1986,6 +2035,25 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1986
2035
|
stripNoResponseMarker: false
|
|
1987
2036
|
});
|
|
1988
2037
|
const suppressReply = normalizedMessage === '[NO RESPONSE]';
|
|
2038
|
+
if (isProactiveTrigger(triggerSource)) {
|
|
2039
|
+
const proactiveValidation = validateProactiveSendMessageArgs({
|
|
2040
|
+
purpose: args.purpose,
|
|
2041
|
+
normalizedMessage,
|
|
2042
|
+
});
|
|
2043
|
+
if (!proactiveValidation.ok) {
|
|
2044
|
+
if (proactiveValidation.error) {
|
|
2045
|
+
return {
|
|
2046
|
+
error: proactiveValidation.error,
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
return {
|
|
2050
|
+
sent: false,
|
|
2051
|
+
suppressed: proactiveValidation.suppressed === true,
|
|
2052
|
+
skipped: proactiveValidation.skipped === true,
|
|
2053
|
+
reason: proactiveValidation.reason,
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
1989
2057
|
if (!suppressReply && hasAlreadySentProactiveMessage({
|
|
1990
2058
|
triggerSource,
|
|
1991
2059
|
runState,
|
|
@@ -2701,4 +2769,4 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2701
2769
|
}
|
|
2702
2770
|
}
|
|
2703
2771
|
|
|
2704
|
-
module.exports = { getAvailableTools, executeTool };
|
|
2772
|
+
module.exports = { getAvailableTools, executeTool, validateProactiveSendMessageArgs };
|
|
@@ -240,6 +240,7 @@ function createFigmaProvider() {
|
|
|
240
240
|
description:
|
|
241
241
|
'Official Figma OAuth account connections for future design file and collaboration workflows.',
|
|
242
242
|
icon: 'figma',
|
|
243
|
+
requiresRefreshToken: true,
|
|
243
244
|
apps: FIGMA_APPS,
|
|
244
245
|
toolDefinitions: figmaToolDefinitions,
|
|
245
246
|
connectPrompt:
|
|
@@ -300,6 +300,7 @@ function createGoogleWorkspaceProvider() {
|
|
|
300
300
|
description:
|
|
301
301
|
'Official Gmail, Calendar, Drive, Docs, and Sheets integrations with app-specific accounts.',
|
|
302
302
|
icon: 'google',
|
|
303
|
+
requiresRefreshToken: true,
|
|
303
304
|
apps: GOOGLE_WORKSPACE_APPS.map(({ id, label, description }) => ({
|
|
304
305
|
id,
|
|
305
306
|
label,
|
|
@@ -524,6 +524,7 @@ function createHomeAssistantProvider() {
|
|
|
524
524
|
description:
|
|
525
525
|
'Official Home Assistant account connections for entity state reads, service control, and automation support.',
|
|
526
526
|
icon: 'home_assistant',
|
|
527
|
+
requiresRefreshToken: true,
|
|
527
528
|
apps: HOME_ASSISTANT_APPS,
|
|
528
529
|
toolDefinitions: homeAssistantToolDefinitions,
|
|
529
530
|
connectPrompt:
|
|
@@ -29,6 +29,27 @@ function isLikelyExpiredConnectionError(error) {
|
|
|
29
29
|
].some((hint) => message.includes(hint));
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function assertDurableOAuthCredentials(provider, credentials) {
|
|
33
|
+
const label = String(provider?.label || 'This integration').trim() || 'This integration';
|
|
34
|
+
const normalizedCredentials =
|
|
35
|
+
credentials && typeof credentials === 'object' ? credentials : {};
|
|
36
|
+
|
|
37
|
+
if (!String(normalizedCredentials.access_token || '').trim()) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`${label} did not return an access token, so the connection could not be completed.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
provider?.requiresRefreshToken === true &&
|
|
45
|
+
!String(normalizedCredentials.refresh_token || '').trim()
|
|
46
|
+
) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`${label} did not return a refresh token, so the connection would expire. Revoke the existing app grant for this provider and reconnect it so offline access is granted.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
class IntegrationManager {
|
|
33
54
|
constructor(options = {}) {
|
|
34
55
|
this.app = options.app || null;
|
|
@@ -303,11 +324,7 @@ class IntegrationManager {
|
|
|
303
324
|
result.accountEmail,
|
|
304
325
|
result.credentials,
|
|
305
326
|
);
|
|
306
|
-
|
|
307
|
-
throw new Error(
|
|
308
|
-
`${provider.label} did not return a refresh token, so the connection would expire. Revoke the existing app grant for this provider and reconnect it so offline access is granted.`,
|
|
309
|
-
);
|
|
310
|
-
}
|
|
327
|
+
assertDurableOAuthCredentials(provider, mergedCredentials);
|
|
311
328
|
|
|
312
329
|
db.prepare(
|
|
313
330
|
`INSERT INTO integration_connections (
|
|
@@ -727,5 +744,6 @@ class IntegrationManager {
|
|
|
727
744
|
}
|
|
728
745
|
|
|
729
746
|
module.exports = {
|
|
747
|
+
assertDurableOAuthCredentials,
|
|
730
748
|
IntegrationManager,
|
|
731
749
|
};
|
|
@@ -346,6 +346,7 @@ function createMicrosoftProvider() {
|
|
|
346
346
|
description:
|
|
347
347
|
'Official Microsoft 365 OAuth account connections for Outlook, Calendar, OneDrive, and Teams.',
|
|
348
348
|
icon: 'microsoft',
|
|
349
|
+
requiresRefreshToken: true,
|
|
349
350
|
apps: MICROSOFT_APPS,
|
|
350
351
|
toolDefinitions: microsoftToolDefinitions,
|
|
351
352
|
connectPrompt:
|
|
@@ -241,6 +241,7 @@ function createOAuthProvider(options = {}) {
|
|
|
241
241
|
label: options.label,
|
|
242
242
|
description: options.description,
|
|
243
243
|
icon: options.icon,
|
|
244
|
+
requiresRefreshToken: options.requiresRefreshToken === true,
|
|
244
245
|
apps: apps.map(({ id, label, description }) => ({ id, label, description })),
|
|
245
246
|
connectPrompt: options.connectPrompt || null,
|
|
246
247
|
supportsMultipleAccounts: options.supportsMultipleAccounts !== false,
|
|
@@ -394,6 +394,7 @@ function createSpotifyProvider() {
|
|
|
394
394
|
label: 'Spotify',
|
|
395
395
|
description: 'Official Spotify account integration for music search and playback control.',
|
|
396
396
|
icon: 'spotify',
|
|
397
|
+
requiresRefreshToken: true,
|
|
397
398
|
apps: SPOTIFY_APPS,
|
|
398
399
|
toolDefinitions: spotifyToolDefinitions,
|
|
399
400
|
connectPrompt:
|
|
@@ -296,7 +296,7 @@ class TaskRuntime {
|
|
|
296
296
|
if (normalizedConfig.callTo) {
|
|
297
297
|
notifyHint = `\n\nThis task is configured to notify the user by phone. Use the make_call tool to call "${normalizedConfig.callTo}" with an appropriate greeting based on your findings. The configured greeting hint is: "${normalizedConfig.callGreeting || 'Hello, this is your task reminder.'}"`;
|
|
298
298
|
} else if (normalizedConfig.notifyPlatform && normalizedConfig.notifyTo) {
|
|
299
|
-
notifyHint = `\n\nIf your task result is worth notifying the user about, send it proactively via send_message to platform="${normalizedConfig.notifyPlatform}" to="${normalizedConfig.notifyTo}".`;
|
|
299
|
+
notifyHint = `\n\nIf your task result is worth notifying the user about, send it proactively via send_message to platform="${normalizedConfig.notifyPlatform}" to="${normalizedConfig.notifyTo}" and set purpose="final_result" for a concrete useful outcome or purpose="blocker" for a real issue the user should know about. If nothing important or actionable changed, call send_message with purpose="no_response" and content="[NO RESPONSE]".`;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
const triggerPayloadText = executionMeta.triggerPayload
|