@agent-native/core 0.7.7 → 0.7.11
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/dist/agent/engine/ai-sdk-engine.d.ts +14 -2
- package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.js +70 -54
- package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
- package/dist/agent/engine/anthropic-engine.d.ts +1 -6
- package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
- package/dist/agent/engine/anthropic-engine.js +3 -14
- package/dist/agent/engine/anthropic-engine.js.map +1 -1
- package/dist/agent/engine/builtin.d.ts.map +1 -1
- package/dist/agent/engine/builtin.js +3 -0
- package/dist/agent/engine/builtin.js.map +1 -1
- package/dist/agent/engine/translate-ai-sdk.d.ts +35 -10
- package/dist/agent/engine/translate-ai-sdk.d.ts.map +1 -1
- package/dist/agent/engine/translate-ai-sdk.js +190 -91
- package/dist/agent/engine/translate-ai-sdk.js.map +1 -1
- package/dist/agent/engine/types.d.ts +10 -1
- package/dist/agent/engine/types.d.ts.map +1 -1
- package/dist/agent/production-agent.d.ts +15 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +78 -21
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/thread-data-builder.js +1 -1
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/agent/types.d.ts +4 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/application-state/script-helpers.d.ts +12 -5
- package/dist/application-state/script-helpers.d.ts.map +1 -1
- package/dist/application-state/script-helpers.js +41 -20
- package/dist/application-state/script-helpers.js.map +1 -1
- package/dist/catalog.json +15 -0
- package/dist/chat-threads/store.d.ts.map +1 -1
- package/dist/chat-threads/store.js +7 -5
- package/dist/chat-threads/store.js.map +1 -1
- package/dist/checkpoints/index.d.ts +3 -0
- package/dist/checkpoints/index.d.ts.map +1 -0
- package/dist/checkpoints/index.js +3 -0
- package/dist/checkpoints/index.js.map +1 -0
- package/dist/checkpoints/service.d.ts +6 -0
- package/dist/checkpoints/service.d.ts.map +1 -0
- package/dist/checkpoints/service.js +107 -0
- package/dist/checkpoints/service.js.map +1 -0
- package/dist/checkpoints/store.d.ts +27 -0
- package/dist/checkpoints/store.d.ts.map +1 -0
- package/dist/checkpoints/store.js +92 -0
- package/dist/checkpoints/store.js.map +1 -0
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +85 -1
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +46 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +33 -0
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +3 -1
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +15 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +145 -67
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +9 -5
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/client/ConnectBuilderCard.js +1 -1
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +78 -4
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts +6 -0
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +4 -0
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +12 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +71 -3
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/notifications/NotificationsBell.d.ts +23 -0
- package/dist/client/notifications/NotificationsBell.d.ts.map +1 -0
- package/dist/client/notifications/NotificationsBell.js +165 -0
- package/dist/client/notifications/NotificationsBell.js.map +1 -0
- package/dist/client/notifications/index.d.ts +2 -0
- package/dist/client/notifications/index.d.ts.map +1 -0
- package/dist/client/notifications/index.js +2 -0
- package/dist/client/notifications/index.js.map +1 -0
- package/dist/client/onboarding/OnboardingPanel.js +6 -3
- package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
- package/dist/client/progress/RunsTray.d.ts +18 -0
- package/dist/client/progress/RunsTray.d.ts.map +1 -0
- package/dist/client/progress/RunsTray.js +70 -0
- package/dist/client/progress/RunsTray.js.map +1 -0
- package/dist/client/progress/index.d.ts +2 -0
- package/dist/client/progress/index.d.ts.map +1 -0
- package/dist/client/progress/index.js +2 -0
- package/dist/client/progress/index.js.map +1 -0
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +19 -4
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/client/settings/AutomationsSection.d.ts +2 -0
- package/dist/client/settings/AutomationsSection.d.ts.map +1 -0
- package/dist/client/settings/AutomationsSection.js +214 -0
- package/dist/client/settings/AutomationsSection.js.map +1 -0
- package/dist/client/settings/ComingSoonSection.d.ts.map +1 -1
- package/dist/client/settings/ComingSoonSection.js +2 -1
- package/dist/client/settings/ComingSoonSection.js.map +1 -1
- package/dist/client/settings/LLMSection.d.ts.map +1 -1
- package/dist/client/settings/LLMSection.js +137 -10
- package/dist/client/settings/LLMSection.js.map +1 -1
- package/dist/client/settings/SecretsSection.d.ts.map +1 -1
- package/dist/client/settings/SecretsSection.js +122 -3
- package/dist/client/settings/SecretsSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +140 -11
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/VoiceTranscriptionSection.d.ts.map +1 -1
- package/dist/client/settings/VoiceTranscriptionSection.js +2 -2
- package/dist/client/settings/VoiceTranscriptionSection.js.map +1 -1
- package/dist/client/use-pausing-interval.d.ts +11 -0
- package/dist/client/use-pausing-interval.d.ts.map +1 -0
- package/dist/client/use-pausing-interval.js +49 -0
- package/dist/client/use-pausing-interval.js.map +1 -0
- package/dist/db/client.d.ts +26 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +84 -2
- package/dist/db/client.js.map +1 -1
- package/dist/db/drizzle-config.d.ts +33 -0
- package/dist/db/drizzle-config.d.ts.map +1 -0
- package/dist/db/drizzle-config.js +132 -0
- package/dist/db/drizzle-config.js.map +1 -0
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +11 -6
- package/dist/db/migrations.js.map +1 -1
- package/dist/deploy/build.js +2 -1
- package/dist/deploy/build.js.map +1 -1
- package/dist/event-bus/bus.d.ts +20 -0
- package/dist/event-bus/bus.d.ts.map +1 -0
- package/dist/event-bus/bus.js +108 -0
- package/dist/event-bus/bus.js.map +1 -0
- package/dist/event-bus/index.d.ts +4 -0
- package/dist/event-bus/index.d.ts.map +1 -0
- package/dist/event-bus/index.js +3 -0
- package/dist/event-bus/index.js.map +1 -0
- package/dist/event-bus/registry.d.ts +22 -0
- package/dist/event-bus/registry.d.ts.map +1 -0
- package/dist/event-bus/registry.js +63 -0
- package/dist/event-bus/registry.js.map +1 -0
- package/dist/event-bus/types.d.ts +27 -0
- package/dist/event-bus/types.d.ts.map +1 -0
- package/dist/event-bus/types.js +2 -0
- package/dist/event-bus/types.js.map +1 -0
- package/dist/integrations/config-store.d.ts.map +1 -1
- package/dist/integrations/config-store.js +16 -12
- package/dist/integrations/config-store.js.map +1 -1
- package/dist/integrations/google-docs-poller.d.ts.map +1 -1
- package/dist/integrations/google-docs-poller.js +5 -1
- package/dist/integrations/google-docs-poller.js.map +1 -1
- package/dist/jobs/scheduler.d.ts.map +1 -1
- package/dist/jobs/scheduler.js +7 -3
- package/dist/jobs/scheduler.js.map +1 -1
- package/dist/notifications/actions.d.ts +10 -0
- package/dist/notifications/actions.d.ts.map +1 -0
- package/dist/notifications/actions.js +114 -0
- package/dist/notifications/actions.js.map +1 -0
- package/dist/notifications/channels.d.ts +15 -0
- package/dist/notifications/channels.d.ts.map +1 -0
- package/dist/notifications/channels.js +97 -0
- package/dist/notifications/channels.js.map +1 -0
- package/dist/notifications/index.d.ts +4 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +3 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/registry.d.ts +9 -0
- package/dist/notifications/registry.d.ts.map +1 -0
- package/dist/notifications/registry.js +146 -0
- package/dist/notifications/registry.js.map +1 -0
- package/dist/notifications/routes.d.ts +34 -0
- package/dist/notifications/routes.d.ts.map +1 -0
- package/dist/notifications/routes.js +69 -0
- package/dist/notifications/routes.js.map +1 -0
- package/dist/notifications/store.d.ts +25 -0
- package/dist/notifications/store.d.ts.map +1 -0
- package/dist/notifications/store.js +158 -0
- package/dist/notifications/store.js.map +1 -0
- package/dist/notifications/types.d.ts +43 -0
- package/dist/notifications/types.d.ts.map +1 -0
- package/dist/notifications/types.js +2 -0
- package/dist/notifications/types.js.map +1 -0
- package/dist/org/handlers.d.ts.map +1 -1
- package/dist/org/handlers.js +7 -26
- package/dist/org/handlers.js.map +1 -1
- package/dist/progress/actions.d.ts +8 -0
- package/dist/progress/actions.d.ts.map +1 -0
- package/dist/progress/actions.js +158 -0
- package/dist/progress/actions.js.map +1 -0
- package/dist/progress/index.d.ts +3 -0
- package/dist/progress/index.d.ts.map +1 -0
- package/dist/progress/index.js +2 -0
- package/dist/progress/index.js.map +1 -0
- package/dist/progress/registry.d.ts +22 -0
- package/dist/progress/registry.d.ts.map +1 -0
- package/dist/progress/registry.js +98 -0
- package/dist/progress/registry.js.map +1 -0
- package/dist/progress/routes.d.ts +21 -0
- package/dist/progress/routes.d.ts.map +1 -0
- package/dist/progress/routes.js +59 -0
- package/dist/progress/routes.js.map +1 -0
- package/dist/progress/store.d.ts +7 -0
- package/dist/progress/store.d.ts.map +1 -0
- package/dist/progress/store.js +195 -0
- package/dist/progress/store.js.map +1 -0
- package/dist/progress/types.d.ts +49 -0
- package/dist/progress/types.d.ts.map +1 -0
- package/dist/progress/types.js +7 -0
- package/dist/progress/types.js.map +1 -0
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +19 -15
- package/dist/resources/store.js.map +1 -1
- package/dist/secrets/index.d.ts +3 -2
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +3 -2
- package/dist/secrets/index.js.map +1 -1
- package/dist/secrets/routes.d.ts +41 -2
- package/dist/secrets/routes.d.ts.map +1 -1
- package/dist/secrets/routes.js +167 -1
- package/dist/secrets/routes.js.map +1 -1
- package/dist/secrets/schema.d.ts +39 -1
- package/dist/secrets/schema.d.ts.map +1 -1
- package/dist/secrets/schema.js +6 -0
- package/dist/secrets/schema.js.map +1 -1
- package/dist/secrets/storage.d.ts +26 -0
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +111 -5
- package/dist/secrets/storage.js.map +1 -1
- package/dist/secrets/substitution.d.ts +39 -0
- package/dist/secrets/substitution.d.ts.map +1 -0
- package/dist/secrets/substitution.js +93 -0
- package/dist/secrets/substitution.js.map +1 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +1657 -1410
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts +11 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +74 -21
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +34 -16
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +115 -1
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/email-templates.d.ts +43 -0
- package/dist/server/email-templates.d.ts.map +1 -0
- package/dist/server/email-templates.js +86 -0
- package/dist/server/email-templates.js.map +1 -0
- package/dist/server/framework-request-handler.d.ts +15 -0
- package/dist/server/framework-request-handler.d.ts.map +1 -1
- package/dist/server/framework-request-handler.js +64 -1
- package/dist/server/framework-request-handler.js.map +1 -1
- package/dist/server/onboarding-html.d.ts +11 -0
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +275 -16
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/server/schema-prompt.d.ts.map +1 -1
- package/dist/server/schema-prompt.js +5 -0
- package/dist/server/schema-prompt.js.map +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +1 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/truncate.d.ts +8 -0
- package/dist/shared/truncate.d.ts.map +1 -0
- package/dist/shared/truncate.js +12 -0
- package/dist/shared/truncate.js.map +1 -0
- package/dist/templates/default/.agents/skills/agent-engines/SKILL.md +60 -4
- package/dist/templates/default/.agents/skills/notifications/SKILL.md +95 -0
- package/dist/templates/default/.agents/skills/progress/SKILL.md +97 -0
- package/dist/templates/default/AGENTS.md +12 -10
- package/dist/templates/default/package.json +10 -10
- package/dist/templates/workspace-core/package.json +5 -5
- package/dist/templates/workspace-root/package.json +1 -1
- package/dist/templates/workspace-root/tsconfig.base.json +1 -2
- package/dist/tools/fetch-tool.d.ts +22 -0
- package/dist/tools/fetch-tool.d.ts.map +1 -0
- package/dist/tools/fetch-tool.js +156 -0
- package/dist/tools/fetch-tool.js.map +1 -0
- package/dist/tracking/index.d.ts +4 -0
- package/dist/tracking/index.d.ts.map +1 -0
- package/dist/tracking/index.js +3 -0
- package/dist/tracking/index.js.map +1 -0
- package/dist/tracking/providers.d.ts +15 -0
- package/dist/tracking/providers.d.ts.map +1 -0
- package/dist/tracking/providers.js +195 -0
- package/dist/tracking/providers.js.map +1 -0
- package/dist/tracking/registry.d.ts +10 -0
- package/dist/tracking/registry.d.ts.map +1 -0
- package/dist/tracking/registry.js +75 -0
- package/dist/tracking/registry.js.map +1 -0
- package/dist/tracking/types.d.ts +13 -0
- package/dist/tracking/types.d.ts.map +1 -0
- package/dist/tracking/types.js +2 -0
- package/dist/tracking/types.js.map +1 -0
- package/dist/triggers/actions.d.ts +10 -0
- package/dist/triggers/actions.d.ts.map +1 -0
- package/dist/triggers/actions.js +277 -0
- package/dist/triggers/actions.js.map +1 -0
- package/dist/triggers/condition-evaluator.d.ts +15 -0
- package/dist/triggers/condition-evaluator.d.ts.map +1 -0
- package/dist/triggers/condition-evaluator.js +107 -0
- package/dist/triggers/condition-evaluator.js.map +1 -0
- package/dist/triggers/dispatcher.d.ts +32 -0
- package/dist/triggers/dispatcher.d.ts.map +1 -0
- package/dist/triggers/dispatcher.js +291 -0
- package/dist/triggers/dispatcher.js.map +1 -0
- package/dist/triggers/index.d.ts +5 -0
- package/dist/triggers/index.d.ts.map +1 -0
- package/dist/triggers/index.js +4 -0
- package/dist/triggers/index.js.map +1 -0
- package/dist/triggers/types.d.ts +35 -0
- package/dist/triggers/types.d.ts.map +1 -0
- package/dist/triggers/types.js +9 -0
- package/dist/triggers/types.js.map +1 -0
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +66 -16
- package/dist/vite/client.js.map +1 -1
- package/docs/content/automations.md +239 -0
- package/docs/content/multi-tenancy.md +88 -0
- package/docs/content/notifications.md +199 -0
- package/docs/content/progress.md +176 -0
- package/docs/content/tracking.md +168 -0
- package/package.json +54 -35
- package/src/templates/default/.agents/skills/agent-engines/SKILL.md +60 -4
- package/src/templates/default/.agents/skills/notifications/SKILL.md +95 -0
- package/src/templates/default/.agents/skills/progress/SKILL.md +97 -0
- package/src/templates/default/AGENTS.md +12 -10
- package/src/templates/default/package.json +10 -10
- package/src/templates/workspace-core/package.json +5 -5
- package/src/templates/workspace-root/package.json +1 -1
- package/src/templates/workspace-root/tsconfig.base.json +1 -2
- package/tsconfig.base.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runWithRequestContext, getRequestOrgId } from "./request-context.js";
|
|
2
2
|
import { getSetting, putSetting } from "../settings/store.js";
|
|
3
|
-
import { getH3App } from "./framework-request-handler.js";
|
|
3
|
+
import { getH3App, trackPluginInit } from "./framework-request-handler.js";
|
|
4
4
|
import { createProductionAgentHandler, runAgentLoop, actionsToEngineTools, getActiveRunForThreadAsync, abortRun, subscribeToRun, } from "../agent/production-agent.js";
|
|
5
5
|
import { resolveEngine, createAnthropicEngine } from "../agent/engine/index.js";
|
|
6
6
|
import { McpClientManager, loadMcpConfig, autoDetectMcpConfig, mcpToolsToActionEntries, syncMcpActionEntries, mountMcpServersRoutes, mountMcpHubRoutes, buildMergedConfig, getHubStatus, isHubServeEnabled, } from "../mcp-client/index.js";
|
|
@@ -240,7 +240,7 @@ async function createDbScriptEntries() {
|
|
|
240
240
|
},
|
|
241
241
|
}, schemaMod.default, { readOnly: true }),
|
|
242
242
|
"db-query": wrapCliScript({
|
|
243
|
-
description: "Read from the app's SQL database. Runs a SELECT
|
|
243
|
+
description: "Read from the app's own SQL database ONLY. Runs a SELECT against the app's internal tables (settings, application_state, template tables). Results are auto-scoped to the current user/org. IMPORTANT: This tool CANNOT access external data sources like BigQuery, HubSpot, Jira, GA4, etc. For those, use the appropriate template action (e.g. `bigquery` for warehouse tables, `ga4-report` for Google Analytics). If a table isn't in the app schema, don't try db-query — use the data-source-specific action.",
|
|
244
244
|
parameters: {
|
|
245
245
|
type: "object",
|
|
246
246
|
properties: {
|
|
@@ -262,7 +262,7 @@ async function createDbScriptEntries() {
|
|
|
262
262
|
},
|
|
263
263
|
}, queryMod.default, { readOnly: true }),
|
|
264
264
|
"db-exec": wrapCliScript({
|
|
265
|
-
description: "Write to the app's SQL database. Runs INSERT / UPDATE / DELETE against the app's
|
|
265
|
+
description: "Write to the app's own SQL database ONLY. Runs INSERT / UPDATE / DELETE against the app's internal tables. Writes are auto-scoped to the current user/org, and `owner_email` / `org_id` are auto-injected on INSERT. IMPORTANT: This tool CANNOT write to external data sources like BigQuery, HubSpot, etc. For external services, use the appropriate template action.",
|
|
266
266
|
parameters: {
|
|
267
267
|
type: "object",
|
|
268
268
|
properties: {
|
|
@@ -1274,6 +1274,8 @@ ${lines.join("\n")}`;
|
|
|
1274
1274
|
|
|
1275
1275
|
**Use these actions directly to accomplish tasks. Do NOT use \`db-schema\`, \`search-files\`, or \`shell\` to explore the app — these actions already connect to the correct database and services.**
|
|
1276
1276
|
|
|
1277
|
+
**For external data sources (BigQuery, HubSpot, Jira, GA4, etc.), use the data-source-specific action below — NOT \`db-query\`.** \`db-query\` only reaches the app's own internal database. If the user asks about tables not in the app schema, pick the matching action here.
|
|
1278
|
+
|
|
1277
1279
|
Parameter notation: \`name*\` = required, \`name?\` = optional. Always pass the tool's parameters as a JSON object to the tool_use call — never via shell or string-concatenated CLI flags.
|
|
1278
1280
|
|
|
1279
1281
|
${lines.join("\n")}`;
|
|
@@ -1353,481 +1355,736 @@ function isLocalhost(event) {
|
|
|
1353
1355
|
}
|
|
1354
1356
|
}
|
|
1355
1357
|
export function createAgentChatPlugin(options) {
|
|
1356
|
-
return
|
|
1357
|
-
//
|
|
1358
|
-
//
|
|
1359
|
-
//
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
const { reapAllStaleRuns } = await import("../agent/run-store.js");
|
|
1370
|
-
const reaped = await reapAllStaleRuns();
|
|
1371
|
-
if (reaped > 0) {
|
|
1372
|
-
console.log(`[agent-chat] reaped ${reaped} stale run(s) on startup`);
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
catch {
|
|
1376
|
-
// Best effort — don't block plugin init if SQL isn't ready yet.
|
|
1377
|
-
}
|
|
1378
|
-
const env = process.env.NODE_ENV;
|
|
1379
|
-
// AGENT_MODE=production forces production agent constraints even in dev
|
|
1380
|
-
const canToggle = (env === "development" || env === "test") &&
|
|
1381
|
-
process.env.AGENT_MODE !== "production";
|
|
1382
|
-
const routePath = options?.path ?? "/_agent-native/agent-chat";
|
|
1383
|
-
// Mutable mode flag — persisted to the `settings` table so a user who
|
|
1384
|
-
// toggles to "Production" stays in prod mode across server restarts.
|
|
1385
|
-
// Hoisted here (before any tool-registry / handler closures are built)
|
|
1386
|
-
// so every runtime decision point can close over it and see live changes
|
|
1387
|
-
// when the user toggles the Environment dropdown.
|
|
1388
|
-
const AGENT_MODE_SETTING_KEY = "agent-chat.mode";
|
|
1389
|
-
let currentDevMode = canToggle;
|
|
1390
|
-
if (canToggle) {
|
|
1358
|
+
return (nitroApp) => {
|
|
1359
|
+
// Nitro v3 calls plugins synchronously and doesn't await async return
|
|
1360
|
+
// values. We track the async init so the framework's readiness gate
|
|
1361
|
+
// holds /_agent-native requests until routes are registered.
|
|
1362
|
+
const initPromise = (async () => {
|
|
1363
|
+
const { awaitBootstrap } = await import("./framework-request-handler.js");
|
|
1364
|
+
await awaitBootstrap(nitroApp);
|
|
1365
|
+
// Reap phantom runs left over from the previous process (HMR restart,
|
|
1366
|
+
// process crash, isolate eviction). Any run whose heartbeat is already
|
|
1367
|
+
// stale by startup time had a dead producer; mark it errored so the
|
|
1368
|
+
// next /runs/active check returns a terminal status and reconnecting
|
|
1369
|
+
// clients don't spin on "Thinking...". Runs owned by OTHER live
|
|
1370
|
+
// isolates are protected by their fresh heartbeats.
|
|
1391
1371
|
try {
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1394
|
-
|
|
1372
|
+
const { reapAllStaleRuns } = await import("../agent/run-store.js");
|
|
1373
|
+
const reaped = await reapAllStaleRuns();
|
|
1374
|
+
if (reaped > 0) {
|
|
1375
|
+
console.log(`[agent-chat] reaped ${reaped} stale run(s) on startup`);
|
|
1395
1376
|
}
|
|
1396
1377
|
}
|
|
1397
1378
|
catch {
|
|
1398
|
-
//
|
|
1379
|
+
// Best effort — don't block plugin init if SQL isn't ready yet.
|
|
1399
1380
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1381
|
+
const env = process.env.NODE_ENV;
|
|
1382
|
+
// AGENT_MODE=production forces production agent constraints even in dev
|
|
1383
|
+
const canToggle = (env === "development" || env === "test") &&
|
|
1384
|
+
process.env.AGENT_MODE !== "production";
|
|
1385
|
+
const routePath = options?.path ?? "/_agent-native/agent-chat";
|
|
1386
|
+
// Mutable mode flag — persisted to the `settings` table so a user who
|
|
1387
|
+
// toggles to "Production" stays in prod mode across server restarts.
|
|
1388
|
+
// Hoisted here (before any tool-registry / handler closures are built)
|
|
1389
|
+
// so every runtime decision point can close over it and see live changes
|
|
1390
|
+
// when the user toggles the Environment dropdown.
|
|
1391
|
+
const AGENT_MODE_SETTING_KEY = "agent-chat.mode";
|
|
1392
|
+
let currentDevMode = canToggle;
|
|
1393
|
+
if (canToggle) {
|
|
1394
|
+
try {
|
|
1395
|
+
const persisted = await getSetting(AGENT_MODE_SETTING_KEY);
|
|
1396
|
+
if (persisted && typeof persisted.devMode === "boolean") {
|
|
1397
|
+
currentDevMode = persisted.devMode;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
catch {
|
|
1401
|
+
// Settings table may not be ready yet — fall back to default.
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
// Every closure that picks between dev/prod tools, prompts, or handlers
|
|
1405
|
+
// at request time should call this getter instead of reading `canToggle`.
|
|
1406
|
+
// `canToggle` means "this environment allows toggling" (static); this
|
|
1407
|
+
// function means "the user currently has dev mode ON" (live).
|
|
1408
|
+
const isDevMode = () => currentDevMode;
|
|
1409
|
+
// Initialize MCP client. Merges file/env config + auto-detected binaries
|
|
1410
|
+
// + any remote servers users have added through the settings UI (persisted
|
|
1411
|
+
// in the settings table, scanned across all scopes so we never drop
|
|
1412
|
+
// another user's entries). Graceful-degrade: any failure yields zero MCP
|
|
1413
|
+
// tools and agent-chat keeps working as before.
|
|
1414
|
+
let mcpConfig = await buildMergedConfig().catch((err) => {
|
|
1415
|
+
console.warn(`[mcp-client] buildMergedConfig failed: ${err?.message ?? err}`);
|
|
1416
|
+
return null;
|
|
1417
|
+
});
|
|
1418
|
+
if (!mcpConfig) {
|
|
1419
|
+
const fileOrEnv = loadMcpConfig() ?? autoDetectMcpConfig();
|
|
1420
|
+
mcpConfig = fileOrEnv;
|
|
1421
|
+
if (mcpConfig?.source) {
|
|
1422
|
+
console.log(`[mcp-client] loaded config from ${mcpConfig.source} (${Object.keys(mcpConfig.servers).length} server(s))`);
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
console.log("[mcp-client] no configured MCP servers — skipping MCP tools");
|
|
1426
|
+
}
|
|
1420
1427
|
}
|
|
1421
|
-
else {
|
|
1422
|
-
console.log(
|
|
1428
|
+
else if (mcpConfig.source) {
|
|
1429
|
+
console.log(`[mcp-client] merged config (${Object.keys(mcpConfig.servers).length} server(s), source: ${mcpConfig.source})`);
|
|
1423
1430
|
}
|
|
1424
|
-
|
|
1425
|
-
else if (mcpConfig.source) {
|
|
1426
|
-
console.log(`[mcp-client] merged config (${Object.keys(mcpConfig.servers).length} server(s), source: ${mcpConfig.source})`);
|
|
1427
|
-
}
|
|
1428
|
-
const mcpManager = new McpClientManager(mcpConfig);
|
|
1429
|
-
try {
|
|
1430
|
-
await mcpManager.start();
|
|
1431
|
-
}
|
|
1432
|
-
catch (err) {
|
|
1433
|
-
console.warn(`[mcp-client] start() failed: ${err?.message ?? err}. Continuing without MCP tools.`);
|
|
1434
|
-
}
|
|
1435
|
-
setGlobalMcpManager(mcpManager);
|
|
1436
|
-
const mcpActionEntries = mcpToolsToActionEntries(mcpManager);
|
|
1437
|
-
// Mount status + management routes so the settings UI can list / add /
|
|
1438
|
-
// remove remote MCP servers and hot-reload the running manager.
|
|
1439
|
-
mountMcpStatusRoute(nitroApp, mcpManager);
|
|
1440
|
-
mountMcpServersRoutes(nitroApp, mcpManager);
|
|
1441
|
-
// Hub-serve: expose org-scope servers to other agent-native apps in the
|
|
1442
|
-
// workspace when `AGENT_NATIVE_MCP_HUB_TOKEN` is set (dispatch, by
|
|
1443
|
-
// convention). Gated by the env var so mounting is a no-op otherwise.
|
|
1444
|
-
if (isHubServeEnabled()) {
|
|
1445
|
-
mountMcpHubRoutes(nitroApp);
|
|
1446
|
-
console.log("[mcp-client] hub serve enabled — other apps can pull org servers via /_agent-native/mcp/hub/servers");
|
|
1447
|
-
}
|
|
1448
|
-
const hubStatus = getHubStatus();
|
|
1449
|
-
if (hubStatus.consuming) {
|
|
1450
|
-
console.log(`[mcp-client] hub consume enabled — pulling from ${hubStatus.hubUrl}`);
|
|
1451
|
-
}
|
|
1452
|
-
mountMcpHubStatusRoute(nitroApp);
|
|
1453
|
-
// Ensure we tear down child processes if the host shuts down cleanly.
|
|
1454
|
-
if (typeof process !== "undefined" &&
|
|
1455
|
-
typeof process.once === "function" &&
|
|
1456
|
-
!globalThis.__agentNativeMcpExitHooked) {
|
|
1457
|
-
globalThis.__agentNativeMcpExitHooked = true;
|
|
1458
|
-
const stop = () => {
|
|
1459
|
-
const mgr = getGlobalMcpManager();
|
|
1460
|
-
if (mgr)
|
|
1461
|
-
void mgr.stop();
|
|
1462
|
-
};
|
|
1463
|
-
process.once("exit", stop);
|
|
1464
|
-
process.once("SIGTERM", stop);
|
|
1465
|
-
process.once("SIGINT", stop);
|
|
1466
|
-
}
|
|
1467
|
-
// Resolve actions — prefer `actions`, fall back to deprecated `scripts`
|
|
1468
|
-
const rawActions = options?.actions ?? options?.scripts;
|
|
1469
|
-
const templateScripts = typeof rawActions === "function"
|
|
1470
|
-
? await rawActions()
|
|
1471
|
-
: (rawActions ?? {});
|
|
1472
|
-
// Resource, chat, docs, db, and cross-agent scripts are available in both prod and dev modes
|
|
1473
|
-
const resourceScripts = await createResourceScriptEntries();
|
|
1474
|
-
const docsScripts = await createDocsScriptEntries();
|
|
1475
|
-
const dbScripts = await createDbScriptEntries();
|
|
1476
|
-
const refreshScreenTool = createRefreshScreenEntry();
|
|
1477
|
-
const urlTools = createUrlTools();
|
|
1478
|
-
const engineScripts = await createAgentEngineScriptEntries();
|
|
1479
|
-
const chatScripts = {
|
|
1480
|
-
...(await createChatScriptEntries()),
|
|
1481
|
-
...engineScripts,
|
|
1482
|
-
};
|
|
1483
|
-
const callAgentScript = await createCallAgentScriptEntry(options?.appId);
|
|
1484
|
-
let _currentRequestOrigin = "http://localhost:3000";
|
|
1485
|
-
const browserTools = createBuilderBrowserTool({
|
|
1486
|
-
getOrigin: () => _currentRequestOrigin,
|
|
1487
|
-
});
|
|
1488
|
-
// Auto-mount A2A protocol endpoints so every app is discoverable
|
|
1489
|
-
// and callable by other agents via the standard protocol.
|
|
1490
|
-
// In dev mode, include dev scripts (filesystem-discovered) so the A2A agent
|
|
1491
|
-
// has access to the same tools as the interactive agent.
|
|
1492
|
-
let devScriptsForA2A = {};
|
|
1493
|
-
let discoveredActions = {};
|
|
1494
|
-
if (canToggle) {
|
|
1431
|
+
const mcpManager = new McpClientManager(mcpConfig);
|
|
1495
1432
|
try {
|
|
1496
|
-
|
|
1497
|
-
devScriptsForA2A = await createDevScriptRegistry();
|
|
1433
|
+
await mcpManager.start();
|
|
1498
1434
|
}
|
|
1499
|
-
catch {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1435
|
+
catch (err) {
|
|
1436
|
+
console.warn(`[mcp-client] start() failed: ${err?.message ?? err}. Continuing without MCP tools.`);
|
|
1437
|
+
}
|
|
1438
|
+
setGlobalMcpManager(mcpManager);
|
|
1439
|
+
const mcpActionEntries = mcpToolsToActionEntries(mcpManager);
|
|
1440
|
+
// Mount status + management routes so the settings UI can list / add /
|
|
1441
|
+
// remove remote MCP servers and hot-reload the running manager.
|
|
1442
|
+
mountMcpStatusRoute(nitroApp, mcpManager);
|
|
1443
|
+
mountMcpServersRoutes(nitroApp, mcpManager);
|
|
1444
|
+
// Hub-serve: expose org-scope servers to other agent-native apps in the
|
|
1445
|
+
// workspace when `AGENT_NATIVE_MCP_HUB_TOKEN` is set (dispatch, by
|
|
1446
|
+
// convention). Gated by the env var so mounting is a no-op otherwise.
|
|
1447
|
+
if (isHubServeEnabled()) {
|
|
1448
|
+
mountMcpHubRoutes(nitroApp);
|
|
1449
|
+
console.log("[mcp-client] hub serve enabled — other apps can pull org servers via /_agent-native/mcp/hub/servers");
|
|
1450
|
+
}
|
|
1451
|
+
const hubStatus = getHubStatus();
|
|
1452
|
+
if (hubStatus.consuming) {
|
|
1453
|
+
console.log(`[mcp-client] hub consume enabled — pulling from ${hubStatus.hubUrl}`);
|
|
1454
|
+
}
|
|
1455
|
+
mountMcpHubStatusRoute(nitroApp);
|
|
1456
|
+
// Ensure we tear down child processes if the host shuts down cleanly.
|
|
1457
|
+
if (typeof process !== "undefined" &&
|
|
1458
|
+
typeof process.once === "function" &&
|
|
1459
|
+
!globalThis.__agentNativeMcpExitHooked) {
|
|
1460
|
+
globalThis.__agentNativeMcpExitHooked = true;
|
|
1461
|
+
const stop = () => {
|
|
1462
|
+
const mgr = getGlobalMcpManager();
|
|
1463
|
+
if (mgr)
|
|
1464
|
+
void mgr.stop();
|
|
1465
|
+
};
|
|
1466
|
+
process.once("exit", stop);
|
|
1467
|
+
process.once("SIGTERM", stop);
|
|
1468
|
+
process.once("SIGINT", stop);
|
|
1469
|
+
}
|
|
1470
|
+
// Resolve actions — prefer `actions`, fall back to deprecated `scripts`
|
|
1471
|
+
const rawActions = options?.actions ?? options?.scripts;
|
|
1472
|
+
const templateScripts = typeof rawActions === "function"
|
|
1473
|
+
? await rawActions()
|
|
1474
|
+
: (rawActions ?? {});
|
|
1475
|
+
// Resource, chat, docs, db, and cross-agent scripts are available in both prod and dev modes
|
|
1476
|
+
const resourceScripts = await createResourceScriptEntries();
|
|
1477
|
+
const docsScripts = await createDocsScriptEntries();
|
|
1478
|
+
const dbScripts = await createDbScriptEntries();
|
|
1479
|
+
const refreshScreenTool = createRefreshScreenEntry();
|
|
1480
|
+
const urlTools = createUrlTools();
|
|
1481
|
+
const engineScripts = await createAgentEngineScriptEntries();
|
|
1482
|
+
const chatScripts = {
|
|
1483
|
+
...(await createChatScriptEntries()),
|
|
1484
|
+
...engineScripts,
|
|
1485
|
+
};
|
|
1486
|
+
const callAgentScript = await createCallAgentScriptEntry(options?.appId);
|
|
1487
|
+
let _currentRequestOrigin = "http://localhost:3000";
|
|
1488
|
+
const browserTools = createBuilderBrowserTool({
|
|
1489
|
+
getOrigin: () => _currentRequestOrigin,
|
|
1490
|
+
});
|
|
1491
|
+
// Auto-mount A2A protocol endpoints so every app is discoverable
|
|
1492
|
+
// and callable by other agents via the standard protocol.
|
|
1493
|
+
// In dev mode, include dev scripts (filesystem-discovered) so the A2A agent
|
|
1494
|
+
// has access to the same tools as the interactive agent.
|
|
1495
|
+
let devScriptsForA2A = {};
|
|
1496
|
+
let discoveredActions = {};
|
|
1497
|
+
if (canToggle) {
|
|
1498
|
+
try {
|
|
1499
|
+
const { createDevScriptRegistry } = await import("../scripts/dev/index.js");
|
|
1500
|
+
devScriptsForA2A = await createDevScriptRegistry();
|
|
1501
|
+
}
|
|
1502
|
+
catch { }
|
|
1503
|
+
// Auto-discover template action files and register as shell-based tools.
|
|
1504
|
+
// This ensures templates without a custom agent-chat plugin (e.g., analytics)
|
|
1505
|
+
// still have their domain actions available as tools.
|
|
1506
|
+
try {
|
|
1507
|
+
const fs = await import("fs");
|
|
1508
|
+
const pathMod = await import("path");
|
|
1509
|
+
const cwd = process.cwd();
|
|
1510
|
+
const skipFiles = new Set([
|
|
1511
|
+
"helpers",
|
|
1512
|
+
"run",
|
|
1513
|
+
"registry",
|
|
1514
|
+
"_utils",
|
|
1515
|
+
"db-connect",
|
|
1516
|
+
"db-status",
|
|
1517
|
+
]);
|
|
1518
|
+
for (const dir of ["actions", "scripts"]) {
|
|
1519
|
+
const actionsDir = pathMod.join(cwd, dir);
|
|
1520
|
+
const _fs = await lazyFs();
|
|
1521
|
+
if (!_fs.existsSync(actionsDir))
|
|
1528
1522
|
continue;
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
const
|
|
1536
|
-
|
|
1537
|
-
? mod.default
|
|
1538
|
-
: mod;
|
|
1539
|
-
if (def?.tool && typeof def.run === "function") {
|
|
1540
|
-
discoveredActions[name] = {
|
|
1541
|
-
tool: def.tool,
|
|
1542
|
-
run: def.run,
|
|
1543
|
-
...(def.http !== undefined ? { http: def.http } : {}),
|
|
1544
|
-
};
|
|
1523
|
+
const files = _fs
|
|
1524
|
+
.readdirSync(actionsDir)
|
|
1525
|
+
.filter((f) => f.endsWith(".ts") &&
|
|
1526
|
+
!f.startsWith("_") &&
|
|
1527
|
+
!skipFiles.has(f.replace(/\.ts$/, "")));
|
|
1528
|
+
for (const file of files) {
|
|
1529
|
+
const name = file.replace(/\.ts$/, "");
|
|
1530
|
+
if (templateScripts[name] || devScriptsForA2A[name])
|
|
1545
1531
|
continue;
|
|
1532
|
+
// Try to load the action module directly so we get the real
|
|
1533
|
+
// run function (not a shell wrapper). This makes HTTP endpoints
|
|
1534
|
+
// work correctly. Only fall back to shell wrapper if the import
|
|
1535
|
+
// fails (e.g., CLI-style scripts that throw at top level).
|
|
1536
|
+
const filePath = pathMod.join(actionsDir, file);
|
|
1537
|
+
try {
|
|
1538
|
+
const mod = await import(/* @vite-ignore */ filePath);
|
|
1539
|
+
const def = mod.default && typeof mod.default === "object"
|
|
1540
|
+
? mod.default
|
|
1541
|
+
: mod;
|
|
1542
|
+
if (def?.tool && typeof def.run === "function") {
|
|
1543
|
+
discoveredActions[name] = {
|
|
1544
|
+
tool: def.tool,
|
|
1545
|
+
run: def.run,
|
|
1546
|
+
...(def.http !== undefined ? { http: def.http } : {}),
|
|
1547
|
+
};
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1546
1550
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
// (and .ts files Node can't parse natively).
|
|
1551
|
-
}
|
|
1552
|
-
// Static-parse the source for `http: false` or
|
|
1553
|
-
// `http: { method: "GET" }` so the shell-wrapper fallback still
|
|
1554
|
-
// mounts HTTP routes with the correct method. We can't load the
|
|
1555
|
-
// .ts module to read the real defineAction object in this Node
|
|
1556
|
-
// context, so this regex sniff is the best we can do until the
|
|
1557
|
-
// discovery is moved into a Vite-aware codepath.
|
|
1558
|
-
let httpConfig;
|
|
1559
|
-
try {
|
|
1560
|
-
const src = _fs.readFileSync(filePath, "utf-8");
|
|
1561
|
-
if (/\bhttp\s*:\s*false\b/.test(src)) {
|
|
1562
|
-
httpConfig = false;
|
|
1551
|
+
catch {
|
|
1552
|
+
// Fall through to shell wrapper for CLI-style scripts
|
|
1553
|
+
// (and .ts files Node can't parse natively).
|
|
1563
1554
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1555
|
+
// Static-parse the source for `http: false` or
|
|
1556
|
+
// `http: { method: "GET" }` so the shell-wrapper fallback still
|
|
1557
|
+
// mounts HTTP routes with the correct method. We can't load the
|
|
1558
|
+
// .ts module to read the real defineAction object in this Node
|
|
1559
|
+
// context, so this regex sniff is the best we can do until the
|
|
1560
|
+
// discovery is moved into a Vite-aware codepath.
|
|
1561
|
+
let httpConfig;
|
|
1562
|
+
try {
|
|
1563
|
+
const src = _fs.readFileSync(filePath, "utf-8");
|
|
1564
|
+
if (/\bhttp\s*:\s*false\b/.test(src)) {
|
|
1565
|
+
httpConfig = false;
|
|
1566
|
+
}
|
|
1567
|
+
else {
|
|
1568
|
+
const httpStart = src.search(/\bhttp\s*:\s*\{/);
|
|
1569
|
+
if (httpStart >= 0) {
|
|
1570
|
+
const window = src.slice(httpStart, httpStart + 200);
|
|
1571
|
+
const m = window.match(/method\s*:\s*['"`](GET|POST|PUT|DELETE)['"`]/);
|
|
1572
|
+
const p = window.match(/path\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
1573
|
+
if (m || p) {
|
|
1574
|
+
httpConfig = {
|
|
1575
|
+
...(m
|
|
1576
|
+
? {
|
|
1577
|
+
method: m[1],
|
|
1578
|
+
}
|
|
1579
|
+
: {}),
|
|
1580
|
+
...(p ? { path: p[1] } : {}),
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1579
1583
|
}
|
|
1580
1584
|
}
|
|
1581
1585
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1586
|
+
catch {
|
|
1587
|
+
// File read failed — leave httpConfig undefined (default POST)
|
|
1588
|
+
}
|
|
1589
|
+
// Fallback: shell-based wrapper for CLI-style scripts
|
|
1590
|
+
discoveredActions[name] = {
|
|
1591
|
+
tool: {
|
|
1592
|
+
description: `Run the ${name} action. Use: pnpm action ${name} --arg=value`,
|
|
1593
|
+
parameters: {
|
|
1594
|
+
type: "object",
|
|
1595
|
+
properties: {
|
|
1596
|
+
args: {
|
|
1597
|
+
type: "string",
|
|
1598
|
+
description: "CLI arguments as a string (e.g., --metrics=sessions --days=7)",
|
|
1599
|
+
},
|
|
1596
1600
|
},
|
|
1597
1601
|
},
|
|
1598
1602
|
},
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
return
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
}
|
|
1603
|
+
run: async (input) => {
|
|
1604
|
+
const shellEntry = devScriptsForA2A["shell"];
|
|
1605
|
+
if (!shellEntry)
|
|
1606
|
+
return "Error: shell not available";
|
|
1607
|
+
return shellEntry.run({
|
|
1608
|
+
command: `pnpm action ${name} ${input.args || ""}`.trim(),
|
|
1609
|
+
});
|
|
1610
|
+
},
|
|
1611
|
+
...(httpConfig !== undefined ? { http: httpConfig } : {}),
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1610
1614
|
}
|
|
1615
|
+
if (Object.keys(discoveredActions).length > 0 && process.env.DEBUG)
|
|
1616
|
+
console.log(`[agent-chat] Auto-discovered ${Object.keys(discoveredActions).length} action(s): ${Object.keys(discoveredActions).join(", ")}`);
|
|
1611
1617
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1618
|
+
catch { }
|
|
1619
|
+
}
|
|
1620
|
+
// Mutable owner — set per-request by the production handler, read by
|
|
1621
|
+
// automation tools and fetch tool via closure. Declared here (before
|
|
1622
|
+
// allScripts) so the tools are in scope when allScripts is built.
|
|
1623
|
+
let _currentRunOwner = "local@localhost";
|
|
1624
|
+
// Automation tools + fetch tool — depend on _currentRunOwner via callback
|
|
1625
|
+
let automationTools = {};
|
|
1626
|
+
try {
|
|
1627
|
+
const { createAutomationToolEntries } = await import("../triggers/actions.js");
|
|
1628
|
+
automationTools = createAutomationToolEntries(() => _currentRunOwner);
|
|
1614
1629
|
}
|
|
1615
1630
|
catch { }
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
// sometimes emit for actions with complex schemas. Production keeps the
|
|
1621
|
-
// native registration since it has no shell access.
|
|
1622
|
-
const allScripts = canToggle
|
|
1623
|
-
? {
|
|
1624
|
-
...resourceScripts,
|
|
1625
|
-
...docsScripts,
|
|
1626
|
-
...chatScripts,
|
|
1627
|
-
...callAgentScript,
|
|
1628
|
-
...browserTools,
|
|
1629
|
-
...devScriptsForA2A,
|
|
1631
|
+
let notificationTools = {};
|
|
1632
|
+
try {
|
|
1633
|
+
const { createNotificationToolEntries } = await import("../notifications/actions.js");
|
|
1634
|
+
notificationTools = createNotificationToolEntries(() => _currentRunOwner);
|
|
1630
1635
|
}
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
skills: Object.entries(allScripts).map(([name, entry]) => ({
|
|
1651
|
-
id: name,
|
|
1652
|
-
name,
|
|
1653
|
-
description: entry.tool.description,
|
|
1654
|
-
})),
|
|
1655
|
-
streaming: true,
|
|
1656
|
-
handler: async function* (message, context) {
|
|
1657
|
-
// Resolve the caller's identity for user-scoped data access.
|
|
1658
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
1659
|
-
let userEmail;
|
|
1660
|
-
if (isDev) {
|
|
1661
|
-
userEmail = context.metadata?.userEmail || undefined;
|
|
1662
|
-
if (!userEmail) {
|
|
1663
|
-
try {
|
|
1664
|
-
const { getDbExec } = await import("../db/client.js");
|
|
1665
|
-
const db = getDbExec();
|
|
1666
|
-
const { rows } = await db.execute({
|
|
1667
|
-
sql: "SELECT email FROM sessions ORDER BY created_at DESC LIMIT 1",
|
|
1668
|
-
args: [],
|
|
1669
|
-
});
|
|
1670
|
-
if (rows[0])
|
|
1671
|
-
userEmail = rows[0].email;
|
|
1636
|
+
catch { }
|
|
1637
|
+
let progressTools = {};
|
|
1638
|
+
try {
|
|
1639
|
+
const { createProgressToolEntries } = await import("../progress/actions.js");
|
|
1640
|
+
progressTools = createProgressToolEntries(() => _currentRunOwner);
|
|
1641
|
+
}
|
|
1642
|
+
catch { }
|
|
1643
|
+
let fetchTool = {};
|
|
1644
|
+
try {
|
|
1645
|
+
const { createFetchToolEntry } = await import("../tools/fetch-tool.js");
|
|
1646
|
+
const { resolveKeyReferences, validateUrlAllowlist, getKeyAllowlist } = await import("../secrets/substitution.js");
|
|
1647
|
+
fetchTool = createFetchToolEntry({
|
|
1648
|
+
resolveKeys: async (text) => resolveKeyReferences(text, "user", _currentRunOwner),
|
|
1649
|
+
validateUrl: async (url, usedKeys) => {
|
|
1650
|
+
for (const keyName of usedKeys) {
|
|
1651
|
+
const allowlist = await getKeyAllowlist(keyName, "user", _currentRunOwner);
|
|
1652
|
+
if (allowlist && !validateUrlAllowlist(url, allowlist)) {
|
|
1653
|
+
return false;
|
|
1654
|
+
}
|
|
1672
1655
|
}
|
|
1673
|
-
|
|
1674
|
-
}
|
|
1656
|
+
return true;
|
|
1657
|
+
},
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
catch { }
|
|
1661
|
+
// In dev mode, template actions (templateScripts and discoveredActions) are
|
|
1662
|
+
// NOT registered as native tools — the agent invokes them via shell instead.
|
|
1663
|
+
// This avoids degenerate empty-object tool calls that Anthropic models
|
|
1664
|
+
// sometimes emit for actions with complex schemas. Production keeps the
|
|
1665
|
+
// native registration since it has no shell access.
|
|
1666
|
+
const allScripts = canToggle
|
|
1667
|
+
? {
|
|
1668
|
+
...resourceScripts,
|
|
1669
|
+
...docsScripts,
|
|
1670
|
+
...chatScripts,
|
|
1671
|
+
...callAgentScript,
|
|
1672
|
+
...automationTools,
|
|
1673
|
+
...notificationTools,
|
|
1674
|
+
...progressTools,
|
|
1675
|
+
...fetchTool,
|
|
1676
|
+
...browserTools,
|
|
1677
|
+
...devScriptsForA2A,
|
|
1675
1678
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1679
|
+
: {
|
|
1680
|
+
...discoveredActions,
|
|
1681
|
+
...templateScripts,
|
|
1682
|
+
...resourceScripts,
|
|
1683
|
+
...docsScripts,
|
|
1684
|
+
...dbScripts,
|
|
1685
|
+
...refreshScreenTool,
|
|
1686
|
+
...urlTools,
|
|
1687
|
+
...chatScripts,
|
|
1688
|
+
...callAgentScript,
|
|
1689
|
+
...automationTools,
|
|
1690
|
+
...notificationTools,
|
|
1691
|
+
...progressTools,
|
|
1692
|
+
...fetchTool,
|
|
1693
|
+
...browserTools,
|
|
1694
|
+
...devScriptsForA2A,
|
|
1695
|
+
};
|
|
1696
|
+
const { mountA2A } = await import("../a2a/server.js");
|
|
1697
|
+
mountA2A(nitroApp, {
|
|
1698
|
+
name: options?.appId
|
|
1699
|
+
? options.appId.charAt(0).toUpperCase() + options.appId.slice(1)
|
|
1700
|
+
: "Agent",
|
|
1701
|
+
description: `Agent-native ${options?.appId ?? "app"} agent`,
|
|
1702
|
+
skills: Object.entries(allScripts).map(([name, entry]) => ({
|
|
1703
|
+
id: name,
|
|
1704
|
+
name,
|
|
1705
|
+
description: entry.tool.description,
|
|
1706
|
+
})),
|
|
1707
|
+
streaming: true,
|
|
1708
|
+
handler: async function* (message, context) {
|
|
1709
|
+
// Resolve the caller's identity for user-scoped data access.
|
|
1710
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
1711
|
+
let userEmail;
|
|
1712
|
+
if (isDev) {
|
|
1713
|
+
userEmail = context.metadata?.userEmail || undefined;
|
|
1714
|
+
if (!userEmail) {
|
|
1715
|
+
try {
|
|
1716
|
+
const { getDbExec } = await import("../db/client.js");
|
|
1717
|
+
const db = getDbExec();
|
|
1718
|
+
const { rows } = await db.execute({
|
|
1719
|
+
sql: "SELECT email FROM sessions ORDER BY created_at DESC LIMIT 1",
|
|
1720
|
+
args: [],
|
|
1721
|
+
});
|
|
1722
|
+
if (rows[0])
|
|
1723
|
+
userEmail = rows[0].email;
|
|
1724
|
+
}
|
|
1725
|
+
catch { }
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
else {
|
|
1729
|
+
const googleToken = context.metadata?.googleToken;
|
|
1730
|
+
if (googleToken) {
|
|
1731
|
+
try {
|
|
1732
|
+
const res = await fetch(`https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(googleToken)}`);
|
|
1733
|
+
if (res.ok) {
|
|
1734
|
+
const info = (await res.json());
|
|
1735
|
+
if (info.email && info.email_verified === "true") {
|
|
1736
|
+
userEmail = info.email;
|
|
1737
|
+
}
|
|
1685
1738
|
}
|
|
1686
1739
|
}
|
|
1740
|
+
catch { }
|
|
1687
1741
|
}
|
|
1688
|
-
catch { }
|
|
1689
1742
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1743
|
+
if (userEmail) {
|
|
1744
|
+
process.env.AGENT_USER_EMAIL = userEmail;
|
|
1745
|
+
}
|
|
1746
|
+
const text = message.parts
|
|
1747
|
+
.filter((p) => p.type === "text")
|
|
1748
|
+
.map((p) => p.text)
|
|
1749
|
+
.join("\n");
|
|
1750
|
+
if (!text) {
|
|
1751
|
+
yield {
|
|
1752
|
+
role: "agent",
|
|
1753
|
+
parts: [
|
|
1754
|
+
{ type: "text", text: "No text content in message" },
|
|
1755
|
+
],
|
|
1756
|
+
};
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
// Use the SAME agent setup as the interactive chat — identical tools,
|
|
1760
|
+
// prompt, and capabilities. The A2A agent IS the app's agent.
|
|
1761
|
+
const a2aEngine = await resolveEngine({
|
|
1762
|
+
engineOption: options?.engine,
|
|
1763
|
+
apiKey: options?.apiKey,
|
|
1764
|
+
});
|
|
1765
|
+
// Use the same handler (dev or prod) that the interactive chat uses
|
|
1766
|
+
const devActive = isDevMode();
|
|
1767
|
+
const handler = devActive && devHandler ? devHandler : prodHandler;
|
|
1768
|
+
// Build the same system prompt the interactive agent uses
|
|
1769
|
+
const owner = userEmail || "local@localhost";
|
|
1770
|
+
const resources = await loadResourcesForPrompt(owner);
|
|
1771
|
+
const schemaBlock = await buildSchemaBlock(owner, devActive);
|
|
1772
|
+
const systemPrompt = devActive
|
|
1773
|
+
? devPrompt + resources + schemaBlock
|
|
1774
|
+
: basePrompt + resources + schemaBlock;
|
|
1775
|
+
const model = options?.model ??
|
|
1776
|
+
(canToggle ? "claude-sonnet-4-6" : "claude-haiku-4-5-20251001");
|
|
1777
|
+
// Build tools — same as interactive handler but WITHOUT call-agent
|
|
1778
|
+
// to prevent infinite recursive A2A loops (agent calling itself).
|
|
1779
|
+
// In dev mode, template actions are invoked via shell (not native tools),
|
|
1780
|
+
// so they're omitted from the tool registry — see allScripts comment.
|
|
1781
|
+
const a2aActions = devActive
|
|
1782
|
+
? {
|
|
1783
|
+
...resourceScripts,
|
|
1784
|
+
...docsScripts,
|
|
1785
|
+
...chatScripts,
|
|
1786
|
+
...browserTools,
|
|
1787
|
+
...devScriptsForA2A,
|
|
1788
|
+
}
|
|
1789
|
+
: {
|
|
1790
|
+
...templateScripts,
|
|
1791
|
+
...resourceScripts,
|
|
1792
|
+
...docsScripts,
|
|
1793
|
+
...dbScripts,
|
|
1794
|
+
...refreshScreenTool,
|
|
1795
|
+
...urlTools,
|
|
1796
|
+
...chatScripts,
|
|
1797
|
+
...browserTools,
|
|
1798
|
+
};
|
|
1799
|
+
const a2aTools = actionsToEngineTools(a2aActions);
|
|
1800
|
+
const a2aMessages = [
|
|
1801
|
+
{ role: "user", content: [{ type: "text", text }] },
|
|
1802
|
+
];
|
|
1803
|
+
// Run the SAME agent loop, collect text events, yield as A2A messages
|
|
1804
|
+
let accumulatedText = "";
|
|
1805
|
+
const controller = new AbortController();
|
|
1806
|
+
console.log(`[A2A] Starting agent loop: ${a2aTools.length} tools, prompt ${systemPrompt.length} chars`);
|
|
1807
|
+
await runAgentLoop({
|
|
1808
|
+
engine: a2aEngine,
|
|
1809
|
+
model,
|
|
1810
|
+
systemPrompt,
|
|
1811
|
+
tools: a2aTools,
|
|
1812
|
+
messages: a2aMessages,
|
|
1813
|
+
actions: a2aActions,
|
|
1814
|
+
send: (event) => {
|
|
1815
|
+
if (event.type === "text") {
|
|
1816
|
+
accumulatedText += event.text;
|
|
1817
|
+
}
|
|
1818
|
+
else if (event.type === "tool_start") {
|
|
1819
|
+
console.log(`[A2A] Tool call: ${event.tool}`);
|
|
1820
|
+
}
|
|
1821
|
+
else if (event.type === "error") {
|
|
1822
|
+
console.error(`[A2A] Error: ${event.error}`);
|
|
1823
|
+
}
|
|
1824
|
+
else if (event.type === "done") {
|
|
1825
|
+
console.log(`[A2A] Done. Response: ${accumulatedText.length} chars`);
|
|
1826
|
+
}
|
|
1827
|
+
},
|
|
1828
|
+
signal: controller.signal,
|
|
1829
|
+
});
|
|
1830
|
+
console.log(`[A2A] Loop complete. Text: ${accumulatedText.slice(0, 100)}...`);
|
|
1831
|
+
// Yield the final accumulated text
|
|
1699
1832
|
yield {
|
|
1700
1833
|
role: "agent",
|
|
1701
1834
|
parts: [
|
|
1702
|
-
{
|
|
1835
|
+
{
|
|
1836
|
+
type: "text",
|
|
1837
|
+
text: accumulatedText || "(no response)",
|
|
1838
|
+
},
|
|
1703
1839
|
],
|
|
1704
1840
|
};
|
|
1705
|
-
|
|
1841
|
+
},
|
|
1842
|
+
});
|
|
1843
|
+
// Generate an "Available Actions" section from template-specific actions
|
|
1844
|
+
// so the agent knows to use them instead of raw SQL.
|
|
1845
|
+
//
|
|
1846
|
+
// Production: actions are native tools — emit `name(arg*: type) — desc`
|
|
1847
|
+
// Dev: actions are invoked via shell — emit `pnpm action name --arg <type>`
|
|
1848
|
+
// and include discoveredActions too, since those are also missing
|
|
1849
|
+
// from the dev tool registry.
|
|
1850
|
+
const prodActionsPrompt = generateActionsPrompt(templateScripts, "tool");
|
|
1851
|
+
const devActionsPrompt = generateActionsPrompt({ ...discoveredActions, ...templateScripts }, "cli");
|
|
1852
|
+
// Build system prompts — dynamic functions that pre-load resources per-request.
|
|
1853
|
+
// Production gets PROD_FRAMEWORK_PROMPT, dev gets DEV_FRAMEWORK_PROMPT.
|
|
1854
|
+
// Custom systemPrompt from options overrides the framework default entirely.
|
|
1855
|
+
const prodPrompt = (options?.systemPrompt ?? PROD_FRAMEWORK_PROMPT) + prodActionsPrompt;
|
|
1856
|
+
const devPrompt = (options?.devSystemPrompt
|
|
1857
|
+
? options.devSystemPrompt +
|
|
1858
|
+
(options?.systemPrompt ?? PROD_FRAMEWORK_PROMPT)
|
|
1859
|
+
: DEV_FRAMEWORK_PROMPT) + devActionsPrompt;
|
|
1860
|
+
// Keep legacy names for the composition below
|
|
1861
|
+
const basePrompt = prodPrompt;
|
|
1862
|
+
const devPrefix = options?.devSystemPrompt ?? DEFAULT_DEV_PROMPT;
|
|
1863
|
+
// Mount MCP remote server — same action registry as A2A + agent chat
|
|
1864
|
+
const { mountMCP } = await import("../mcp/server.js");
|
|
1865
|
+
mountMCP(nitroApp, {
|
|
1866
|
+
name: options?.appId
|
|
1867
|
+
? options.appId.charAt(0).toUpperCase() + options.appId.slice(1)
|
|
1868
|
+
: "Agent",
|
|
1869
|
+
description: `Agent-native ${options?.appId ?? "app"} agent`,
|
|
1870
|
+
actions: allScripts,
|
|
1871
|
+
askAgent: async (message) => {
|
|
1872
|
+
const mcpEngine = await resolveEngine({
|
|
1873
|
+
engineOption: options?.engine,
|
|
1874
|
+
apiKey: options?.apiKey,
|
|
1875
|
+
});
|
|
1876
|
+
const model = options?.model ??
|
|
1877
|
+
(canToggle ? "claude-sonnet-4-6" : "claude-haiku-4-5-20251001");
|
|
1878
|
+
// Same actions as A2A — without call-agent to prevent loops.
|
|
1879
|
+
// In dev mode, template actions go through shell, not native tools.
|
|
1880
|
+
const devActiveMcp = isDevMode();
|
|
1881
|
+
const mcpActions = devActiveMcp
|
|
1882
|
+
? {
|
|
1883
|
+
...resourceScripts,
|
|
1884
|
+
...docsScripts,
|
|
1885
|
+
...chatScripts,
|
|
1886
|
+
...devScriptsForA2A,
|
|
1887
|
+
}
|
|
1888
|
+
: {
|
|
1889
|
+
...templateScripts,
|
|
1890
|
+
...resourceScripts,
|
|
1891
|
+
...docsScripts,
|
|
1892
|
+
...dbScripts,
|
|
1893
|
+
...refreshScreenTool,
|
|
1894
|
+
...urlTools,
|
|
1895
|
+
...chatScripts,
|
|
1896
|
+
};
|
|
1897
|
+
const mcpTools = actionsToEngineTools(mcpActions);
|
|
1898
|
+
const resources = await loadResourcesForPrompt("local@localhost");
|
|
1899
|
+
const schemaBlock = await buildSchemaBlock("local@localhost", devActiveMcp);
|
|
1900
|
+
const systemPrompt = devActiveMcp
|
|
1901
|
+
? devPrompt + resources + schemaBlock
|
|
1902
|
+
: basePrompt + resources + schemaBlock;
|
|
1903
|
+
let accumulatedText = "";
|
|
1904
|
+
const controller = new AbortController();
|
|
1905
|
+
await runAgentLoop({
|
|
1906
|
+
engine: mcpEngine,
|
|
1907
|
+
model,
|
|
1908
|
+
systemPrompt,
|
|
1909
|
+
tools: mcpTools,
|
|
1910
|
+
messages: [
|
|
1911
|
+
{ role: "user", content: [{ type: "text", text: message }] },
|
|
1912
|
+
],
|
|
1913
|
+
actions: mcpActions,
|
|
1914
|
+
send: (event) => {
|
|
1915
|
+
if (event.type === "text")
|
|
1916
|
+
accumulatedText += event.text;
|
|
1917
|
+
},
|
|
1918
|
+
signal: controller.signal,
|
|
1919
|
+
});
|
|
1920
|
+
return accumulatedText || "(no response)";
|
|
1921
|
+
},
|
|
1922
|
+
});
|
|
1923
|
+
// Resolve owner from the H3 event's session — matches how resources are created
|
|
1924
|
+
const getOwnerFromEvent = async (event) => {
|
|
1925
|
+
try {
|
|
1926
|
+
const session = await getSession(event);
|
|
1927
|
+
return session?.email || "local@localhost";
|
|
1706
1928
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1929
|
+
catch {
|
|
1930
|
+
return "local@localhost";
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
// Auto-mount template actions as HTTP endpoints under /_agent-native/actions/
|
|
1934
|
+
// Include engine management scripts so the UI can call list/set/test-agent-engine.
|
|
1935
|
+
const httpActions = {
|
|
1936
|
+
...discoveredActions,
|
|
1937
|
+
...templateScripts,
|
|
1938
|
+
...engineScripts,
|
|
1939
|
+
};
|
|
1940
|
+
// Framework-level sharing actions — merged with skipExisting semantics so
|
|
1941
|
+
// any template that provides a same-named action wins. When templates use
|
|
1942
|
+
// `loadActionsFromStaticRegistry`, `autoDiscoverActions` never runs, so
|
|
1943
|
+
// this is the single point that guarantees share-resource, unshare-resource,
|
|
1944
|
+
// list-resource-shares, and set-resource-visibility are always mounted.
|
|
1945
|
+
try {
|
|
1946
|
+
const { mergeCoreSharingActions } = await import("./action-discovery.js");
|
|
1947
|
+
await mergeCoreSharingActions(httpActions);
|
|
1948
|
+
}
|
|
1949
|
+
catch {
|
|
1950
|
+
// Ignore — templates without sharing still work.
|
|
1951
|
+
}
|
|
1952
|
+
if (Object.keys(httpActions).length > 0) {
|
|
1953
|
+
const { mountActionRoutes } = await import("./action-routes.js");
|
|
1954
|
+
mountActionRoutes(nitroApp, httpActions, {
|
|
1955
|
+
getOwnerFromEvent,
|
|
1956
|
+
resolveOrgId: options?.resolveOrgId,
|
|
1712
1957
|
});
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
...browserTools,
|
|
1735
|
-
...devScriptsForA2A,
|
|
1736
|
-
}
|
|
1737
|
-
: {
|
|
1738
|
-
...templateScripts,
|
|
1739
|
-
...resourceScripts,
|
|
1740
|
-
...docsScripts,
|
|
1741
|
-
...dbScripts,
|
|
1742
|
-
...refreshScreenTool,
|
|
1743
|
-
...urlTools,
|
|
1744
|
-
...chatScripts,
|
|
1745
|
-
...browserTools,
|
|
1746
|
-
};
|
|
1747
|
-
const a2aTools = actionsToEngineTools(a2aActions);
|
|
1748
|
-
const a2aMessages = [
|
|
1749
|
-
{ role: "user", content: [{ type: "text", text }] },
|
|
1750
|
-
];
|
|
1751
|
-
// Run the SAME agent loop, collect text events, yield as A2A messages
|
|
1752
|
-
let accumulatedText = "";
|
|
1753
|
-
const controller = new AbortController();
|
|
1754
|
-
console.log(`[A2A] Starting agent loop: ${a2aTools.length} tools, prompt ${systemPrompt.length} chars`);
|
|
1755
|
-
await runAgentLoop({
|
|
1756
|
-
engine: a2aEngine,
|
|
1757
|
-
model,
|
|
1758
|
-
systemPrompt,
|
|
1759
|
-
tools: a2aTools,
|
|
1760
|
-
messages: a2aMessages,
|
|
1761
|
-
actions: a2aActions,
|
|
1762
|
-
send: (event) => {
|
|
1763
|
-
if (event.type === "text") {
|
|
1764
|
-
accumulatedText += event.text;
|
|
1958
|
+
}
|
|
1959
|
+
// Callback to persist agent response when run finishes (even if client disconnected).
|
|
1960
|
+
// Reconstructs the assistant message from buffered events and appends to thread_data.
|
|
1961
|
+
const onRunComplete = async (run, threadId) => {
|
|
1962
|
+
if (!threadId)
|
|
1963
|
+
return;
|
|
1964
|
+
// Serialize the read-modify-write against the same thread's other
|
|
1965
|
+
// `thread_data` writers (setThreadQueuedMessages, setThreadEngineMeta,
|
|
1966
|
+
// the frontend-triggered saves below). Without the lock, a concurrent
|
|
1967
|
+
// queued-message save can clobber the assistant message we just
|
|
1968
|
+
// appended here, or vice versa.
|
|
1969
|
+
await withThreadDataLock(threadId, async () => {
|
|
1970
|
+
try {
|
|
1971
|
+
const thread = await getThread(threadId);
|
|
1972
|
+
if (!thread)
|
|
1973
|
+
return;
|
|
1974
|
+
const assistantMsg = buildAssistantMessage(run.events ?? [], run.runId);
|
|
1975
|
+
if (!assistantMsg) {
|
|
1976
|
+
// No content produced — just bump timestamp
|
|
1977
|
+
await updateThreadData(threadId, thread.threadData, thread.title, thread.preview, thread.messageCount);
|
|
1978
|
+
return;
|
|
1765
1979
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1980
|
+
// Parse existing thread_data, append assistant message only if
|
|
1981
|
+
// the frontend hasn't already saved it (avoids duplicates when
|
|
1982
|
+
// the client is still connected during a normal flow).
|
|
1983
|
+
let repo;
|
|
1984
|
+
try {
|
|
1985
|
+
repo = JSON.parse(thread.threadData || "{}");
|
|
1768
1986
|
}
|
|
1769
|
-
|
|
1770
|
-
|
|
1987
|
+
catch {
|
|
1988
|
+
repo = {};
|
|
1771
1989
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1990
|
+
if (!Array.isArray(repo.messages))
|
|
1991
|
+
repo.messages = [];
|
|
1992
|
+
const lastMsg = repo.messages[repo.messages.length - 1];
|
|
1993
|
+
// Check both wrapped ({ message: { role } }) and unwrapped ({ role }) formats
|
|
1994
|
+
const lastRole = lastMsg?.message?.role ?? lastMsg?.role;
|
|
1995
|
+
const lastContent = lastMsg?.message?.content ?? lastMsg?.content;
|
|
1996
|
+
const lastContentIsEmpty = Array.isArray(lastContent)
|
|
1997
|
+
? lastContent.length === 0
|
|
1998
|
+
: lastContent == null || lastContent === "";
|
|
1999
|
+
if (lastRole === "assistant" && !lastContentIsEmpty) {
|
|
2000
|
+
// Frontend already saved the assistant response — just bump timestamp
|
|
2001
|
+
await updateThreadData(threadId, thread.threadData, thread.title, thread.preview, thread.messageCount);
|
|
2002
|
+
return;
|
|
1774
2003
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
// Build system prompts — dynamic functions that pre-load resources per-request.
|
|
1801
|
-
// Production gets PROD_FRAMEWORK_PROMPT, dev gets DEV_FRAMEWORK_PROMPT.
|
|
1802
|
-
// Custom systemPrompt from options overrides the framework default entirely.
|
|
1803
|
-
const prodPrompt = (options?.systemPrompt ?? PROD_FRAMEWORK_PROMPT) + prodActionsPrompt;
|
|
1804
|
-
const devPrompt = (options?.devSystemPrompt
|
|
1805
|
-
? options.devSystemPrompt +
|
|
1806
|
-
(options?.systemPrompt ?? PROD_FRAMEWORK_PROMPT)
|
|
1807
|
-
: DEV_FRAMEWORK_PROMPT) + devActionsPrompt;
|
|
1808
|
-
// Keep legacy names for the composition below
|
|
1809
|
-
const basePrompt = prodPrompt;
|
|
1810
|
-
const devPrefix = options?.devSystemPrompt ?? DEFAULT_DEV_PROMPT;
|
|
1811
|
-
// Mount MCP remote server — same action registry as A2A + agent chat
|
|
1812
|
-
const { mountMCP } = await import("../mcp/server.js");
|
|
1813
|
-
mountMCP(nitroApp, {
|
|
1814
|
-
name: options?.appId
|
|
1815
|
-
? options.appId.charAt(0).toUpperCase() + options.appId.slice(1)
|
|
1816
|
-
: "Agent",
|
|
1817
|
-
description: `Agent-native ${options?.appId ?? "app"} agent`,
|
|
1818
|
-
actions: allScripts,
|
|
1819
|
-
askAgent: async (message) => {
|
|
1820
|
-
const mcpEngine = await resolveEngine({
|
|
1821
|
-
engineOption: options?.engine,
|
|
1822
|
-
apiKey: options?.apiKey,
|
|
2004
|
+
if (lastRole === "assistant" && lastContentIsEmpty) {
|
|
2005
|
+
// The frontend wrote an empty assistant placeholder before the stream
|
|
2006
|
+
// had any content (common when the user reloads mid-run, and the 5s
|
|
2007
|
+
// periodic save raced with the first text chunk). Replace it with
|
|
2008
|
+
// the server's reconstructed message so the turn isn't lost.
|
|
2009
|
+
repo.messages.pop();
|
|
2010
|
+
}
|
|
2011
|
+
// Determine if repo uses wrapped format ({ message, parentId }) or flat format
|
|
2012
|
+
const isWrapped = lastMsg && "message" in lastMsg;
|
|
2013
|
+
if (isWrapped) {
|
|
2014
|
+
const parentId = repo.messages.length > 0
|
|
2015
|
+
? (repo.messages[repo.messages.length - 1].message?.id ??
|
|
2016
|
+
null)
|
|
2017
|
+
: null;
|
|
2018
|
+
repo.messages.push({ message: assistantMsg, parentId });
|
|
2019
|
+
}
|
|
2020
|
+
else {
|
|
2021
|
+
repo.messages.push(assistantMsg);
|
|
2022
|
+
}
|
|
2023
|
+
const meta = extractThreadMeta(repo);
|
|
2024
|
+
await updateThreadData(threadId, JSON.stringify(repo), meta.title || thread.title, meta.preview || thread.preview, repo.messages.length);
|
|
2025
|
+
}
|
|
2026
|
+
catch {
|
|
2027
|
+
// Best-effort — don't break cleanup
|
|
2028
|
+
}
|
|
1823
2029
|
});
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2030
|
+
// Emit agent.turn.completed for automation triggers
|
|
2031
|
+
try {
|
|
2032
|
+
const { emit } = await import("../event-bus/index.js");
|
|
2033
|
+
emit("agent.turn.completed", {
|
|
2034
|
+
threadId,
|
|
2035
|
+
model: resolvedModel,
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
catch {
|
|
2039
|
+
// Event bus not available — skip
|
|
2040
|
+
}
|
|
2041
|
+
// Auto-checkpoint in dev mode after file-modifying agent turns
|
|
2042
|
+
if (isDevMode()) {
|
|
2043
|
+
try {
|
|
2044
|
+
const { createCheckpoint: gitCheckpoint, isGitRepo, hasUncommittedChanges, } = await import("../checkpoints/service.js");
|
|
2045
|
+
const cwd = process.cwd();
|
|
2046
|
+
if (isGitRepo(cwd) && hasUncommittedChanges(cwd)) {
|
|
2047
|
+
const toolNames = new Set();
|
|
2048
|
+
for (const { event } of run.events ?? []) {
|
|
2049
|
+
if (event.type === "tool_start" &&
|
|
2050
|
+
typeof event.tool === "string") {
|
|
2051
|
+
toolNames.add(event.tool);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
const summary = toolNames.size > 0
|
|
2055
|
+
? `Used: ${[...toolNames].join(", ")}`
|
|
2056
|
+
: "Agent turn";
|
|
2057
|
+
const sha = gitCheckpoint(cwd, `[agent-native] ${summary}`);
|
|
2058
|
+
if (sha) {
|
|
2059
|
+
const { insertCheckpoint } = await import("../checkpoints/store.js");
|
|
2060
|
+
const cpId = `cp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2061
|
+
await insertCheckpoint(cpId, threadId, run.runId, sha, summary);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
catch {
|
|
2066
|
+
// Checkpointing is best-effort — never break the run
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
// ─── Agent Teams: per-run send reference ─────────────────────────
|
|
2071
|
+
// Team tools need to emit events to the parent chat's SSE stream.
|
|
2072
|
+
// Each run gets its own send function, keyed by threadId so concurrent
|
|
2073
|
+
// requests for different threads don't clobber each other.
|
|
2074
|
+
const _runSendByThread = new Map();
|
|
2075
|
+
let _currentRunUserApiKey;
|
|
2076
|
+
let _currentRunThreadId = "";
|
|
2077
|
+
let _currentRunSystemPrompt = basePrompt;
|
|
2078
|
+
// Default to Haiku in production mode to manage costs for hosted apps
|
|
2079
|
+
const resolvedModel = options?.model ??
|
|
2080
|
+
(canToggle ? "claude-sonnet-4-6" : "claude-haiku-4-5-20251001");
|
|
2081
|
+
const teamTools = createTeamTools({
|
|
2082
|
+
getOwner: () => _currentRunOwner,
|
|
2083
|
+
getSystemPrompt: () => _currentRunSystemPrompt,
|
|
2084
|
+
getActions: () => isDevMode()
|
|
1830
2085
|
? {
|
|
2086
|
+
// Sub-agents spawned in dev mode also invoke template actions
|
|
2087
|
+
// via shell, so omit them from the native tool registry.
|
|
1831
2088
|
...resourceScripts,
|
|
1832
2089
|
...docsScripts,
|
|
1833
2090
|
...chatScripts,
|
|
@@ -1841,326 +2098,97 @@ export function createAgentChatPlugin(options) {
|
|
|
1841
2098
|
...refreshScreenTool,
|
|
1842
2099
|
...urlTools,
|
|
1843
2100
|
...chatScripts,
|
|
1844
|
-
};
|
|
1845
|
-
const mcpTools = actionsToEngineTools(mcpActions);
|
|
1846
|
-
const resources = await loadResourcesForPrompt("local@localhost");
|
|
1847
|
-
const schemaBlock = await buildSchemaBlock("local@localhost", devActiveMcp);
|
|
1848
|
-
const systemPrompt = devActiveMcp
|
|
1849
|
-
? devPrompt + resources + schemaBlock
|
|
1850
|
-
: basePrompt + resources + schemaBlock;
|
|
1851
|
-
let accumulatedText = "";
|
|
1852
|
-
const controller = new AbortController();
|
|
1853
|
-
await runAgentLoop({
|
|
1854
|
-
engine: mcpEngine,
|
|
1855
|
-
model,
|
|
1856
|
-
systemPrompt,
|
|
1857
|
-
tools: mcpTools,
|
|
1858
|
-
messages: [
|
|
1859
|
-
{ role: "user", content: [{ type: "text", text: message }] },
|
|
1860
|
-
],
|
|
1861
|
-
actions: mcpActions,
|
|
1862
|
-
send: (event) => {
|
|
1863
|
-
if (event.type === "text")
|
|
1864
|
-
accumulatedText += event.text;
|
|
1865
2101
|
},
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2102
|
+
getEngine: () => createAnthropicEngine({
|
|
2103
|
+
// Sub-agents must inherit the parent run's resolved key so a
|
|
2104
|
+
// BYO-key user can't bypass the free-tier check on the parent
|
|
2105
|
+
// run and then have spawn-task delegations bill the platform key.
|
|
2106
|
+
apiKey: _currentRunUserApiKey ??
|
|
2107
|
+
options?.apiKey ??
|
|
2108
|
+
process.env.ANTHROPIC_API_KEY,
|
|
2109
|
+
}),
|
|
2110
|
+
getModel: () => resolvedModel,
|
|
2111
|
+
getParentThreadId: () => _currentRunThreadId,
|
|
2112
|
+
getSend: () => {
|
|
2113
|
+
// Return the send for the current run's thread
|
|
2114
|
+
const send = _runSendByThread.get(_currentRunThreadId);
|
|
2115
|
+
return send ?? null;
|
|
2116
|
+
},
|
|
2117
|
+
});
|
|
2118
|
+
// Hook into the run lifecycle to set/clear the send reference.
|
|
2119
|
+
// Job management tools (create-job, list-jobs, update-job)
|
|
2120
|
+
let jobTools = {};
|
|
1873
2121
|
try {
|
|
1874
|
-
const
|
|
1875
|
-
|
|
1876
|
-
}
|
|
1877
|
-
catch {
|
|
1878
|
-
return "local@localhost";
|
|
2122
|
+
const { createJobTools } = await import("../jobs/tools.js");
|
|
2123
|
+
jobTools = createJobTools();
|
|
1879
2124
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2125
|
+
catch { }
|
|
2126
|
+
const prodActions = {
|
|
2127
|
+
...templateScripts,
|
|
2128
|
+
...resourceScripts,
|
|
2129
|
+
...docsScripts,
|
|
2130
|
+
...dbScripts,
|
|
2131
|
+
...refreshScreenTool,
|
|
2132
|
+
...urlTools,
|
|
2133
|
+
...chatScripts,
|
|
2134
|
+
...callAgentScript,
|
|
2135
|
+
...teamTools,
|
|
2136
|
+
...jobTools,
|
|
2137
|
+
...automationTools,
|
|
2138
|
+
...notificationTools,
|
|
2139
|
+
...progressTools,
|
|
2140
|
+
...fetchTool,
|
|
2141
|
+
...browserTools,
|
|
2142
|
+
...mcpActionEntries,
|
|
2143
|
+
};
|
|
2144
|
+
// Keep the prod action dict's MCP entries in sync when the manager's
|
|
2145
|
+
// server set changes at runtime (e.g. a user adds a remote MCP server
|
|
2146
|
+
// through the settings UI). getEngineTools() in production-agent re-reads
|
|
2147
|
+
// the registry per request, so updates here propagate without restart.
|
|
2148
|
+
mcpManager.onChange(() => {
|
|
2149
|
+
syncMcpActionEntries(mcpManager, prodActions);
|
|
1905
2150
|
});
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
// Serialize the read-modify-write against the same thread's other
|
|
1913
|
-
// `thread_data` writers (setThreadQueuedMessages, setThreadEngineMeta,
|
|
1914
|
-
// the frontend-triggered saves below). Without the lock, a concurrent
|
|
1915
|
-
// queued-message save can clobber the assistant message we just
|
|
1916
|
-
// appended here, or vice versa.
|
|
1917
|
-
await withThreadDataLock(threadId, async () => {
|
|
2151
|
+
// Always build the production handler (includes resource tools + call-agent + team tools)
|
|
2152
|
+
// In production mode (!canToggle), enable usage tracking and limits
|
|
2153
|
+
const isHostedProd = !canToggle;
|
|
2154
|
+
const resolveExtraContext = async (event, owner) => {
|
|
2155
|
+
if (!options?.extraContext)
|
|
2156
|
+
return "";
|
|
1918
2157
|
try {
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
return;
|
|
1922
|
-
const assistantMsg = buildAssistantMessage(run.events ?? [], run.runId);
|
|
1923
|
-
if (!assistantMsg) {
|
|
1924
|
-
// No content produced — just bump timestamp
|
|
1925
|
-
await updateThreadData(threadId, thread.threadData, thread.title, thread.preview, thread.messageCount);
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
// Parse existing thread_data, append assistant message only if
|
|
1929
|
-
// the frontend hasn't already saved it (avoids duplicates when
|
|
1930
|
-
// the client is still connected during a normal flow).
|
|
1931
|
-
let repo;
|
|
1932
|
-
try {
|
|
1933
|
-
repo = JSON.parse(thread.threadData || "{}");
|
|
1934
|
-
}
|
|
1935
|
-
catch {
|
|
1936
|
-
repo = {};
|
|
1937
|
-
}
|
|
1938
|
-
if (!Array.isArray(repo.messages))
|
|
1939
|
-
repo.messages = [];
|
|
1940
|
-
const lastMsg = repo.messages[repo.messages.length - 1];
|
|
1941
|
-
// Check both wrapped ({ message: { role } }) and unwrapped ({ role }) formats
|
|
1942
|
-
const lastRole = lastMsg?.message?.role ?? lastMsg?.role;
|
|
1943
|
-
const lastContent = lastMsg?.message?.content ?? lastMsg?.content;
|
|
1944
|
-
const lastContentIsEmpty = Array.isArray(lastContent)
|
|
1945
|
-
? lastContent.length === 0
|
|
1946
|
-
: lastContent == null || lastContent === "";
|
|
1947
|
-
if (lastRole === "assistant" && !lastContentIsEmpty) {
|
|
1948
|
-
// Frontend already saved the assistant response — just bump timestamp
|
|
1949
|
-
await updateThreadData(threadId, thread.threadData, thread.title, thread.preview, thread.messageCount);
|
|
1950
|
-
return;
|
|
1951
|
-
}
|
|
1952
|
-
if (lastRole === "assistant" && lastContentIsEmpty) {
|
|
1953
|
-
// The frontend wrote an empty assistant placeholder before the stream
|
|
1954
|
-
// had any content (common when the user reloads mid-run, and the 5s
|
|
1955
|
-
// periodic save raced with the first text chunk). Replace it with
|
|
1956
|
-
// the server's reconstructed message so the turn isn't lost.
|
|
1957
|
-
repo.messages.pop();
|
|
1958
|
-
}
|
|
1959
|
-
// Determine if repo uses wrapped format ({ message, parentId }) or flat format
|
|
1960
|
-
const isWrapped = lastMsg && "message" in lastMsg;
|
|
1961
|
-
if (isWrapped) {
|
|
1962
|
-
const parentId = repo.messages.length > 0
|
|
1963
|
-
? (repo.messages[repo.messages.length - 1].message?.id ?? null)
|
|
1964
|
-
: null;
|
|
1965
|
-
repo.messages.push({ message: assistantMsg, parentId });
|
|
1966
|
-
}
|
|
1967
|
-
else {
|
|
1968
|
-
repo.messages.push(assistantMsg);
|
|
1969
|
-
}
|
|
1970
|
-
const meta = extractThreadMeta(repo);
|
|
1971
|
-
await updateThreadData(threadId, JSON.stringify(repo), meta.title || thread.title, meta.preview || thread.preview, repo.messages.length);
|
|
2158
|
+
const extra = await options.extraContext(event, owner);
|
|
2159
|
+
return extra ? `\n\n${extra}` : "";
|
|
1972
2160
|
}
|
|
1973
|
-
catch {
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
});
|
|
1977
|
-
};
|
|
1978
|
-
// ─── Agent Teams: per-run send reference ─────────────────────────
|
|
1979
|
-
// Team tools need to emit events to the parent chat's SSE stream.
|
|
1980
|
-
// Each run gets its own send function, keyed by threadId so concurrent
|
|
1981
|
-
// requests for different threads don't clobber each other.
|
|
1982
|
-
const _runSendByThread = new Map();
|
|
1983
|
-
let _currentRunOwner = "local@localhost";
|
|
1984
|
-
let _currentRunUserApiKey;
|
|
1985
|
-
let _currentRunThreadId = "";
|
|
1986
|
-
let _currentRunSystemPrompt = basePrompt;
|
|
1987
|
-
// Default to Haiku in production mode to manage costs for hosted apps
|
|
1988
|
-
const resolvedModel = options?.model ??
|
|
1989
|
-
(canToggle ? "claude-sonnet-4-6" : "claude-haiku-4-5-20251001");
|
|
1990
|
-
const teamTools = createTeamTools({
|
|
1991
|
-
getOwner: () => _currentRunOwner,
|
|
1992
|
-
getSystemPrompt: () => _currentRunSystemPrompt,
|
|
1993
|
-
getActions: () => isDevMode()
|
|
1994
|
-
? {
|
|
1995
|
-
// Sub-agents spawned in dev mode also invoke template actions
|
|
1996
|
-
// via shell, so omit them from the native tool registry.
|
|
1997
|
-
...resourceScripts,
|
|
1998
|
-
...docsScripts,
|
|
1999
|
-
...chatScripts,
|
|
2000
|
-
...devScriptsForA2A,
|
|
2001
|
-
}
|
|
2002
|
-
: {
|
|
2003
|
-
...templateScripts,
|
|
2004
|
-
...resourceScripts,
|
|
2005
|
-
...docsScripts,
|
|
2006
|
-
...dbScripts,
|
|
2007
|
-
...refreshScreenTool,
|
|
2008
|
-
...urlTools,
|
|
2009
|
-
...chatScripts,
|
|
2010
|
-
},
|
|
2011
|
-
getEngine: () => createAnthropicEngine({
|
|
2012
|
-
// Sub-agents must inherit the parent run's resolved key so a
|
|
2013
|
-
// BYO-key user can't bypass the free-tier check on the parent
|
|
2014
|
-
// run and then have spawn-task delegations bill the platform key.
|
|
2015
|
-
apiKey: _currentRunUserApiKey ??
|
|
2016
|
-
options?.apiKey ??
|
|
2017
|
-
process.env.ANTHROPIC_API_KEY,
|
|
2018
|
-
}),
|
|
2019
|
-
getModel: () => resolvedModel,
|
|
2020
|
-
getParentThreadId: () => _currentRunThreadId,
|
|
2021
|
-
getSend: () => {
|
|
2022
|
-
// Return the send for the current run's thread
|
|
2023
|
-
const send = _runSendByThread.get(_currentRunThreadId);
|
|
2024
|
-
return send ?? null;
|
|
2025
|
-
},
|
|
2026
|
-
});
|
|
2027
|
-
// Hook into the run lifecycle to set/clear the send reference.
|
|
2028
|
-
// Job management tools (create-job, list-jobs, update-job)
|
|
2029
|
-
let jobTools = {};
|
|
2030
|
-
try {
|
|
2031
|
-
const { createJobTools } = await import("../jobs/tools.js");
|
|
2032
|
-
jobTools = createJobTools();
|
|
2033
|
-
}
|
|
2034
|
-
catch { }
|
|
2035
|
-
const prodActions = {
|
|
2036
|
-
...templateScripts,
|
|
2037
|
-
...resourceScripts,
|
|
2038
|
-
...docsScripts,
|
|
2039
|
-
...dbScripts,
|
|
2040
|
-
...refreshScreenTool,
|
|
2041
|
-
...urlTools,
|
|
2042
|
-
...chatScripts,
|
|
2043
|
-
...callAgentScript,
|
|
2044
|
-
...teamTools,
|
|
2045
|
-
...jobTools,
|
|
2046
|
-
...browserTools,
|
|
2047
|
-
...mcpActionEntries,
|
|
2048
|
-
};
|
|
2049
|
-
// Keep the prod action dict's MCP entries in sync when the manager's
|
|
2050
|
-
// server set changes at runtime (e.g. a user adds a remote MCP server
|
|
2051
|
-
// through the settings UI). getEngineTools() in production-agent re-reads
|
|
2052
|
-
// the registry per request, so updates here propagate without restart.
|
|
2053
|
-
mcpManager.onChange(() => {
|
|
2054
|
-
syncMcpActionEntries(mcpManager, prodActions);
|
|
2055
|
-
});
|
|
2056
|
-
// Always build the production handler (includes resource tools + call-agent + team tools)
|
|
2057
|
-
// In production mode (!canToggle), enable usage tracking and limits
|
|
2058
|
-
const isHostedProd = !canToggle;
|
|
2059
|
-
const resolveExtraContext = async (event, owner) => {
|
|
2060
|
-
if (!options?.extraContext)
|
|
2061
|
-
return "";
|
|
2062
|
-
try {
|
|
2063
|
-
const extra = await options.extraContext(event, owner);
|
|
2064
|
-
return extra ? `\n\n${extra}` : "";
|
|
2065
|
-
}
|
|
2066
|
-
catch (err) {
|
|
2067
|
-
console.warn("[agent-chat] extraContext threw:", err instanceof Error ? err.message : err);
|
|
2068
|
-
return "";
|
|
2069
|
-
}
|
|
2070
|
-
};
|
|
2071
|
-
const leanPrompt = options?.leanPrompt === true;
|
|
2072
|
-
// Lean mode: use only the template's systemPrompt + actions list.
|
|
2073
|
-
// Skip resource loading, schema block, and extraContext — those add
|
|
2074
|
-
// DB round-trips and tokens that minimal/voice apps don't need.
|
|
2075
|
-
const leanBasePrompt = (options?.systemPrompt ?? "") + prodActionsPrompt;
|
|
2076
|
-
const prodHandler = createProductionAgentHandler({
|
|
2077
|
-
actions: prodActions,
|
|
2078
|
-
systemPrompt: async (event) => {
|
|
2079
|
-
_currentRequestOrigin = getOrigin(event);
|
|
2080
|
-
const owner = await getOwnerFromEvent(event);
|
|
2081
|
-
_currentRunOwner = owner;
|
|
2082
|
-
const { getOwnerAnthropicApiKey } = await import("../agent/production-agent.js");
|
|
2083
|
-
_currentRunUserApiKey = await getOwnerAnthropicApiKey(owner);
|
|
2084
|
-
if (leanPrompt) {
|
|
2085
|
-
_currentRunSystemPrompt = leanBasePrompt;
|
|
2086
|
-
return _currentRunSystemPrompt;
|
|
2161
|
+
catch (err) {
|
|
2162
|
+
console.warn("[agent-chat] extraContext threw:", err instanceof Error ? err.message : err);
|
|
2163
|
+
return "";
|
|
2087
2164
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
apiKey: options?.apiKey,
|
|
2097
|
-
skipFilesContext: leanPrompt,
|
|
2098
|
-
onRunStart: (send, threadId) => {
|
|
2099
|
-
_runSendByThread.set(threadId, send);
|
|
2100
|
-
_currentRunThreadId = threadId;
|
|
2101
|
-
},
|
|
2102
|
-
onRunComplete: async (run, threadId) => {
|
|
2103
|
-
if (threadId)
|
|
2104
|
-
_runSendByThread.delete(threadId);
|
|
2105
|
-
await onRunComplete(run, threadId);
|
|
2106
|
-
},
|
|
2107
|
-
// Usage tracking for hosted production deployments
|
|
2108
|
-
trackUsage: isHostedProd,
|
|
2109
|
-
resolveOwnerEmail: isHostedProd ? getOwnerFromEvent : undefined,
|
|
2110
|
-
});
|
|
2111
|
-
// Build the dev handler (with filesystem/shell/db tools) if environment allows toggling
|
|
2112
|
-
let devHandler = null;
|
|
2113
|
-
if (canToggle) {
|
|
2114
|
-
const { createDevScriptRegistry } = await import("../scripts/dev/index.js");
|
|
2115
|
-
// Dev mode: template actions (templateScripts and discoveredActions) are
|
|
2116
|
-
// intentionally OMITTED from the native tool registry. The agent invokes
|
|
2117
|
-
// them via `shell(command="pnpm action <name> ...")` instead. This mirrors
|
|
2118
|
-
// how Claude Code works locally and dramatically reduces the rate of
|
|
2119
|
-
// degenerate empty-object tool calls. The CLI syntax for each action is
|
|
2120
|
-
// listed in the dev system prompt's "Available Actions" section.
|
|
2121
|
-
// In lean mode, expose the template's actions directly as native tools
|
|
2122
|
-
// instead of routing through shell — the lean system prompt has no
|
|
2123
|
-
// shell-usage guidance, so shell-based action invocation would break.
|
|
2124
|
-
const devActions = leanPrompt
|
|
2125
|
-
? prodActions
|
|
2126
|
-
: {
|
|
2127
|
-
...resourceScripts,
|
|
2128
|
-
...docsScripts,
|
|
2129
|
-
...chatScripts,
|
|
2130
|
-
...callAgentScript,
|
|
2131
|
-
...teamTools,
|
|
2132
|
-
...jobTools,
|
|
2133
|
-
...browserTools,
|
|
2134
|
-
...mcpActionEntries,
|
|
2135
|
-
...(await createDevScriptRegistry()),
|
|
2136
|
-
};
|
|
2137
|
-
// Keep dev action dict in sync with runtime MCP additions. When
|
|
2138
|
-
// leanPrompt is true, devActions === prodActions so the prod listener
|
|
2139
|
-
// already covers it.
|
|
2140
|
-
if (devActions !== prodActions) {
|
|
2141
|
-
mcpManager.onChange(() => {
|
|
2142
|
-
syncMcpActionEntries(mcpManager, devActions);
|
|
2143
|
-
});
|
|
2144
|
-
}
|
|
2145
|
-
devHandler = createProductionAgentHandler({
|
|
2146
|
-
actions: devActions,
|
|
2165
|
+
};
|
|
2166
|
+
const leanPrompt = options?.leanPrompt === true;
|
|
2167
|
+
// Lean mode: use only the template's systemPrompt + actions list.
|
|
2168
|
+
// Skip resource loading, schema block, and extraContext — those add
|
|
2169
|
+
// DB round-trips and tokens that minimal/voice apps don't need.
|
|
2170
|
+
const leanBasePrompt = (options?.systemPrompt ?? "") + prodActionsPrompt;
|
|
2171
|
+
const prodHandler = createProductionAgentHandler({
|
|
2172
|
+
actions: prodActions,
|
|
2147
2173
|
systemPrompt: async (event) => {
|
|
2148
2174
|
_currentRequestOrigin = getOrigin(event);
|
|
2149
2175
|
const owner = await getOwnerFromEvent(event);
|
|
2150
2176
|
_currentRunOwner = owner;
|
|
2151
|
-
const {
|
|
2152
|
-
_currentRunUserApiKey = await
|
|
2177
|
+
const { getOwnerActiveApiKey } = await import("../agent/production-agent.js");
|
|
2178
|
+
_currentRunUserApiKey = await getOwnerActiveApiKey(owner);
|
|
2153
2179
|
if (leanPrompt) {
|
|
2154
2180
|
_currentRunSystemPrompt = leanBasePrompt;
|
|
2155
2181
|
return _currentRunSystemPrompt;
|
|
2156
2182
|
}
|
|
2157
2183
|
const resources = await loadResourcesForPrompt(owner);
|
|
2158
|
-
const schemaBlock = await buildSchemaBlock(owner,
|
|
2184
|
+
const schemaBlock = await buildSchemaBlock(owner, false);
|
|
2159
2185
|
const extra = await resolveExtraContext(event, owner);
|
|
2160
|
-
_currentRunSystemPrompt =
|
|
2186
|
+
_currentRunSystemPrompt =
|
|
2187
|
+
basePrompt + resources + schemaBlock + extra;
|
|
2161
2188
|
return _currentRunSystemPrompt;
|
|
2162
2189
|
},
|
|
2163
|
-
model: options?.model
|
|
2190
|
+
model: options?.model ??
|
|
2191
|
+
(isHostedProd ? "claude-haiku-4-5-20251001" : undefined),
|
|
2164
2192
|
apiKey: options?.apiKey,
|
|
2165
2193
|
skipFilesContext: leanPrompt,
|
|
2166
2194
|
onRunStart: (send, threadId) => {
|
|
@@ -2172,763 +2200,982 @@ export function createAgentChatPlugin(options) {
|
|
|
2172
2200
|
_runSendByThread.delete(threadId);
|
|
2173
2201
|
await onRunComplete(run, threadId);
|
|
2174
2202
|
},
|
|
2203
|
+
// Usage tracking for hosted production deployments
|
|
2204
|
+
trackUsage: isHostedProd,
|
|
2205
|
+
resolveOwnerEmail: isHostedProd ? getOwnerFromEvent : undefined,
|
|
2175
2206
|
});
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2207
|
+
// Build the dev handler (with filesystem/shell/db tools) if environment allows toggling
|
|
2208
|
+
let devHandler = null;
|
|
2209
|
+
if (canToggle) {
|
|
2210
|
+
const { createDevScriptRegistry } = await import("../scripts/dev/index.js");
|
|
2211
|
+
// Dev mode: template actions (templateScripts and discoveredActions) are
|
|
2212
|
+
// intentionally OMITTED from the native tool registry. The agent invokes
|
|
2213
|
+
// them via `shell(command="pnpm action <name> ...")` instead. This mirrors
|
|
2214
|
+
// how Claude Code works locally and dramatically reduces the rate of
|
|
2215
|
+
// degenerate empty-object tool calls. The CLI syntax for each action is
|
|
2216
|
+
// listed in the dev system prompt's "Available Actions" section.
|
|
2217
|
+
// In lean mode, expose the template's actions directly as native tools
|
|
2218
|
+
// instead of routing through shell — the lean system prompt has no
|
|
2219
|
+
// shell-usage guidance, so shell-based action invocation would break.
|
|
2220
|
+
const devActions = leanPrompt
|
|
2221
|
+
? prodActions
|
|
2222
|
+
: {
|
|
2223
|
+
...resourceScripts,
|
|
2224
|
+
...docsScripts,
|
|
2225
|
+
...chatScripts,
|
|
2226
|
+
...callAgentScript,
|
|
2227
|
+
...teamTools,
|
|
2228
|
+
...jobTools,
|
|
2229
|
+
...automationTools,
|
|
2230
|
+
...notificationTools,
|
|
2231
|
+
...progressTools,
|
|
2232
|
+
...fetchTool,
|
|
2233
|
+
...browserTools,
|
|
2234
|
+
...mcpActionEntries,
|
|
2235
|
+
...(await createDevScriptRegistry()),
|
|
2236
|
+
};
|
|
2237
|
+
// Keep dev action dict in sync with runtime MCP additions. When
|
|
2238
|
+
// leanPrompt is true, devActions === prodActions so the prod listener
|
|
2239
|
+
// already covers it.
|
|
2240
|
+
if (devActions !== prodActions) {
|
|
2241
|
+
mcpManager.onChange(() => {
|
|
2242
|
+
syncMcpActionEntries(mcpManager, devActions);
|
|
2243
|
+
});
|
|
2194
2244
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2245
|
+
devHandler = createProductionAgentHandler({
|
|
2246
|
+
actions: devActions,
|
|
2247
|
+
systemPrompt: async (event) => {
|
|
2248
|
+
_currentRequestOrigin = getOrigin(event);
|
|
2249
|
+
const owner = await getOwnerFromEvent(event);
|
|
2250
|
+
_currentRunOwner = owner;
|
|
2251
|
+
const { getOwnerActiveApiKey } = await import("../agent/production-agent.js");
|
|
2252
|
+
_currentRunUserApiKey = await getOwnerActiveApiKey(owner);
|
|
2253
|
+
if (leanPrompt) {
|
|
2254
|
+
_currentRunSystemPrompt = leanBasePrompt;
|
|
2255
|
+
return _currentRunSystemPrompt;
|
|
2256
|
+
}
|
|
2257
|
+
const resources = await loadResourcesForPrompt(owner);
|
|
2258
|
+
const schemaBlock = await buildSchemaBlock(owner, true);
|
|
2259
|
+
const extra = await resolveExtraContext(event, owner);
|
|
2260
|
+
_currentRunSystemPrompt =
|
|
2261
|
+
devPrompt + resources + schemaBlock + extra;
|
|
2262
|
+
return _currentRunSystemPrompt;
|
|
2263
|
+
},
|
|
2264
|
+
model: options?.model,
|
|
2265
|
+
apiKey: options?.apiKey,
|
|
2266
|
+
skipFilesContext: leanPrompt,
|
|
2267
|
+
onRunStart: (send, threadId) => {
|
|
2268
|
+
_runSendByThread.set(threadId, send);
|
|
2269
|
+
_currentRunThreadId = threadId;
|
|
2270
|
+
},
|
|
2271
|
+
onRunComplete: async (run, threadId) => {
|
|
2272
|
+
if (threadId)
|
|
2273
|
+
_runSendByThread.delete(threadId);
|
|
2274
|
+
await onRunComplete(run, threadId);
|
|
2275
|
+
},
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
// Resolve mention providers
|
|
2279
|
+
const rawProviders = options?.mentionProviders;
|
|
2280
|
+
const mentionProviders = typeof rawProviders === "function"
|
|
2281
|
+
? await rawProviders()
|
|
2282
|
+
: (rawProviders ?? {});
|
|
2283
|
+
// currentDevMode + persistence were hoisted to the top of this function
|
|
2284
|
+
// so every closure built below can close over the live flag.
|
|
2285
|
+
// Mount mode endpoint — GET returns current mode, POST toggles it (localhost only)
|
|
2286
|
+
getH3App(nitroApp).use(`${routePath}/mode`, defineEventHandler(async (event) => {
|
|
2287
|
+
if (getMethod(event) === "POST") {
|
|
2288
|
+
if (!canToggle) {
|
|
2289
|
+
setResponseStatus(event, 403);
|
|
2290
|
+
return { error: "Mode switching not available in production" };
|
|
2291
|
+
}
|
|
2292
|
+
if (!isLocalhost(event)) {
|
|
2293
|
+
setResponseStatus(event, 403);
|
|
2294
|
+
return { error: "Mode switching only available on localhost" };
|
|
2295
|
+
}
|
|
2296
|
+
const body = await readBody(event);
|
|
2297
|
+
if (typeof body?.devMode === "boolean") {
|
|
2298
|
+
currentDevMode = body.devMode;
|
|
2299
|
+
}
|
|
2300
|
+
else {
|
|
2301
|
+
currentDevMode = !currentDevMode;
|
|
2302
|
+
}
|
|
2303
|
+
try {
|
|
2304
|
+
await putSetting(AGENT_MODE_SETTING_KEY, {
|
|
2305
|
+
devMode: currentDevMode,
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
catch {
|
|
2309
|
+
// Persistence is best-effort — in-memory flag still applies for
|
|
2310
|
+
// the lifetime of this process even if the settings write fails.
|
|
2311
|
+
}
|
|
2312
|
+
return { devMode: currentDevMode, canToggle };
|
|
2198
2313
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2314
|
+
return { devMode: currentDevMode, canToggle };
|
|
2315
|
+
}));
|
|
2316
|
+
// Mount save-key BEFORE the prefix handler so it isn't shadowed.
|
|
2317
|
+
// Persists the user's key per-owner in the SQL settings table so it
|
|
2318
|
+
// survives across serverless invocations (where mutating process.env
|
|
2319
|
+
// and writing .env are both no-ops). Also updates process.env and
|
|
2320
|
+
// .env when running locally for fast pickup by other handlers.
|
|
2321
|
+
getH3App(nitroApp).use(`${routePath}/save-key`, defineEventHandler(async (event) => {
|
|
2322
|
+
if (getMethod(event) !== "POST") {
|
|
2323
|
+
setResponseStatus(event, 405);
|
|
2324
|
+
return { error: "Method not allowed" };
|
|
2201
2325
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2326
|
+
const body = await readBody(event);
|
|
2327
|
+
const { key, provider: rawProvider } = body;
|
|
2328
|
+
const provider = rawProvider || "anthropic";
|
|
2329
|
+
if (!key || typeof key !== "string" || !key.trim()) {
|
|
2330
|
+
setResponseStatus(event, 400);
|
|
2331
|
+
return { error: "API key is required" };
|
|
2206
2332
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2333
|
+
const trimmedKey = key.trim();
|
|
2334
|
+
// Persist per-owner so the key survives cold starts in serverless
|
|
2335
|
+
// and so the user's key isn't shared across users on multi-tenant
|
|
2336
|
+
// hosted deployments. We require a real authenticated owner here —
|
|
2337
|
+
// `local@localhost` is the unauthenticated fallback and must never
|
|
2338
|
+
// become the shared key bucket on hosted deployments.
|
|
2339
|
+
const ownerEmail = await getOwnerFromEvent(event);
|
|
2340
|
+
if (isHostedProd &&
|
|
2341
|
+
(!ownerEmail || ownerEmail === "local@localhost")) {
|
|
2342
|
+
setResponseStatus(event, 401);
|
|
2343
|
+
return { error: "Authentication required" };
|
|
2210
2344
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
const ownerEmail = await getOwnerFromEvent(event);
|
|
2238
|
-
if (isHostedProd && (!ownerEmail || ownerEmail === "local@localhost")) {
|
|
2239
|
-
setResponseStatus(event, 401);
|
|
2240
|
-
return { error: "Authentication required" };
|
|
2241
|
-
}
|
|
2242
|
-
if (ownerEmail && ownerEmail !== "local@localhost") {
|
|
2243
|
-
try {
|
|
2244
|
-
await putSetting(`user-anthropic-api-key:${ownerEmail}`, {
|
|
2245
|
-
key: trimmedKey,
|
|
2246
|
-
});
|
|
2247
|
-
// Verify the write actually landed — some managed DB drivers
|
|
2248
|
-
// swallow errors on degraded connections. Without this the
|
|
2249
|
-
// client sees "saved", reloads, and the usage-limit card
|
|
2250
|
-
// re-appears on the next message because the key isn't
|
|
2251
|
-
// really persisted.
|
|
2252
|
-
const check = await getSetting(`user-anthropic-api-key:${ownerEmail}`);
|
|
2253
|
-
if (!check ||
|
|
2254
|
-
typeof check.key !== "string" ||
|
|
2255
|
-
check.key !== trimmedKey) {
|
|
2256
|
-
throw new Error("settings write did not persist");
|
|
2345
|
+
if (ownerEmail && ownerEmail !== "local@localhost") {
|
|
2346
|
+
try {
|
|
2347
|
+
await putSetting(`user-api-key:${provider}:${ownerEmail}`, {
|
|
2348
|
+
key: trimmedKey,
|
|
2349
|
+
});
|
|
2350
|
+
// Verify the write actually landed — some managed DB drivers
|
|
2351
|
+
// swallow errors on degraded connections. Without this the
|
|
2352
|
+
// client sees "saved", reloads, and the usage-limit card
|
|
2353
|
+
// re-appears on the next message because the key isn't
|
|
2354
|
+
// really persisted.
|
|
2355
|
+
const check = await getSetting(`user-api-key:${provider}:${ownerEmail}`);
|
|
2356
|
+
if (!check ||
|
|
2357
|
+
typeof check.key !== "string" ||
|
|
2358
|
+
check.key !== trimmedKey) {
|
|
2359
|
+
throw new Error("settings write did not persist");
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
catch (err) {
|
|
2363
|
+
if (isHostedProd) {
|
|
2364
|
+
console.error("[agent-chat] save-key persistence failed:", err instanceof Error ? err.message : err);
|
|
2365
|
+
setResponseStatus(event, 500);
|
|
2366
|
+
return {
|
|
2367
|
+
error: "Failed to persist API key. Please try again or contact support.",
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
// Local dev falls through to the env-file path below.
|
|
2257
2371
|
}
|
|
2258
2372
|
}
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2373
|
+
// In hosted/multi-tenant mode we deliberately do NOT touch
|
|
2374
|
+
// process.env or .env: the per-owner SQL lookup above is the
|
|
2375
|
+
// single source of truth, and overwriting the shared env key
|
|
2376
|
+
// would leak one tenant's credentials into every subsequent
|
|
2377
|
+
// request that hit the same warm instance without its own key.
|
|
2378
|
+
if (!isHostedProd) {
|
|
2379
|
+
const providerToEnv = {
|
|
2380
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
2381
|
+
openai: "OPENAI_API_KEY",
|
|
2382
|
+
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
2383
|
+
groq: "GROQ_API_KEY",
|
|
2384
|
+
mistral: "MISTRAL_API_KEY",
|
|
2385
|
+
cohere: "COHERE_API_KEY",
|
|
2386
|
+
};
|
|
2387
|
+
const envVar = providerToEnv[provider] ?? `${provider.toUpperCase()}_API_KEY`;
|
|
2388
|
+
try {
|
|
2389
|
+
const path = await import("path");
|
|
2390
|
+
const { upsertEnvFile } = await import("./create-server.js");
|
|
2391
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
2392
|
+
await upsertEnvFile(envPath, [
|
|
2393
|
+
{ key: envVar, value: trimmedKey },
|
|
2394
|
+
]);
|
|
2266
2395
|
}
|
|
2267
|
-
|
|
2396
|
+
catch {
|
|
2397
|
+
// Edge runtime — can't write .env, but can still update process.env
|
|
2398
|
+
}
|
|
2399
|
+
// Update process.env so the agent works immediately in the
|
|
2400
|
+
// current local-dev invocation; the SQL persist above covers
|
|
2401
|
+
// future invocations.
|
|
2402
|
+
process.env[envVar] = trimmedKey;
|
|
2268
2403
|
}
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
//
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
try {
|
|
2277
|
-
const path = await import("path");
|
|
2278
|
-
const { upsertEnvFile } = await import("./create-server.js");
|
|
2279
|
-
const envPath = path.join(process.cwd(), ".env");
|
|
2280
|
-
await upsertEnvFile(envPath, [
|
|
2281
|
-
{ key: "ANTHROPIC_API_KEY", value: trimmedKey },
|
|
2282
|
-
]);
|
|
2404
|
+
return { ok: true };
|
|
2405
|
+
}));
|
|
2406
|
+
// Mount file search endpoint
|
|
2407
|
+
getH3App(nitroApp).use(`${routePath}/files`, defineEventHandler(async (event) => {
|
|
2408
|
+
if (getMethod(event) !== "GET") {
|
|
2409
|
+
setResponseStatus(event, 405);
|
|
2410
|
+
return { error: "Method not allowed" };
|
|
2283
2411
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2412
|
+
const query = getQuery(event);
|
|
2413
|
+
const q = typeof query.q === "string" ? query.q.toLowerCase() : "";
|
|
2414
|
+
const files = [];
|
|
2415
|
+
const seen = new Set();
|
|
2416
|
+
// In dev mode, walk the filesystem
|
|
2417
|
+
if (currentDevMode) {
|
|
2418
|
+
const codebaseFiles = [];
|
|
2419
|
+
try {
|
|
2420
|
+
await collectFiles(process.cwd(), "", 0, codebaseFiles);
|
|
2421
|
+
}
|
|
2422
|
+
catch {
|
|
2423
|
+
// Filesystem access failed — skip
|
|
2424
|
+
}
|
|
2425
|
+
for (const f of codebaseFiles) {
|
|
2426
|
+
if (!seen.has(f.path)) {
|
|
2427
|
+
seen.add(f.path);
|
|
2428
|
+
files.push({
|
|
2429
|
+
path: f.path,
|
|
2430
|
+
name: f.name,
|
|
2431
|
+
source: "codebase",
|
|
2432
|
+
type: f.type,
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2286
2436
|
}
|
|
2287
|
-
//
|
|
2288
|
-
// current local-dev invocation; the SQL persist above covers
|
|
2289
|
-
// future invocations.
|
|
2290
|
-
process.env.ANTHROPIC_API_KEY = trimmedKey;
|
|
2291
|
-
}
|
|
2292
|
-
return { ok: true };
|
|
2293
|
-
}));
|
|
2294
|
-
// Mount file search endpoint
|
|
2295
|
-
getH3App(nitroApp).use(`${routePath}/files`, defineEventHandler(async (event) => {
|
|
2296
|
-
if (getMethod(event) !== "GET") {
|
|
2297
|
-
setResponseStatus(event, 405);
|
|
2298
|
-
return { error: "Method not allowed" };
|
|
2299
|
-
}
|
|
2300
|
-
const query = getQuery(event);
|
|
2301
|
-
const q = typeof query.q === "string" ? query.q.toLowerCase() : "";
|
|
2302
|
-
const files = [];
|
|
2303
|
-
const seen = new Set();
|
|
2304
|
-
// In dev mode, walk the filesystem
|
|
2305
|
-
if (currentDevMode) {
|
|
2306
|
-
const codebaseFiles = [];
|
|
2437
|
+
// Query resources
|
|
2307
2438
|
try {
|
|
2308
|
-
|
|
2439
|
+
const resources = currentDevMode
|
|
2440
|
+
? await resourceListAccessible("local@localhost")
|
|
2441
|
+
: await resourceList(SHARED_OWNER);
|
|
2442
|
+
for (const r of resources) {
|
|
2443
|
+
if (!seen.has(r.path)) {
|
|
2444
|
+
seen.add(r.path);
|
|
2445
|
+
files.push({
|
|
2446
|
+
path: r.path,
|
|
2447
|
+
name: r.path.split("/").pop() || r.path,
|
|
2448
|
+
source: "resource",
|
|
2449
|
+
type: "file",
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2309
2453
|
}
|
|
2310
2454
|
catch {
|
|
2311
|
-
//
|
|
2455
|
+
// Resources not available — skip
|
|
2312
2456
|
}
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2457
|
+
// Filter by query and limit
|
|
2458
|
+
const filtered = q
|
|
2459
|
+
? files.filter((f) => f.path.toLowerCase().includes(q))
|
|
2460
|
+
: files;
|
|
2461
|
+
return { files: filtered.slice(0, 30) };
|
|
2462
|
+
}));
|
|
2463
|
+
// Mount skills listing endpoint
|
|
2464
|
+
getH3App(nitroApp).use(`${routePath}/skills`, defineEventHandler(async (event) => {
|
|
2465
|
+
if (getMethod(event) !== "GET") {
|
|
2466
|
+
setResponseStatus(event, 405);
|
|
2467
|
+
return { error: "Method not allowed" };
|
|
2323
2468
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
files.push({
|
|
2334
|
-
path: r.path,
|
|
2335
|
-
name: r.path.split("/").pop() || r.path,
|
|
2336
|
-
source: "resource",
|
|
2337
|
-
type: "file",
|
|
2469
|
+
const skills = [];
|
|
2470
|
+
const seenNames = new Set();
|
|
2471
|
+
// In dev mode, scan .agents/skills/ directory
|
|
2472
|
+
if (currentDevMode) {
|
|
2473
|
+
try {
|
|
2474
|
+
const _fs = await lazyFs();
|
|
2475
|
+
const skillsDir = nodePath.join(process.cwd(), ".agents", "skills");
|
|
2476
|
+
const entries = _fs.readdirSync(skillsDir, {
|
|
2477
|
+
withFileTypes: true,
|
|
2338
2478
|
});
|
|
2479
|
+
for (const entry of entries) {
|
|
2480
|
+
// Support both flat .md files and subdirectory-based skills (dir/SKILL.md)
|
|
2481
|
+
let skillFilePath;
|
|
2482
|
+
let skillRelPath;
|
|
2483
|
+
if (entry.isDirectory()) {
|
|
2484
|
+
// Subdirectory layout: .agents/skills/<name>/SKILL.md
|
|
2485
|
+
const candidate = nodePath.join(skillsDir, entry.name, "SKILL.md");
|
|
2486
|
+
if (!_fs.existsSync(candidate))
|
|
2487
|
+
continue;
|
|
2488
|
+
skillFilePath = candidate;
|
|
2489
|
+
skillRelPath = `.agents/skills/${entry.name}/SKILL.md`;
|
|
2490
|
+
}
|
|
2491
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2492
|
+
// Flat layout: .agents/skills/<name>.md
|
|
2493
|
+
skillFilePath = nodePath.join(skillsDir, entry.name);
|
|
2494
|
+
skillRelPath = `.agents/skills/${entry.name}`;
|
|
2495
|
+
}
|
|
2496
|
+
else {
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
try {
|
|
2500
|
+
const content = _fs.readFileSync(skillFilePath, "utf-8");
|
|
2501
|
+
const fm = parseSkillFrontmatter(content);
|
|
2502
|
+
const skillName = fm.name || entry.name.replace(/\.md$/, "");
|
|
2503
|
+
if (!seenNames.has(skillName)) {
|
|
2504
|
+
seenNames.add(skillName);
|
|
2505
|
+
skills.push({
|
|
2506
|
+
name: skillName,
|
|
2507
|
+
description: fm.description,
|
|
2508
|
+
path: skillRelPath,
|
|
2509
|
+
source: "codebase",
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
catch {
|
|
2514
|
+
// Could not read individual skill file — skip
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
catch {
|
|
2519
|
+
// .agents/skills/ directory doesn't exist or not readable — skip
|
|
2339
2520
|
}
|
|
2340
2521
|
}
|
|
2341
|
-
|
|
2342
|
-
catch {
|
|
2343
|
-
// Resources not available — skip
|
|
2344
|
-
}
|
|
2345
|
-
// Filter by query and limit
|
|
2346
|
-
const filtered = q
|
|
2347
|
-
? files.filter((f) => f.path.toLowerCase().includes(q))
|
|
2348
|
-
: files;
|
|
2349
|
-
return { files: filtered.slice(0, 30) };
|
|
2350
|
-
}));
|
|
2351
|
-
// Mount skills listing endpoint
|
|
2352
|
-
getH3App(nitroApp).use(`${routePath}/skills`, defineEventHandler(async (event) => {
|
|
2353
|
-
if (getMethod(event) !== "GET") {
|
|
2354
|
-
setResponseStatus(event, 405);
|
|
2355
|
-
return { error: "Method not allowed" };
|
|
2356
|
-
}
|
|
2357
|
-
const skills = [];
|
|
2358
|
-
const seenNames = new Set();
|
|
2359
|
-
// In dev mode, scan .agents/skills/ directory
|
|
2360
|
-
if (currentDevMode) {
|
|
2522
|
+
// Query resources with skills/ prefix
|
|
2361
2523
|
try {
|
|
2362
|
-
const
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
let skillFilePath;
|
|
2370
|
-
let skillRelPath;
|
|
2371
|
-
if (entry.isDirectory()) {
|
|
2372
|
-
// Subdirectory layout: .agents/skills/<name>/SKILL.md
|
|
2373
|
-
const candidate = nodePath.join(skillsDir, entry.name, "SKILL.md");
|
|
2374
|
-
if (!_fs.existsSync(candidate))
|
|
2375
|
-
continue;
|
|
2376
|
-
skillFilePath = candidate;
|
|
2377
|
-
skillRelPath = `.agents/skills/${entry.name}/SKILL.md`;
|
|
2378
|
-
}
|
|
2379
|
-
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2380
|
-
// Flat layout: .agents/skills/<name>.md
|
|
2381
|
-
skillFilePath = nodePath.join(skillsDir, entry.name);
|
|
2382
|
-
skillRelPath = `.agents/skills/${entry.name}`;
|
|
2383
|
-
}
|
|
2384
|
-
else {
|
|
2385
|
-
continue;
|
|
2386
|
-
}
|
|
2524
|
+
const resourceSkills = currentDevMode
|
|
2525
|
+
? await resourceListAccessible("local@localhost", "skills/")
|
|
2526
|
+
: await resourceList(SHARED_OWNER, "skills/");
|
|
2527
|
+
for (const r of resourceSkills) {
|
|
2528
|
+
// Try to get content to parse frontmatter
|
|
2529
|
+
let skillName = r.path.split("/").pop()?.replace(/\.md$/, "") || r.path;
|
|
2530
|
+
let description;
|
|
2387
2531
|
try {
|
|
2388
|
-
const
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
name: skillName,
|
|
2395
|
-
description: fm.description,
|
|
2396
|
-
path: skillRelPath,
|
|
2397
|
-
source: "codebase",
|
|
2398
|
-
});
|
|
2532
|
+
const full = await resourceGet(r.id);
|
|
2533
|
+
if (full) {
|
|
2534
|
+
const fm = parseSkillFrontmatter(full.content);
|
|
2535
|
+
if (fm.name)
|
|
2536
|
+
skillName = fm.name;
|
|
2537
|
+
description = fm.description;
|
|
2399
2538
|
}
|
|
2400
2539
|
}
|
|
2401
2540
|
catch {
|
|
2402
|
-
// Could not read
|
|
2541
|
+
// Could not read resource content — use path-based name
|
|
2542
|
+
}
|
|
2543
|
+
if (!seenNames.has(skillName)) {
|
|
2544
|
+
seenNames.add(skillName);
|
|
2545
|
+
skills.push({
|
|
2546
|
+
name: skillName,
|
|
2547
|
+
description,
|
|
2548
|
+
path: r.path,
|
|
2549
|
+
source: "resource",
|
|
2550
|
+
});
|
|
2403
2551
|
}
|
|
2404
2552
|
}
|
|
2405
2553
|
}
|
|
2406
2554
|
catch {
|
|
2407
|
-
//
|
|
2555
|
+
// Resources not available — skip
|
|
2408
2556
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
? await resourceListAccessible("local@localhost", "skills/")
|
|
2414
|
-
: await resourceList(SHARED_OWNER, "skills/");
|
|
2415
|
-
for (const r of resourceSkills) {
|
|
2416
|
-
// Try to get content to parse frontmatter
|
|
2417
|
-
let skillName = r.path.split("/").pop()?.replace(/\.md$/, "") || r.path;
|
|
2418
|
-
let description;
|
|
2419
|
-
try {
|
|
2420
|
-
const full = await resourceGet(r.id);
|
|
2421
|
-
if (full) {
|
|
2422
|
-
const fm = parseSkillFrontmatter(full.content);
|
|
2423
|
-
if (fm.name)
|
|
2424
|
-
skillName = fm.name;
|
|
2425
|
-
description = fm.description;
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
|
-
catch {
|
|
2429
|
-
// Could not read resource content — use path-based name
|
|
2430
|
-
}
|
|
2431
|
-
if (!seenNames.has(skillName)) {
|
|
2432
|
-
seenNames.add(skillName);
|
|
2433
|
-
skills.push({
|
|
2434
|
-
name: skillName,
|
|
2435
|
-
description,
|
|
2436
|
-
path: r.path,
|
|
2437
|
-
source: "resource",
|
|
2438
|
-
});
|
|
2439
|
-
}
|
|
2557
|
+
const result = { skills };
|
|
2558
|
+
if (skills.length === 0) {
|
|
2559
|
+
result.hint =
|
|
2560
|
+
"No skills found. Add skill files under skills/ in Resources. Learn more: https://agent-native.com/docs/resources#skills";
|
|
2440
2561
|
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
if (toSend.length > 0) {
|
|
2481
|
-
totalSent += toSend.length;
|
|
2482
|
-
try {
|
|
2483
|
-
controller.enqueue(enc.encode(JSON.stringify({ items: toSend }) + "\n"));
|
|
2562
|
+
return result;
|
|
2563
|
+
}));
|
|
2564
|
+
// Mount unified mentions endpoint (files + resources + custom providers)
|
|
2565
|
+
getH3App(nitroApp).use(`${routePath}/mentions`, defineEventHandler(async (event) => {
|
|
2566
|
+
if (getMethod(event) !== "GET") {
|
|
2567
|
+
setResponseStatus(event, 405);
|
|
2568
|
+
return { error: "Method not allowed" };
|
|
2569
|
+
}
|
|
2570
|
+
const query = getQuery(event);
|
|
2571
|
+
const q = typeof query.q === "string" ? query.q.toLowerCase() : "";
|
|
2572
|
+
const matchesQuery = (item) => !q ||
|
|
2573
|
+
item.label.toLowerCase().includes(q) ||
|
|
2574
|
+
(item.description?.toLowerCase().includes(q) ?? false);
|
|
2575
|
+
const enc = new TextEncoder();
|
|
2576
|
+
// Stream NDJSON — each source flushes its batch as soon as it's ready.
|
|
2577
|
+
setResponseHeader(event, "Content-Type", "application/x-ndjson");
|
|
2578
|
+
setResponseHeader(event, "Cache-Control", "no-cache");
|
|
2579
|
+
const stream = new ReadableStream({
|
|
2580
|
+
async start(controller) {
|
|
2581
|
+
const MAX_RESULTS = 50;
|
|
2582
|
+
let totalSent = 0;
|
|
2583
|
+
let cancelled = false;
|
|
2584
|
+
const flush = (batch) => {
|
|
2585
|
+
if (cancelled)
|
|
2586
|
+
return;
|
|
2587
|
+
const filtered = batch.filter(matchesQuery);
|
|
2588
|
+
if (filtered.length === 0)
|
|
2589
|
+
return;
|
|
2590
|
+
const remaining = MAX_RESULTS - totalSent;
|
|
2591
|
+
const toSend = filtered.slice(0, remaining);
|
|
2592
|
+
if (toSend.length > 0) {
|
|
2593
|
+
totalSent += toSend.length;
|
|
2594
|
+
try {
|
|
2595
|
+
controller.enqueue(enc.encode(JSON.stringify({ items: toSend }) + "\n"));
|
|
2596
|
+
}
|
|
2597
|
+
catch {
|
|
2598
|
+
// Stream was closed by client
|
|
2599
|
+
cancelled = true;
|
|
2600
|
+
}
|
|
2484
2601
|
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2602
|
+
};
|
|
2603
|
+
// All sources run in parallel; each flushes independently.
|
|
2604
|
+
const sources = [];
|
|
2605
|
+
// 1. Resources from SQL (fast — flush first)
|
|
2606
|
+
sources.push((async () => {
|
|
2607
|
+
try {
|
|
2608
|
+
const resources = currentDevMode
|
|
2609
|
+
? await resourceListAccessible("local@localhost")
|
|
2610
|
+
: await resourceList(SHARED_OWNER);
|
|
2611
|
+
flush(resources.map((r) => {
|
|
2612
|
+
const isShared = r.owner === SHARED_OWNER;
|
|
2613
|
+
return {
|
|
2614
|
+
id: `resource:${r.path}`,
|
|
2615
|
+
label: r.path.split("/").pop() || r.path,
|
|
2616
|
+
description: r.path,
|
|
2617
|
+
icon: "file",
|
|
2618
|
+
source: isShared
|
|
2619
|
+
? "resource:shared"
|
|
2620
|
+
: "resource:private",
|
|
2621
|
+
refType: "file",
|
|
2622
|
+
refPath: r.path,
|
|
2623
|
+
section: "Files",
|
|
2624
|
+
};
|
|
2625
|
+
}));
|
|
2488
2626
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
icon: "file",
|
|
2506
|
-
source: isShared
|
|
2507
|
-
? "resource:shared"
|
|
2508
|
-
: "resource:private",
|
|
2627
|
+
catch { }
|
|
2628
|
+
})());
|
|
2629
|
+
// 2. Codebase files (dev mode only — can be slow on large repos)
|
|
2630
|
+
if (currentDevMode) {
|
|
2631
|
+
sources.push((async () => {
|
|
2632
|
+
const codebaseFiles = [];
|
|
2633
|
+
try {
|
|
2634
|
+
await collectFiles(process.cwd(), "", 0, codebaseFiles);
|
|
2635
|
+
}
|
|
2636
|
+
catch { }
|
|
2637
|
+
flush(codebaseFiles.map((f) => ({
|
|
2638
|
+
id: `codebase:${f.path}`,
|
|
2639
|
+
label: f.name,
|
|
2640
|
+
description: f.path !== f.name ? f.path : undefined,
|
|
2641
|
+
icon: f.type,
|
|
2642
|
+
source: "codebase",
|
|
2509
2643
|
refType: "file",
|
|
2510
|
-
refPath:
|
|
2644
|
+
refPath: f.path,
|
|
2511
2645
|
section: "Files",
|
|
2512
|
-
};
|
|
2513
|
-
}));
|
|
2646
|
+
})));
|
|
2647
|
+
})());
|
|
2514
2648
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2649
|
+
// 3. Custom mention providers (each flushes independently)
|
|
2650
|
+
for (const [key, provider] of Object.entries(mentionProviders)) {
|
|
2651
|
+
sources.push((async () => {
|
|
2652
|
+
try {
|
|
2653
|
+
const providerItems = await provider.search(q, event);
|
|
2654
|
+
flush(providerItems.map((item) => ({
|
|
2655
|
+
id: item.id,
|
|
2656
|
+
label: item.label,
|
|
2657
|
+
description: item.description,
|
|
2658
|
+
icon: item.icon || provider.icon || "file",
|
|
2659
|
+
source: key,
|
|
2660
|
+
refType: item.refType,
|
|
2661
|
+
refPath: item.refPath,
|
|
2662
|
+
refId: item.refId,
|
|
2663
|
+
section: provider.label,
|
|
2664
|
+
})));
|
|
2665
|
+
}
|
|
2666
|
+
catch (e) {
|
|
2667
|
+
console.error(`[agent-native] Mention provider "${key}" failed:`, e);
|
|
2668
|
+
}
|
|
2669
|
+
})());
|
|
2670
|
+
}
|
|
2671
|
+
// 4. Custom workspace agents
|
|
2519
2672
|
sources.push((async () => {
|
|
2520
|
-
const codebaseFiles = [];
|
|
2521
2673
|
try {
|
|
2522
|
-
|
|
2674
|
+
const owner = await getOwnerFromEvent(event);
|
|
2675
|
+
const { listAccessibleCustomAgents } = await import("../resources/agents.js");
|
|
2676
|
+
const agents = await listAccessibleCustomAgents(owner);
|
|
2677
|
+
flush(agents.map((agent) => ({
|
|
2678
|
+
id: `custom-agent:${agent.id}`,
|
|
2679
|
+
label: agent.name,
|
|
2680
|
+
description: agent.description || agent.path,
|
|
2681
|
+
icon: "agent",
|
|
2682
|
+
source: "agent:custom",
|
|
2683
|
+
refType: "custom-agent",
|
|
2684
|
+
refPath: agent.path,
|
|
2685
|
+
refId: agent.id,
|
|
2686
|
+
section: "Agents",
|
|
2687
|
+
})));
|
|
2688
|
+
}
|
|
2689
|
+
catch (e) {
|
|
2690
|
+
console.error("[agent-native] Custom agent discovery failed:", e);
|
|
2523
2691
|
}
|
|
2524
|
-
catch { }
|
|
2525
|
-
flush(codebaseFiles.map((f) => ({
|
|
2526
|
-
id: `codebase:${f.path}`,
|
|
2527
|
-
label: f.name,
|
|
2528
|
-
description: f.path !== f.name ? f.path : undefined,
|
|
2529
|
-
icon: f.type,
|
|
2530
|
-
source: "codebase",
|
|
2531
|
-
refType: "file",
|
|
2532
|
-
refPath: f.path,
|
|
2533
|
-
section: "Files",
|
|
2534
|
-
})));
|
|
2535
2692
|
})());
|
|
2536
|
-
|
|
2537
|
-
// 3. Custom mention providers (each flushes independently)
|
|
2538
|
-
for (const [key, provider] of Object.entries(mentionProviders)) {
|
|
2693
|
+
// 5. Peer agent discovery (network call — often slowest)
|
|
2539
2694
|
sources.push((async () => {
|
|
2540
2695
|
try {
|
|
2541
|
-
const
|
|
2542
|
-
flush(
|
|
2543
|
-
id:
|
|
2544
|
-
label:
|
|
2545
|
-
description:
|
|
2546
|
-
icon:
|
|
2547
|
-
source:
|
|
2548
|
-
refType:
|
|
2549
|
-
refPath:
|
|
2550
|
-
refId:
|
|
2551
|
-
section:
|
|
2696
|
+
const agents = await discoverAgents(options?.appId);
|
|
2697
|
+
flush(agents.map((agent) => ({
|
|
2698
|
+
id: `agent:${agent.id}`,
|
|
2699
|
+
label: agent.name,
|
|
2700
|
+
description: agent.description,
|
|
2701
|
+
icon: "agent",
|
|
2702
|
+
source: "agent",
|
|
2703
|
+
refType: "agent",
|
|
2704
|
+
refPath: agent.url,
|
|
2705
|
+
refId: agent.id,
|
|
2706
|
+
section: "Connected Agents",
|
|
2552
2707
|
})));
|
|
2553
2708
|
}
|
|
2554
2709
|
catch (e) {
|
|
2555
|
-
console.error(
|
|
2710
|
+
console.error("[agent-native] Agent discovery failed:", e);
|
|
2556
2711
|
}
|
|
2557
2712
|
})());
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
const agents = await listAccessibleCustomAgents(owner);
|
|
2565
|
-
flush(agents.map((agent) => ({
|
|
2566
|
-
id: `custom-agent:${agent.id}`,
|
|
2567
|
-
label: agent.name,
|
|
2568
|
-
description: agent.description || agent.path,
|
|
2569
|
-
icon: "agent",
|
|
2570
|
-
source: "agent:custom",
|
|
2571
|
-
refType: "custom-agent",
|
|
2572
|
-
refPath: agent.path,
|
|
2573
|
-
refId: agent.id,
|
|
2574
|
-
section: "Agents",
|
|
2575
|
-
})));
|
|
2576
|
-
}
|
|
2577
|
-
catch (e) {
|
|
2578
|
-
console.error("[agent-native] Custom agent discovery failed:", e);
|
|
2579
|
-
}
|
|
2580
|
-
})());
|
|
2581
|
-
// 5. Peer agent discovery (network call — often slowest)
|
|
2582
|
-
sources.push((async () => {
|
|
2583
|
-
try {
|
|
2584
|
-
const agents = await discoverAgents(options?.appId);
|
|
2585
|
-
flush(agents.map((agent) => ({
|
|
2586
|
-
id: `agent:${agent.id}`,
|
|
2587
|
-
label: agent.name,
|
|
2588
|
-
description: agent.description,
|
|
2589
|
-
icon: "agent",
|
|
2590
|
-
source: "agent",
|
|
2591
|
-
refType: "agent",
|
|
2592
|
-
refPath: agent.url,
|
|
2593
|
-
refId: agent.id,
|
|
2594
|
-
section: "Connected Agents",
|
|
2595
|
-
})));
|
|
2596
|
-
}
|
|
2597
|
-
catch (e) {
|
|
2598
|
-
console.error("[agent-native] Agent discovery failed:", e);
|
|
2599
|
-
}
|
|
2600
|
-
})());
|
|
2601
|
-
await Promise.all(sources);
|
|
2602
|
-
if (!cancelled)
|
|
2603
|
-
controller.close();
|
|
2604
|
-
},
|
|
2605
|
-
cancel() {
|
|
2606
|
-
// Client disconnected — stop enqueuing
|
|
2607
|
-
},
|
|
2608
|
-
});
|
|
2609
|
-
return stream;
|
|
2610
|
-
}));
|
|
2611
|
-
// ─── Generate thread title ──────────────────────────────────────────
|
|
2612
|
-
getH3App(nitroApp).use(`${routePath}/generate-title`, defineEventHandler(async (event) => {
|
|
2613
|
-
if (getMethod(event) !== "POST") {
|
|
2614
|
-
setResponseStatus(event, 405);
|
|
2615
|
-
return { error: "Method not allowed" };
|
|
2616
|
-
}
|
|
2617
|
-
const ownerEmail = await getOwnerFromEvent(event);
|
|
2618
|
-
const body = await readBody(event);
|
|
2619
|
-
const message = body?.message;
|
|
2620
|
-
if (!message || typeof message !== "string") {
|
|
2621
|
-
setResponseStatus(event, 400);
|
|
2622
|
-
return { error: "message is required" };
|
|
2623
|
-
}
|
|
2624
|
-
// Strip mention markup: @[Name|type] → @Name
|
|
2625
|
-
const cleanMessage = message.replace(/@\[([^\]|]+)\|[^\]]*\]/g, "@$1");
|
|
2626
|
-
// Mirror the chat-run resolution so BYO-key users have title
|
|
2627
|
-
// generation billed to their own key instead of the platform key.
|
|
2628
|
-
const { getOwnerAnthropicApiKey } = await import("../agent/production-agent.js");
|
|
2629
|
-
const userApiKey = await getOwnerAnthropicApiKey(ownerEmail);
|
|
2630
|
-
const apiKey = userApiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
2631
|
-
if (!apiKey) {
|
|
2632
|
-
// Fallback: truncate the message
|
|
2633
|
-
return { title: cleanMessage.trim().slice(0, 60) };
|
|
2634
|
-
}
|
|
2635
|
-
try {
|
|
2636
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
2637
|
-
method: "POST",
|
|
2638
|
-
headers: {
|
|
2639
|
-
"Content-Type": "application/json",
|
|
2640
|
-
"x-api-key": apiKey,
|
|
2641
|
-
"anthropic-version": "2023-06-01",
|
|
2713
|
+
await Promise.all(sources);
|
|
2714
|
+
if (!cancelled)
|
|
2715
|
+
controller.close();
|
|
2716
|
+
},
|
|
2717
|
+
cancel() {
|
|
2718
|
+
// Client disconnected — stop enqueuing
|
|
2642
2719
|
},
|
|
2643
|
-
body: JSON.stringify({
|
|
2644
|
-
model: "claude-haiku-4-5-20251001",
|
|
2645
|
-
max_tokens: 30,
|
|
2646
|
-
messages: [
|
|
2647
|
-
{
|
|
2648
|
-
role: "user",
|
|
2649
|
-
content: `Generate a very short title (3-6 words, no quotes) for a chat that starts with this message:\n\n${cleanMessage.slice(0, 500)}`,
|
|
2650
|
-
},
|
|
2651
|
-
],
|
|
2652
|
-
}),
|
|
2653
2720
|
});
|
|
2654
|
-
|
|
2721
|
+
return stream;
|
|
2722
|
+
}));
|
|
2723
|
+
// ─── Generate thread title ──────────────────────────────────────────
|
|
2724
|
+
getH3App(nitroApp).use(`${routePath}/generate-title`, defineEventHandler(async (event) => {
|
|
2725
|
+
if (getMethod(event) !== "POST") {
|
|
2726
|
+
setResponseStatus(event, 405);
|
|
2727
|
+
return { error: "Method not allowed" };
|
|
2728
|
+
}
|
|
2729
|
+
const ownerEmail = await getOwnerFromEvent(event);
|
|
2730
|
+
const body = await readBody(event);
|
|
2731
|
+
const message = body?.message;
|
|
2732
|
+
if (!message || typeof message !== "string") {
|
|
2733
|
+
setResponseStatus(event, 400);
|
|
2734
|
+
return { error: "message is required" };
|
|
2735
|
+
}
|
|
2736
|
+
// Strip mention markup: @[Name|type] → @Name
|
|
2737
|
+
const cleanMessage = message.replace(/@\[([^\]|]+)\|[^\]]*\]/g, "@$1");
|
|
2738
|
+
// Mirror the chat-run resolution so BYO-key users have title
|
|
2739
|
+
// generation billed to their own key instead of the platform key.
|
|
2740
|
+
const { getOwnerActiveApiKey } = await import("../agent/production-agent.js");
|
|
2741
|
+
const userApiKey = await getOwnerActiveApiKey(ownerEmail);
|
|
2742
|
+
const apiKey = userApiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
2743
|
+
if (!apiKey) {
|
|
2744
|
+
// Fallback: truncate the message
|
|
2655
2745
|
return { title: cleanMessage.trim().slice(0, 60) };
|
|
2656
2746
|
}
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
// Match both full URL (/runs/{id}/events) and h3 prefix-stripped (/{id}/events)
|
|
2683
|
-
const eventsMatch = url.match(/\/runs\/([^/?]+)\/events/) ||
|
|
2684
|
-
url.match(/^\/([^/?]+)\/events/);
|
|
2685
|
-
if (eventsMatch && method === "GET") {
|
|
2686
|
-
const runId = decodeURIComponent(eventsMatch[1]);
|
|
2687
|
-
const query = getQuery(event);
|
|
2688
|
-
const after = parseInt(String(query.after ?? "0"), 10) || 0;
|
|
2689
|
-
const stream = subscribeToRun(runId, after);
|
|
2690
|
-
if (!stream) {
|
|
2691
|
-
setResponseStatus(event, 404);
|
|
2692
|
-
return { error: "Run not found" };
|
|
2747
|
+
try {
|
|
2748
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
2749
|
+
method: "POST",
|
|
2750
|
+
headers: {
|
|
2751
|
+
"Content-Type": "application/json",
|
|
2752
|
+
"x-api-key": apiKey,
|
|
2753
|
+
"anthropic-version": "2023-06-01",
|
|
2754
|
+
},
|
|
2755
|
+
body: JSON.stringify({
|
|
2756
|
+
model: "claude-haiku-4-5-20251001",
|
|
2757
|
+
max_tokens: 30,
|
|
2758
|
+
messages: [
|
|
2759
|
+
{
|
|
2760
|
+
role: "user",
|
|
2761
|
+
content: `Generate a very short title (3-6 words, no quotes) for a chat that starts with this message:\n\n${cleanMessage.slice(0, 500)}`,
|
|
2762
|
+
},
|
|
2763
|
+
],
|
|
2764
|
+
}),
|
|
2765
|
+
});
|
|
2766
|
+
if (!res.ok) {
|
|
2767
|
+
return { title: cleanMessage.trim().slice(0, 60) };
|
|
2768
|
+
}
|
|
2769
|
+
const data = (await res.json());
|
|
2770
|
+
const text = data.content?.[0]?.text?.trim();
|
|
2771
|
+
return { title: text || cleanMessage.trim().slice(0, 60) };
|
|
2693
2772
|
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
setResponseHeader(event, "Connection", "keep-alive");
|
|
2697
|
-
return stream;
|
|
2698
|
-
}
|
|
2699
|
-
// Route: GET /runs/active?threadId=X
|
|
2700
|
-
if (method === "GET") {
|
|
2701
|
-
const query = getQuery(event);
|
|
2702
|
-
const threadId = query.threadId ? String(query.threadId) : null;
|
|
2703
|
-
if (!threadId) {
|
|
2704
|
-
setResponseStatus(event, 400);
|
|
2705
|
-
return { error: "threadId query parameter is required" };
|
|
2773
|
+
catch {
|
|
2774
|
+
return { title: cleanMessage.trim().slice(0, 60) };
|
|
2706
2775
|
}
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2776
|
+
}));
|
|
2777
|
+
// ─── Run management endpoints (for hot-reload resilience) ─────────────
|
|
2778
|
+
// GET /runs/active?threadId=X — check if there's an active run for a thread
|
|
2779
|
+
getH3App(nitroApp).use(`${routePath}/runs`, defineEventHandler(async (event) => {
|
|
2780
|
+
// Auth check — ensure the user is authenticated
|
|
2781
|
+
await getOwnerFromEvent(event);
|
|
2782
|
+
const method = getMethod(event);
|
|
2783
|
+
const url = event.node?.req?.url || event.path || "";
|
|
2784
|
+
// Route: POST /runs/:id/abort
|
|
2785
|
+
// Match both full URL (/runs/{id}/abort) and h3 prefix-stripped (/{id}/abort)
|
|
2786
|
+
const abortMatch = url.match(/\/runs\/([^/?]+)\/abort/) ||
|
|
2787
|
+
url.match(/^\/([^/?]+)\/abort/);
|
|
2788
|
+
if (abortMatch && method === "POST") {
|
|
2789
|
+
const runId = decodeURIComponent(abortMatch[1]);
|
|
2790
|
+
abortRun(runId); // Aborts in-memory + marks aborted in SQL
|
|
2791
|
+
return { ok: true };
|
|
2712
2792
|
}
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
// We also check the original URL as a fallback.
|
|
2733
|
-
const remainder = (event.path || "").replace(/^\/+/, "");
|
|
2734
|
-
const fromUrl = (event.node?.req?.url || "").match(/\/threads\/([^/?]+)/);
|
|
2735
|
-
const threadId = remainder
|
|
2736
|
-
? decodeURIComponent(remainder.split("?")[0].split("/")[0])
|
|
2737
|
-
: fromUrl
|
|
2738
|
-
? decodeURIComponent(fromUrl[1])
|
|
2739
|
-
: null;
|
|
2740
|
-
// ── Specific thread: GET/PUT/DELETE /threads/:id ──
|
|
2741
|
-
if (threadId) {
|
|
2793
|
+
// Route: GET /runs/:id/events?after=N
|
|
2794
|
+
// Match both full URL (/runs/{id}/events) and h3 prefix-stripped (/{id}/events)
|
|
2795
|
+
const eventsMatch = url.match(/\/runs\/([^/?]+)\/events/) ||
|
|
2796
|
+
url.match(/^\/([^/?]+)\/events/);
|
|
2797
|
+
if (eventsMatch && method === "GET") {
|
|
2798
|
+
const runId = decodeURIComponent(eventsMatch[1]);
|
|
2799
|
+
const query = getQuery(event);
|
|
2800
|
+
const after = parseInt(String(query.after ?? "0"), 10) || 0;
|
|
2801
|
+
const stream = subscribeToRun(runId, after);
|
|
2802
|
+
if (!stream) {
|
|
2803
|
+
setResponseStatus(event, 404);
|
|
2804
|
+
return { error: "Run not found" };
|
|
2805
|
+
}
|
|
2806
|
+
setResponseHeader(event, "Content-Type", "text/event-stream");
|
|
2807
|
+
setResponseHeader(event, "Cache-Control", "no-cache");
|
|
2808
|
+
setResponseHeader(event, "Connection", "keep-alive");
|
|
2809
|
+
return stream;
|
|
2810
|
+
}
|
|
2811
|
+
// Route: GET /runs/active?threadId=X
|
|
2742
2812
|
if (method === "GET") {
|
|
2813
|
+
const query = getQuery(event);
|
|
2814
|
+
const threadId = query.threadId ? String(query.threadId) : null;
|
|
2815
|
+
if (!threadId) {
|
|
2816
|
+
setResponseStatus(event, 400);
|
|
2817
|
+
return { error: "threadId query parameter is required" };
|
|
2818
|
+
}
|
|
2819
|
+
// Check in-memory first, then SQL (cross-isolate on Workers)
|
|
2820
|
+
const run = await getActiveRunForThreadAsync(threadId);
|
|
2821
|
+
if (!run) {
|
|
2822
|
+
setResponseStatus(event, 404);
|
|
2823
|
+
return { error: "No active run for this thread" };
|
|
2824
|
+
}
|
|
2825
|
+
return {
|
|
2826
|
+
runId: run.runId,
|
|
2827
|
+
threadId: run.threadId,
|
|
2828
|
+
status: run.status,
|
|
2829
|
+
heartbeatAt: run.heartbeatAt,
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
setResponseStatus(event, 405);
|
|
2833
|
+
return { error: "Method not allowed" };
|
|
2834
|
+
}));
|
|
2835
|
+
// ─── Checkpoint endpoints ──────────────────────────────────────────────
|
|
2836
|
+
getH3App(nitroApp).use(`${routePath}/checkpoints`, defineEventHandler(async (event) => {
|
|
2837
|
+
const method = getMethod(event);
|
|
2838
|
+
// GET /checkpoints?threadId=... — list checkpoints for a thread
|
|
2839
|
+
if (method === "GET") {
|
|
2840
|
+
if (!canToggle) {
|
|
2841
|
+
setResponseStatus(event, 403);
|
|
2842
|
+
return { error: "Checkpoints only available in dev mode" };
|
|
2843
|
+
}
|
|
2844
|
+
if (!isLocalhost(event)) {
|
|
2845
|
+
setResponseStatus(event, 403);
|
|
2846
|
+
return { error: "Checkpoints only available on localhost" };
|
|
2847
|
+
}
|
|
2848
|
+
const query = getQuery(event);
|
|
2849
|
+
const threadId = String(query.threadId || "");
|
|
2850
|
+
if (!threadId) {
|
|
2851
|
+
setResponseStatus(event, 400);
|
|
2852
|
+
return { error: "threadId query parameter is required" };
|
|
2853
|
+
}
|
|
2854
|
+
const owner = await getOwnerFromEvent(event);
|
|
2743
2855
|
const thread = await getThread(threadId);
|
|
2744
2856
|
if (!thread || thread.ownerEmail !== owner) {
|
|
2745
2857
|
setResponseStatus(event, 404);
|
|
2746
2858
|
return { error: "Thread not found" };
|
|
2747
2859
|
}
|
|
2748
|
-
|
|
2860
|
+
try {
|
|
2861
|
+
const { getCheckpointsByThread } = await import("../checkpoints/store.js");
|
|
2862
|
+
return await getCheckpointsByThread(threadId);
|
|
2863
|
+
}
|
|
2864
|
+
catch {
|
|
2865
|
+
return [];
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
// POST /checkpoints — restore to a checkpoint
|
|
2869
|
+
// h3 prefix-matches, so /checkpoints/restore hits this handler with
|
|
2870
|
+
// event.path containing "/restore".
|
|
2871
|
+
const remainder = (event.path || "").replace(/^\/+/, "");
|
|
2872
|
+
if (method === "POST" && remainder.startsWith("restore")) {
|
|
2873
|
+
if (!canToggle) {
|
|
2874
|
+
setResponseStatus(event, 403);
|
|
2875
|
+
return { error: "Checkpoints only available in dev mode" };
|
|
2876
|
+
}
|
|
2877
|
+
if (!isLocalhost(event)) {
|
|
2878
|
+
setResponseStatus(event, 403);
|
|
2879
|
+
return { error: "Restore only available on localhost" };
|
|
2880
|
+
}
|
|
2881
|
+
const body = await readBody(event);
|
|
2882
|
+
const checkpointId = body?.checkpointId;
|
|
2883
|
+
if (!checkpointId) {
|
|
2884
|
+
setResponseStatus(event, 400);
|
|
2885
|
+
return { error: "checkpointId is required" };
|
|
2886
|
+
}
|
|
2887
|
+
try {
|
|
2888
|
+
const { getCheckpointById } = await import("../checkpoints/store.js");
|
|
2889
|
+
const checkpoint = await getCheckpointById(checkpointId);
|
|
2890
|
+
if (!checkpoint) {
|
|
2891
|
+
setResponseStatus(event, 404);
|
|
2892
|
+
return { error: "Checkpoint not found" };
|
|
2893
|
+
}
|
|
2894
|
+
const owner = await getOwnerFromEvent(event);
|
|
2895
|
+
const thread = await getThread(checkpoint.threadId);
|
|
2896
|
+
if (!thread || thread.ownerEmail !== owner) {
|
|
2897
|
+
setResponseStatus(event, 404);
|
|
2898
|
+
return { error: "Checkpoint not found" };
|
|
2899
|
+
}
|
|
2900
|
+
const { createCheckpoint: gitCheckpoint, restoreToCheckpoint, hasUncommittedChanges, isGitRepo, } = await import("../checkpoints/service.js");
|
|
2901
|
+
const cwd = process.cwd();
|
|
2902
|
+
if (!isGitRepo(cwd)) {
|
|
2903
|
+
setResponseStatus(event, 400);
|
|
2904
|
+
return { error: "Not a git repository" };
|
|
2905
|
+
}
|
|
2906
|
+
// Save current state before restoring so user can undo the undo
|
|
2907
|
+
if (hasUncommittedChanges(cwd)) {
|
|
2908
|
+
gitCheckpoint(cwd, "[agent-native] Pre-restore checkpoint");
|
|
2909
|
+
}
|
|
2910
|
+
const restored = restoreToCheckpoint(cwd, checkpoint.commitSha);
|
|
2911
|
+
if (!restored) {
|
|
2912
|
+
setResponseStatus(event, 500);
|
|
2913
|
+
return { error: "Failed to restore checkpoint" };
|
|
2914
|
+
}
|
|
2915
|
+
// Trigger UI refresh
|
|
2916
|
+
try {
|
|
2917
|
+
const { recordChange } = await import("./poll.js");
|
|
2918
|
+
recordChange({
|
|
2919
|
+
source: "checkpoint",
|
|
2920
|
+
type: "change",
|
|
2921
|
+
key: "*",
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
catch { }
|
|
2925
|
+
return { success: true, commitSha: checkpoint.commitSha };
|
|
2926
|
+
}
|
|
2927
|
+
catch (err) {
|
|
2928
|
+
setResponseStatus(event, 500);
|
|
2929
|
+
return { error: err?.message ?? "Restore failed" };
|
|
2930
|
+
}
|
|
2749
2931
|
}
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2932
|
+
setResponseStatus(event, 405);
|
|
2933
|
+
return { error: "Method not allowed" };
|
|
2934
|
+
}));
|
|
2935
|
+
// ─── Thread management endpoints ──────────────────────────────────────
|
|
2936
|
+
// Single handler for /threads and /threads/:id — h3's use() does prefix
|
|
2937
|
+
// matching so we can't reliably split them into separate handlers.
|
|
2938
|
+
getH3App(nitroApp).use(`${routePath}/threads`, defineEventHandler(async (event) => {
|
|
2939
|
+
const owner = await getOwnerFromEvent(event);
|
|
2940
|
+
const method = getMethod(event);
|
|
2941
|
+
// Determine if this is a specific-thread request.
|
|
2942
|
+
// h3's use() strips the mount prefix, so event.path contains
|
|
2943
|
+
// only the remainder after /threads — e.g., "/thread-abc" or "/".
|
|
2944
|
+
// We also check the original URL as a fallback.
|
|
2945
|
+
const remainder = (event.path || "").replace(/^\/+/, "");
|
|
2946
|
+
const fromUrl = (event.node?.req?.url || "").match(/\/threads\/([^/?]+)/);
|
|
2947
|
+
const threadId = remainder
|
|
2948
|
+
? decodeURIComponent(remainder.split("?")[0].split("/")[0])
|
|
2949
|
+
: fromUrl
|
|
2950
|
+
? decodeURIComponent(fromUrl[1])
|
|
2951
|
+
: null;
|
|
2952
|
+
// ── Specific thread: GET/PUT/DELETE /threads/:id ──
|
|
2953
|
+
if (threadId) {
|
|
2954
|
+
if (method === "GET") {
|
|
2758
2955
|
const thread = await getThread(threadId);
|
|
2759
2956
|
if (!thread || thread.ownerEmail !== owner) {
|
|
2760
2957
|
setResponseStatus(event, 404);
|
|
2761
2958
|
return { error: "Thread not found" };
|
|
2762
2959
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
//
|
|
2767
|
-
//
|
|
2768
|
-
//
|
|
2769
|
-
//
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2960
|
+
return thread;
|
|
2961
|
+
}
|
|
2962
|
+
if (method === "PUT") {
|
|
2963
|
+
// Hold the thread_data lock for the full read-modify-write so
|
|
2964
|
+
// periodic saves from the frontend don't race with
|
|
2965
|
+
// onRunComplete / setThreadQueuedMessages / setThreadEngineMeta.
|
|
2966
|
+
// Without the lock, a client save that lands during an agent
|
|
2967
|
+
// run could clobber the assistant message the server just
|
|
2968
|
+
// appended (and vice versa).
|
|
2969
|
+
return await withThreadDataLock(threadId, async () => {
|
|
2970
|
+
const thread = await getThread(threadId);
|
|
2971
|
+
if (!thread || thread.ownerEmail !== owner) {
|
|
2972
|
+
setResponseStatus(event, 404);
|
|
2973
|
+
return { error: "Thread not found" };
|
|
2974
|
+
}
|
|
2975
|
+
const body = await readBody(event);
|
|
2976
|
+
let newThreadData = body.threadData || thread.threadData;
|
|
2977
|
+
// Preserve queuedMessages from the existing thread_data when the
|
|
2978
|
+
// incoming blob doesn't include it. Periodic full-thread saves
|
|
2979
|
+
// (exported via threadRuntime.export) don't carry the queue, and
|
|
2980
|
+
// we don't want them to clobber queued-message state persisted
|
|
2981
|
+
// via POST /threads/:id/queued.
|
|
2982
|
+
if (body.threadData) {
|
|
2983
|
+
try {
|
|
2984
|
+
const existing = JSON.parse(thread.threadData);
|
|
2985
|
+
if (existing.queuedMessages !== undefined) {
|
|
2986
|
+
const incoming = JSON.parse(newThreadData);
|
|
2987
|
+
if (incoming.queuedMessages === undefined) {
|
|
2988
|
+
incoming.queuedMessages = existing.queuedMessages;
|
|
2989
|
+
newThreadData = JSON.stringify(incoming);
|
|
2990
|
+
}
|
|
2778
2991
|
}
|
|
2779
2992
|
}
|
|
2993
|
+
catch {
|
|
2994
|
+
// Invalid JSON in either side — fall back to raw body blob.
|
|
2995
|
+
}
|
|
2780
2996
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2997
|
+
await updateThreadData(threadId, newThreadData, body.title ?? thread.title, body.preview ?? thread.preview, body.messageCount || thread.messageCount);
|
|
2998
|
+
return { ok: true };
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
// POST /threads/:id/queued — debounced writes from the client
|
|
3002
|
+
// when the user adds/removes/dequeues a queued message. Keeps
|
|
3003
|
+
// queued messages durable across reloads without piggybacking
|
|
3004
|
+
// on full-thread saves.
|
|
3005
|
+
if (method === "POST" &&
|
|
3006
|
+
/\/threads\/[^/?]+\/queued/.test(event.node?.req?.url || event.path || "")) {
|
|
3007
|
+
const thread = await getThread(threadId);
|
|
3008
|
+
if (!thread || thread.ownerEmail !== owner) {
|
|
3009
|
+
setResponseStatus(event, 404);
|
|
3010
|
+
return { error: "Thread not found" };
|
|
2784
3011
|
}
|
|
2785
|
-
|
|
3012
|
+
const body = await readBody(event);
|
|
3013
|
+
const queued = Array.isArray(body?.queuedMessages)
|
|
3014
|
+
? body.queuedMessages
|
|
3015
|
+
: [];
|
|
3016
|
+
await setThreadQueuedMessages(threadId, queued);
|
|
2786
3017
|
return { ok: true };
|
|
2787
|
-
});
|
|
2788
|
-
}
|
|
2789
|
-
// POST /threads/:id/queued — debounced writes from the client
|
|
2790
|
-
// when the user adds/removes/dequeues a queued message. Keeps
|
|
2791
|
-
// queued messages durable across reloads without piggybacking
|
|
2792
|
-
// on full-thread saves.
|
|
2793
|
-
if (method === "POST" &&
|
|
2794
|
-
/\/threads\/[^/?]+\/queued/.test(event.node?.req?.url || event.path || "")) {
|
|
2795
|
-
const thread = await getThread(threadId);
|
|
2796
|
-
if (!thread || thread.ownerEmail !== owner) {
|
|
2797
|
-
setResponseStatus(event, 404);
|
|
2798
|
-
return { error: "Thread not found" };
|
|
2799
3018
|
}
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
3019
|
+
if (method === "DELETE") {
|
|
3020
|
+
const thread = await getThread(threadId);
|
|
3021
|
+
if (!thread || thread.ownerEmail !== owner) {
|
|
3022
|
+
setResponseStatus(event, 404);
|
|
3023
|
+
return { error: "Thread not found" };
|
|
3024
|
+
}
|
|
3025
|
+
await deleteThread(threadId);
|
|
3026
|
+
return { ok: true };
|
|
3027
|
+
}
|
|
3028
|
+
setResponseStatus(event, 405);
|
|
3029
|
+
return { error: "Method not allowed" };
|
|
2806
3030
|
}
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
3031
|
+
// ── Thread list: GET/POST /threads ──
|
|
3032
|
+
if (method === "GET") {
|
|
3033
|
+
const query = getQuery(event);
|
|
3034
|
+
const limit = Math.min(parseInt(String(query.limit ?? "50"), 10) || 50, 200);
|
|
3035
|
+
const q = query.q ? String(query.q).trim() : "";
|
|
3036
|
+
if (q) {
|
|
3037
|
+
const threads = await searchThreads(owner, q, limit);
|
|
3038
|
+
return { threads };
|
|
2812
3039
|
}
|
|
2813
|
-
|
|
2814
|
-
|
|
3040
|
+
const offset = parseInt(String(query.offset ?? "0"), 10) || 0;
|
|
3041
|
+
const threads = await listThreads(owner, limit, offset);
|
|
3042
|
+
return { threads };
|
|
3043
|
+
}
|
|
3044
|
+
if (method === "POST") {
|
|
3045
|
+
const body = await readBody(event);
|
|
3046
|
+
const thread = await createThread(owner, {
|
|
3047
|
+
id: body?.id,
|
|
3048
|
+
title: body?.title ?? "",
|
|
3049
|
+
});
|
|
3050
|
+
return thread;
|
|
2815
3051
|
}
|
|
2816
3052
|
setResponseStatus(event, 405);
|
|
2817
3053
|
return { error: "Method not allowed" };
|
|
2818
|
-
}
|
|
2819
|
-
//
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
3054
|
+
}));
|
|
3055
|
+
// Mount the main chat handler — delegates to dev or prod handler based on current mode.
|
|
3056
|
+
// This is mounted last because h3's use() is prefix-based, meaning /_agent-native/agent-chat
|
|
3057
|
+
// also matches /_agent-native/agent-chat/threads/... — we skip sub-path requests here so the
|
|
3058
|
+
// earlier-mounted handlers (mode, save-key, files, skills, mentions, threads) handle them.
|
|
3059
|
+
getH3App(nitroApp).use(routePath, defineEventHandler(async (event) => {
|
|
3060
|
+
// Skip sub-path requests — they're handled by earlier-mounted handlers
|
|
3061
|
+
const url = event.node?.req?.url || event.path || "";
|
|
3062
|
+
const afterBase = url.slice(url.indexOf(routePath) + routePath.length);
|
|
3063
|
+
if (afterBase && afterBase !== "/" && !afterBase.startsWith("?")) {
|
|
3064
|
+
// Not for us — return 404 so h3 doesn't swallow the request
|
|
3065
|
+
setResponseStatus(event, 404);
|
|
3066
|
+
return { error: "Not found" };
|
|
2827
3067
|
}
|
|
2828
|
-
|
|
2829
|
-
const
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
const thread = await createThread(owner, {
|
|
2835
|
-
id: body?.id,
|
|
2836
|
-
title: body?.title ?? "",
|
|
2837
|
-
});
|
|
2838
|
-
return thread;
|
|
2839
|
-
}
|
|
2840
|
-
setResponseStatus(event, 405);
|
|
2841
|
-
return { error: "Method not allowed" };
|
|
2842
|
-
}));
|
|
2843
|
-
// Mount the main chat handler — delegates to dev or prod handler based on current mode.
|
|
2844
|
-
// This is mounted last because h3's use() is prefix-based, meaning /_agent-native/agent-chat
|
|
2845
|
-
// also matches /_agent-native/agent-chat/threads/... — we skip sub-path requests here so the
|
|
2846
|
-
// earlier-mounted handlers (mode, save-key, files, skills, mentions, threads) handle them.
|
|
2847
|
-
getH3App(nitroApp).use(routePath, defineEventHandler(async (event) => {
|
|
2848
|
-
// Skip sub-path requests — they're handled by earlier-mounted handlers
|
|
2849
|
-
const url = event.node?.req?.url || event.path || "";
|
|
2850
|
-
const afterBase = url.slice(url.indexOf(routePath) + routePath.length);
|
|
2851
|
-
if (afterBase && afterBase !== "/" && !afterBase.startsWith("?")) {
|
|
2852
|
-
// Not for us — return 404 so h3 doesn't swallow the request
|
|
2853
|
-
setResponseStatus(event, 404);
|
|
2854
|
-
return { error: "Not found" };
|
|
2855
|
-
}
|
|
2856
|
-
// Resolve per-request auth context
|
|
2857
|
-
const owner = await getOwnerFromEvent(event);
|
|
2858
|
-
// Resolve org ID: explicit callback > session.orgId from Better Auth
|
|
2859
|
-
let resolvedOrgId;
|
|
2860
|
-
if (options?.resolveOrgId) {
|
|
2861
|
-
resolvedOrgId = (await options.resolveOrgId(event)) ?? undefined;
|
|
2862
|
-
}
|
|
2863
|
-
else {
|
|
2864
|
-
try {
|
|
2865
|
-
const session = await getSession(event);
|
|
2866
|
-
resolvedOrgId = session?.orgId ?? undefined;
|
|
3068
|
+
// Resolve per-request auth context
|
|
3069
|
+
const owner = await getOwnerFromEvent(event);
|
|
3070
|
+
// Resolve org ID: explicit callback > session.orgId from Better Auth
|
|
3071
|
+
let resolvedOrgId;
|
|
3072
|
+
if (options?.resolveOrgId) {
|
|
3073
|
+
resolvedOrgId = (await options.resolveOrgId(event)) ?? undefined;
|
|
2867
3074
|
}
|
|
2868
|
-
|
|
2869
|
-
|
|
3075
|
+
else {
|
|
3076
|
+
try {
|
|
3077
|
+
const session = await getSession(event);
|
|
3078
|
+
resolvedOrgId = session?.orgId ?? undefined;
|
|
3079
|
+
}
|
|
3080
|
+
catch {
|
|
3081
|
+
// Session not available
|
|
3082
|
+
}
|
|
2870
3083
|
}
|
|
3084
|
+
// Also set process.env for backwards compat (CLI scripts, legacy readers)
|
|
3085
|
+
process.env.AGENT_USER_EMAIL = owner;
|
|
3086
|
+
if (resolvedOrgId) {
|
|
3087
|
+
process.env.AGENT_ORG_ID = resolvedOrgId;
|
|
3088
|
+
}
|
|
3089
|
+
else {
|
|
3090
|
+
delete process.env.AGENT_ORG_ID;
|
|
3091
|
+
}
|
|
3092
|
+
// Propagate the caller's IANA timezone from `x-user-timezone` so that
|
|
3093
|
+
// tool calls made by the agent (e.g. log-meal with no explicit date)
|
|
3094
|
+
// resolve "today" in the user's local timezone instead of server UTC.
|
|
3095
|
+
const tzRaw = getHeader(event, "x-user-timezone");
|
|
3096
|
+
const timezone = typeof tzRaw === "string" &&
|
|
3097
|
+
tzRaw.trim().length > 0 &&
|
|
3098
|
+
tzRaw.trim().length < 64
|
|
3099
|
+
? tzRaw.trim()
|
|
3100
|
+
: undefined;
|
|
3101
|
+
if (timezone)
|
|
3102
|
+
process.env.AGENT_USER_TIMEZONE = timezone;
|
|
3103
|
+
return runWithRequestContext({ userEmail: owner, orgId: resolvedOrgId, timezone }, () => {
|
|
3104
|
+
const handler = currentDevMode && devHandler ? devHandler : prodHandler;
|
|
3105
|
+
return handler(event);
|
|
3106
|
+
});
|
|
3107
|
+
}));
|
|
3108
|
+
// ─── Recurring Jobs Scheduler ──────────────────────────────────────
|
|
3109
|
+
// Poll every 60 seconds for due recurring jobs and execute them.
|
|
3110
|
+
// Uses setInterval so it works in all deployment environments without
|
|
3111
|
+
// requiring Nitro experimental tasks configuration.
|
|
3112
|
+
try {
|
|
3113
|
+
const { processRecurringJobs } = await import("../jobs/scheduler.js");
|
|
3114
|
+
const schedulerDeps = {
|
|
3115
|
+
getActions: () => ({
|
|
3116
|
+
...templateScripts,
|
|
3117
|
+
...resourceScripts,
|
|
3118
|
+
...docsScripts,
|
|
3119
|
+
...chatScripts,
|
|
3120
|
+
...jobTools,
|
|
3121
|
+
...automationTools,
|
|
3122
|
+
...notificationTools,
|
|
3123
|
+
...progressTools,
|
|
3124
|
+
...fetchTool,
|
|
3125
|
+
}),
|
|
3126
|
+
getSystemPrompt: async (owner) => {
|
|
3127
|
+
const resources = await loadResourcesForPrompt(owner);
|
|
3128
|
+
const schemaBlock = await buildSchemaBlock(owner, false);
|
|
3129
|
+
return basePrompt + resources + schemaBlock;
|
|
3130
|
+
},
|
|
3131
|
+
apiKey: options?.apiKey ?? process.env.ANTHROPIC_API_KEY,
|
|
3132
|
+
model: resolvedModel,
|
|
3133
|
+
};
|
|
3134
|
+
// Start after a 10-second delay to let the server fully initialize
|
|
3135
|
+
setTimeout(() => {
|
|
3136
|
+
setInterval(() => {
|
|
3137
|
+
processRecurringJobs(schedulerDeps).catch((err) => {
|
|
3138
|
+
console.error("[recurring-jobs] Scheduler error:", err?.message);
|
|
3139
|
+
});
|
|
3140
|
+
}, 60_000);
|
|
3141
|
+
if (process.env.DEBUG)
|
|
3142
|
+
console.log("[recurring-jobs] Scheduler started (60s interval)");
|
|
3143
|
+
}, 10_000);
|
|
2871
3144
|
}
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
if (resolvedOrgId) {
|
|
2875
|
-
process.env.AGENT_ORG_ID = resolvedOrgId;
|
|
2876
|
-
}
|
|
2877
|
-
else {
|
|
2878
|
-
delete process.env.AGENT_ORG_ID;
|
|
3145
|
+
catch (err) {
|
|
3146
|
+
// Jobs module not available — skip silently
|
|
2879
3147
|
}
|
|
2880
|
-
//
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
getActions: () => ({
|
|
2904
|
-
...templateScripts,
|
|
2905
|
-
...resourceScripts,
|
|
2906
|
-
...docsScripts,
|
|
2907
|
-
...chatScripts,
|
|
2908
|
-
...jobTools,
|
|
2909
|
-
}),
|
|
2910
|
-
getSystemPrompt: async (owner) => {
|
|
2911
|
-
const resources = await loadResourcesForPrompt(owner);
|
|
2912
|
-
const schemaBlock = await buildSchemaBlock(owner, false);
|
|
2913
|
-
return basePrompt + resources + schemaBlock;
|
|
2914
|
-
},
|
|
2915
|
-
apiKey: options?.apiKey ?? process.env.ANTHROPIC_API_KEY,
|
|
2916
|
-
model: resolvedModel,
|
|
2917
|
-
};
|
|
2918
|
-
// Start after a 10-second delay to let the server fully initialize
|
|
2919
|
-
setTimeout(() => {
|
|
2920
|
-
setInterval(() => {
|
|
2921
|
-
processRecurringJobs(schedulerDeps).catch((err) => {
|
|
2922
|
-
console.error("[recurring-jobs] Scheduler error:", err?.message);
|
|
2923
|
-
});
|
|
2924
|
-
}, 60_000);
|
|
3148
|
+
// ─── Trigger Dispatcher (event-based automations) ─────────────────
|
|
3149
|
+
try {
|
|
3150
|
+
const { initTriggerDispatcher } = await import("../triggers/dispatcher.js");
|
|
3151
|
+
await initTriggerDispatcher({
|
|
3152
|
+
getActions: () => ({
|
|
3153
|
+
...templateScripts,
|
|
3154
|
+
...resourceScripts,
|
|
3155
|
+
...docsScripts,
|
|
3156
|
+
...chatScripts,
|
|
3157
|
+
...jobTools,
|
|
3158
|
+
...automationTools,
|
|
3159
|
+
...notificationTools,
|
|
3160
|
+
...progressTools,
|
|
3161
|
+
...fetchTool,
|
|
3162
|
+
}),
|
|
3163
|
+
getSystemPrompt: async (owner) => {
|
|
3164
|
+
const resources = await loadResourcesForPrompt(owner);
|
|
3165
|
+
const schemaBlock = await buildSchemaBlock(owner, false);
|
|
3166
|
+
return basePrompt + resources + schemaBlock;
|
|
3167
|
+
},
|
|
3168
|
+
apiKey: options?.apiKey ?? process.env.ANTHROPIC_API_KEY,
|
|
3169
|
+
model: resolvedModel,
|
|
3170
|
+
});
|
|
2925
3171
|
if (process.env.DEBUG)
|
|
2926
|
-
console.log("[
|
|
2927
|
-
}
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
}
|
|
3172
|
+
console.log("[triggers] Trigger dispatcher initialized");
|
|
3173
|
+
}
|
|
3174
|
+
catch (err) {
|
|
3175
|
+
// Triggers module not available — skip silently
|
|
3176
|
+
}
|
|
3177
|
+
})();
|
|
3178
|
+
trackPluginInit(nitroApp, initPromise);
|
|
2932
3179
|
};
|
|
2933
3180
|
}
|
|
2934
3181
|
/**
|