@bbigbang/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +380 -0
- package/dist/execution/executionDispatcher.js +3810 -0
- package/dist/main.js +90 -0
- package/dist/nodeEventHistory.js +206 -0
- package/dist/scheduler/dreamLogic.js +50 -0
- package/dist/scheduler/dreamScheduler.js +65 -0
- package/dist/services/agentFileAccessService.js +1913 -0
- package/dist/services/agentRuntimeCleanupBroker.js +62 -0
- package/dist/services/agentSkillsBroker.js +118 -0
- package/dist/services/agentSkillsService.js +83 -0
- package/dist/services/agentWorkspaceBroker.js +937 -0
- package/dist/services/agentWorkspaceService.js +70 -0
- package/dist/services/appVersion.js +14 -0
- package/dist/services/auth.js +586 -0
- package/dist/services/claudeControlBroker.js +154 -0
- package/dist/services/claudeTranscriptBroker.js +100 -0
- package/dist/services/claudeTranscriptService.js +359 -0
- package/dist/services/codexAppServerBroker.js +155 -0
- package/dist/services/codexTranscriptBroker.js +98 -0
- package/dist/services/codexTranscriptService.js +961 -0
- package/dist/services/droidMissionBroker.js +124 -0
- package/dist/services/droidMissionImporter.js +630 -0
- package/dist/services/droidModelOptions.js +165 -0
- package/dist/services/hubServerRegistrationService.js +268 -0
- package/dist/services/libraryManifest.js +43 -0
- package/dist/services/libraryScaffold.js +26 -0
- package/dist/services/libraryService.js +2263 -0
- package/dist/services/memoryService.js +386 -0
- package/dist/services/missionEvidence.js +377 -0
- package/dist/services/missionService.js +2361 -0
- package/dist/services/missionTrace.js +158 -0
- package/dist/services/nativeMissionBriefParser.js +120 -0
- package/dist/services/nativeMissionOrchestrator.js +2045 -0
- package/dist/services/nativeMissionReportGenerator.js +227 -0
- package/dist/services/nativeMissionValidationRunner.js +452 -0
- package/dist/services/nativeMissionWorkerBroker.js +190 -0
- package/dist/services/nodeRegistry.js +34 -0
- package/dist/services/nodeStateReconciler.js +97 -0
- package/dist/services/panelMediaScanner.js +119 -0
- package/dist/services/persistentRuntimeJsonlClient.js +153 -0
- package/dist/services/platformAgentPolicy.js +180 -0
- package/dist/services/platformAgentService.js +2041 -0
- package/dist/services/projectAccessResolver.js +93 -0
- package/dist/services/projectService.js +392 -0
- package/dist/services/resourceSpaceService.js +140 -0
- package/dist/services/scenarioRuntimeService.js +1130 -0
- package/dist/services/suggestedPlannerService.js +868 -0
- package/dist/services/workbenchGitBroker.js +161 -0
- package/dist/services/workbenchGitService.js +69 -0
- package/dist/services/workbenchInspectBroker.js +65 -0
- package/dist/services/workbenchNodePathService.js +79 -0
- package/dist/services/workbenchRegistryService.js +240 -0
- package/dist/services/workbenchRootService.js +181 -0
- package/dist/services/workbenchTerminalBroker.js +378 -0
- package/dist/services/workspaceRunOwnership.js +60 -0
- package/dist/services/workspaceScaffold.js +105 -0
- package/dist/services/workspaceSessionRuntimeService.js +576 -0
- package/dist/services/workspaceSessionService.js +245 -0
- package/dist/services/workspaceToolActionRunner.js +1582 -0
- package/dist/services/workspaceToolErrors.js +10 -0
- package/dist/services/workspaceToolExecutionUtils.js +895 -0
- package/dist/services/workspaceToolLatestStateProjector.js +91 -0
- package/dist/services/workspaceToolManifest.js +572 -0
- package/dist/services/workspaceToolMutationQueue.js +43 -0
- package/dist/services/workspaceToolPanelProjection.js +460 -0
- package/dist/services/workspaceToolPromotion.js +255 -0
- package/dist/services/workspaceToolPromotionState.js +224 -0
- package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
- package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
- package/dist/services/workspaceToolReadModel.js +378 -0
- package/dist/services/workspaceToolRunLedger.js +239 -0
- package/dist/services/workspaceToolService.js +3067 -0
- package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
- package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
- package/dist/services/workspaceToolTypes.js +1 -0
- package/dist/services/workspaceToolUploadMaterializer.js +228 -0
- package/dist/web/actionCardRoutes.js +129 -0
- package/dist/web/actionCards.js +469 -0
- package/dist/web/activationContext.js +684 -0
- package/dist/web/agentChannelGuards.js +48 -0
- package/dist/web/agentMentionCooldowns.js +32 -0
- package/dist/web/agentReminders.js +1668 -0
- package/dist/web/agentRuntimePresence.js +197 -0
- package/dist/web/agentSelfState.js +494 -0
- package/dist/web/agentTaskLinks.js +26 -0
- package/dist/web/agentVisibility.js +79 -0
- package/dist/web/assets.js +95 -0
- package/dist/web/channelActivationPrompt.js +395 -0
- package/dist/web/channelMemoryNotes.js +127 -0
- package/dist/web/channelMentions.js +10 -0
- package/dist/web/channelMessageSequences.js +19 -0
- package/dist/web/channelSubscriptions.js +26 -0
- package/dist/web/clearedTaskRoots.js +10 -0
- package/dist/web/collaborationPromptGuidance.js +36 -0
- package/dist/web/collaborationSurfaceState.js +140 -0
- package/dist/web/contextBundleRanking.js +154 -0
- package/dist/web/contextBundleResolver.js +488 -0
- package/dist/web/conversationBuiltinSkillRoots.js +50 -0
- package/dist/web/conversationControls.js +232 -0
- package/dist/web/conversationHandoffs.js +612 -0
- package/dist/web/conversationManager.js +2511 -0
- package/dist/web/conversationSummaries.js +876 -0
- package/dist/web/conversationSurfaceKinds.js +17 -0
- package/dist/web/conversationTargets.js +173 -0
- package/dist/web/directActivationPrompt.js +122 -0
- package/dist/web/directReplyTargets.js +69 -0
- package/dist/web/directThreadResolver.js +129 -0
- package/dist/web/dmTaskHandoffPrompt.js +120 -0
- package/dist/web/dmTaskThreadStatusProjection.js +229 -0
- package/dist/web/ftsQuery.js +33 -0
- package/dist/web/internalAgentRouter.js +11341 -0
- package/dist/web/libraryCuratorScheduler.js +58 -0
- package/dist/web/libraryDocumentPromptGuidance.js +8 -0
- package/dist/web/messageCheckpoints.js +19 -0
- package/dist/web/nodeWsHandler.js +2495 -0
- package/dist/web/notificationRounds.js +1061 -0
- package/dist/web/panelActionMessages.js +108 -0
- package/dist/web/panelActivationPrompt.js +18 -0
- package/dist/web/panelAudit.js +273 -0
- package/dist/web/panelLifecycle.js +222 -0
- package/dist/web/panelMediaPolicy.js +43 -0
- package/dist/web/panelPathPolicy.js +63 -0
- package/dist/web/panelPreviews.js +175 -0
- package/dist/web/panelQueryHandles.js +2749 -0
- package/dist/web/panelRoutes.js +2147 -0
- package/dist/web/panels.js +904 -0
- package/dist/web/peerInboxAggregates.js +1247 -0
- package/dist/web/planApprovalState.js +92 -0
- package/dist/web/platformAgentScheduler.js +66 -0
- package/dist/web/proactiveOpportunities.js +452 -0
- package/dist/web/promptContextSections.js +242 -0
- package/dist/web/promptHistorySanitizer.js +26 -0
- package/dist/web/promptSlashCommands.js +158 -0
- package/dist/web/rollingConversationSummary.js +453 -0
- package/dist/web/routeHelpers.js +11 -0
- package/dist/web/routes/handoff.js +288 -0
- package/dist/web/routes/history.js +345 -0
- package/dist/web/routes/memory.js +258 -0
- package/dist/web/routes/selfState.js +171 -0
- package/dist/web/routes/workspace.js +154 -0
- package/dist/web/runSurfaceWatermarks.js +431 -0
- package/dist/web/runtimeCapabilities.js +48 -0
- package/dist/web/sameAgentHandoffs.js +494 -0
- package/dist/web/server.js +15567 -0
- package/dist/web/sharedCollaborationCapsules.js +163 -0
- package/dist/web/soloSessionRelay.js +42 -0
- package/dist/web/soloWsHandler.js +138 -0
- package/dist/web/suggestedPlannerScheduler.js +56 -0
- package/dist/web/surfaceActivationPolicy.js +108 -0
- package/dist/web/surfaceCollaborators.js +61 -0
- package/dist/web/surfaceSystemStatus.js +263 -0
- package/dist/web/targetParticipants.js +77 -0
- package/dist/web/taskEvents.js +49 -0
- package/dist/web/taskLifecycleMessages.js +165 -0
- package/dist/web/taskLoops.js +732 -0
- package/dist/web/taskMemoryNotes.js +224 -0
- package/dist/web/taskNumbers.js +16 -0
- package/dist/web/taskOwnerGuards.js +49 -0
- package/dist/web/taskParticipantResolver.js +42 -0
- package/dist/web/taskParticipants.js +97 -0
- package/dist/web/taskSourceDetails.js +20 -0
- package/dist/web/taskStateViews.js +210 -0
- package/dist/web/taskStatusTransitions.js +9 -0
- package/dist/web/taskThreadFollowups.js +599 -0
- package/dist/web/taskThreadRuntimeClosure.js +685 -0
- package/dist/web/taskUpdateDelivery.js +104 -0
- package/dist/web/threadReplyContentHeuristics.js +30 -0
- package/dist/web/threadRoots.js +61 -0
- package/dist/web/threadTaskBindings.js +365 -0
- package/dist/web/uiPanelPromptGuidance.js +27 -0
- package/dist/web/workspaceMemoryHints.js +143 -0
- package/dist/web/workspaceToolPromptGuidance.js +30 -0
- package/dist/web/wsHandler.js +397 -0
- package/dist/web/wsSink.js +116 -0
- package/package.json +54 -0
|
@@ -0,0 +1,2041 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { log } from '@bbigbang/runtime-acp';
|
|
5
|
+
import { insertPlannerTaskCandidateOpportunity } from '../web/proactiveOpportunities.js';
|
|
6
|
+
import { getLibraryCuratorTerminalError, } from './libraryService.js';
|
|
7
|
+
import { getBuiltinPlatformDefinition, getPlatformAgentConversationTitle, getPlatformAgentLegacyConversationTitles, getPlatformWorkflowCatalog, getPlatformWorkflowCatalogEntry, isBuiltinPlatformAgentDefinitionId, normalizePlatformAgentToolPolicy, normalizePlatformResourceScopes, } from './platformAgentPolicy.js';
|
|
8
|
+
import { getSuggestedPlannerTerminalError, parseSuggestedPlannerOutput, } from './suggestedPlannerService.js';
|
|
9
|
+
const DEFAULT_PLATFORM_AGENT_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
10
|
+
const DEFAULT_LOOKBACK_DAYS = 14;
|
|
11
|
+
const PLATFORM_AGENT_WORKSPACE_DIRECTORY_NAME = '.platform-agents';
|
|
12
|
+
const PLANNER_WORKSPACE_DIRECTORY_NAME = '.suggested-planner';
|
|
13
|
+
const LIBRARY_ROOT_DIRECTORY_NAME = '.library-curator';
|
|
14
|
+
const CURATOR_WORKSPACE_DIRECTORY_NAME = 'workspaces';
|
|
15
|
+
const PLATFORM_AGENT_TYPE = 'claude_sdk';
|
|
16
|
+
const LEGACY_SUGGESTED_PLANNER_PROMPT_PREFIX = '# Suggested Planner run';
|
|
17
|
+
const LEGACY_SUGGESTED_PLANNER_CLEANUP_ERROR = 'Legacy Suggested Planner workflow run was cleaned up after the platform-agent migration.';
|
|
18
|
+
export function getPlatformWorkflowTerminalError(workflowType, stopReason, error) {
|
|
19
|
+
if (workflowType === 'planner_suggestions') {
|
|
20
|
+
return getSuggestedPlannerTerminalError(stopReason, error);
|
|
21
|
+
}
|
|
22
|
+
return getLibraryCuratorTerminalError(stopReason, error);
|
|
23
|
+
}
|
|
24
|
+
function safeUsername(username) {
|
|
25
|
+
return username.replace(/[^a-zA-Z0-9._-]+/g, '_').trim() || 'user';
|
|
26
|
+
}
|
|
27
|
+
function parseJsonRecord(raw) {
|
|
28
|
+
if (!raw?.trim())
|
|
29
|
+
return {};
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
33
|
+
? parsed
|
|
34
|
+
: {};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function parseJsonStringArray(raw) {
|
|
41
|
+
if (!raw?.trim())
|
|
42
|
+
return [];
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
return Array.isArray(parsed)
|
|
46
|
+
? parsed.filter((item) => typeof item === 'string' && item.trim().length > 0).map((item) => item.trim())
|
|
47
|
+
: [];
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function mapDefinitionRow(row) {
|
|
54
|
+
return {
|
|
55
|
+
definitionId: row.definitionId,
|
|
56
|
+
workflowType: row.workflowType,
|
|
57
|
+
label: row.label,
|
|
58
|
+
description: row.description,
|
|
59
|
+
resourceScopes: normalizePlatformResourceScopes(row.workflowType, parseJsonStringArray(row.resourceScopesJson)),
|
|
60
|
+
resourceConfig: parseJsonRecord(row.resourceConfigJson),
|
|
61
|
+
promptTemplateOverride: row.promptTemplateOverride,
|
|
62
|
+
toolPolicy: normalizePlatformAgentToolPolicy(row.workflowType, parseJsonRecord(row.toolPolicyJson)),
|
|
63
|
+
enabled: row.enabled === 1,
|
|
64
|
+
createdAt: row.createdAt,
|
|
65
|
+
updatedAt: row.updatedAt,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function mapInstanceRow(row) {
|
|
69
|
+
const runtimeConfigRecord = parseJsonRecord(row.runtimeConfigJson);
|
|
70
|
+
return {
|
|
71
|
+
userId: row.userId,
|
|
72
|
+
definitionId: row.definitionId,
|
|
73
|
+
agentId: row.agentId,
|
|
74
|
+
enabled: row.enabled === 1,
|
|
75
|
+
scheduleIntervalMs: row.scheduleIntervalMs,
|
|
76
|
+
workflowConfig: parseJsonRecord(row.workflowConfigJson),
|
|
77
|
+
runtimeConfig: {
|
|
78
|
+
model: typeof runtimeConfigRecord.model === 'string' ? runtimeConfigRecord.model : null,
|
|
79
|
+
reasoningEffort: typeof runtimeConfigRecord.reasoningEffort === 'string' ? runtimeConfigRecord.reasoningEffort : null,
|
|
80
|
+
envVars: normalizeEnvVars(runtimeConfigRecord.envVars),
|
|
81
|
+
},
|
|
82
|
+
lastRunAt: row.lastRunAt,
|
|
83
|
+
nextRunAt: row.nextRunAt,
|
|
84
|
+
createdAt: row.createdAt,
|
|
85
|
+
updatedAt: row.updatedAt,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function mapRunRow(row) {
|
|
89
|
+
let resourcesAccessed = null;
|
|
90
|
+
if (row.resourcesAccessedJson) {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(row.resourcesAccessedJson);
|
|
93
|
+
if (parsed &&
|
|
94
|
+
typeof parsed === 'object' &&
|
|
95
|
+
!Array.isArray(parsed) &&
|
|
96
|
+
typeof parsed.savedItems === 'number' &&
|
|
97
|
+
typeof parsed.tasks === 'number' &&
|
|
98
|
+
typeof parsed.messages === 'number') {
|
|
99
|
+
resourcesAccessed = parsed;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// ignore invalid JSON
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
runId: row.runId,
|
|
108
|
+
definitionId: row.definitionId,
|
|
109
|
+
userId: row.userId,
|
|
110
|
+
mode: row.mode,
|
|
111
|
+
status: row.status,
|
|
112
|
+
createdAt: row.createdAt,
|
|
113
|
+
completedAt: row.completedAt,
|
|
114
|
+
error: row.error,
|
|
115
|
+
generatedCount: row.generatedCount,
|
|
116
|
+
resourcesAccessed,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function normalizeEnvVars(value) {
|
|
120
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
121
|
+
return {};
|
|
122
|
+
const out = {};
|
|
123
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
124
|
+
if (typeof raw === 'string')
|
|
125
|
+
out[key] = raw;
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
function buildPlannerWorkspacePath(workspaceRoot, username) {
|
|
130
|
+
return path.join(workspaceRoot, PLANNER_WORKSPACE_DIRECTORY_NAME, safeUsername(username));
|
|
131
|
+
}
|
|
132
|
+
function buildCuratorWorkspacePath(workspaceRoot, username) {
|
|
133
|
+
return path.join(workspaceRoot, LIBRARY_ROOT_DIRECTORY_NAME, CURATOR_WORKSPACE_DIRECTORY_NAME, safeUsername(username));
|
|
134
|
+
}
|
|
135
|
+
function buildGenericPlatformWorkspacePath(workspaceRoot, definitionId, username) {
|
|
136
|
+
return path.join(workspaceRoot, PLATFORM_AGENT_WORKSPACE_DIRECTORY_NAME, definitionId, safeUsername(username));
|
|
137
|
+
}
|
|
138
|
+
function buildPlannerEnvVars() {
|
|
139
|
+
const envVars = {};
|
|
140
|
+
const baseUrl = process.env.SUGGESTED_PLANNER_ANTHROPIC_BASE_URL?.trim()
|
|
141
|
+
|| process.env.ANTHROPIC_BASE_URL?.trim();
|
|
142
|
+
const model = process.env.SUGGESTED_PLANNER_ANTHROPIC_MODEL?.trim()
|
|
143
|
+
|| process.env.ANTHROPIC_MODEL?.trim()
|
|
144
|
+
|| 'deepseek-v4-pro';
|
|
145
|
+
const authToken = process.env.SUGGESTED_PLANNER_ANTHROPIC_AUTH_TOKEN?.trim()
|
|
146
|
+
|| process.env.SUGGESTED_PLANNER_ANTHROPIC_API_KEY?.trim()
|
|
147
|
+
|| process.env.ANTHROPIC_AUTH_TOKEN?.trim()
|
|
148
|
+
|| process.env.ANTHROPIC_API_KEY?.trim();
|
|
149
|
+
if (baseUrl)
|
|
150
|
+
envVars.ANTHROPIC_BASE_URL = baseUrl;
|
|
151
|
+
if (model)
|
|
152
|
+
envVars.ANTHROPIC_MODEL = model;
|
|
153
|
+
if (authToken)
|
|
154
|
+
envVars.ANTHROPIC_AUTH_TOKEN = authToken;
|
|
155
|
+
for (const key of [
|
|
156
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
157
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
158
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
159
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
160
|
+
'CLAUDE_CODE_EFFORT_LEVEL',
|
|
161
|
+
]) {
|
|
162
|
+
const value = process.env[key]?.trim();
|
|
163
|
+
if (value)
|
|
164
|
+
envVars[key] = value;
|
|
165
|
+
}
|
|
166
|
+
return envVars;
|
|
167
|
+
}
|
|
168
|
+
function buildCuratorEnvVars() {
|
|
169
|
+
const envVars = {};
|
|
170
|
+
const baseUrl = process.env.LIBRARY_CURATOR_ANTHROPIC_BASE_URL?.trim()
|
|
171
|
+
|| process.env.ANTHROPIC_BASE_URL?.trim();
|
|
172
|
+
const model = process.env.LIBRARY_CURATOR_ANTHROPIC_MODEL?.trim()
|
|
173
|
+
|| process.env.ANTHROPIC_MODEL?.trim()
|
|
174
|
+
|| 'deepseek-v4-pro';
|
|
175
|
+
const authToken = process.env.LIBRARY_CURATOR_ANTHROPIC_AUTH_TOKEN?.trim()
|
|
176
|
+
|| process.env.LIBRARY_CURATOR_ANTHROPIC_API_KEY?.trim()
|
|
177
|
+
|| process.env.ANTHROPIC_AUTH_TOKEN?.trim()
|
|
178
|
+
|| process.env.ANTHROPIC_API_KEY?.trim();
|
|
179
|
+
if (baseUrl)
|
|
180
|
+
envVars.ANTHROPIC_BASE_URL = baseUrl;
|
|
181
|
+
if (model)
|
|
182
|
+
envVars.ANTHROPIC_MODEL = model;
|
|
183
|
+
if (authToken)
|
|
184
|
+
envVars.ANTHROPIC_AUTH_TOKEN = authToken;
|
|
185
|
+
for (const key of [
|
|
186
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
187
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
188
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
189
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
190
|
+
'CLAUDE_CODE_EFFORT_LEVEL',
|
|
191
|
+
]) {
|
|
192
|
+
const value = process.env[key]?.trim();
|
|
193
|
+
if (value)
|
|
194
|
+
envVars[key] = value;
|
|
195
|
+
}
|
|
196
|
+
return envVars;
|
|
197
|
+
}
|
|
198
|
+
function pickPlannerNodeId(nodeRegistry) {
|
|
199
|
+
const localNode = nodeRegistry.getNode('local-node-1');
|
|
200
|
+
if (localNode?.agentTypes.includes('claude_sdk'))
|
|
201
|
+
return localNode.nodeId;
|
|
202
|
+
for (const node of nodeRegistry.listNodes()) {
|
|
203
|
+
const agentTypes = Array.isArray(node.agentTypes) ? node.agentTypes : [];
|
|
204
|
+
if (agentTypes.includes('claude_sdk') && !node.nodeId.startsWith('scenario-'))
|
|
205
|
+
return node.nodeId;
|
|
206
|
+
}
|
|
207
|
+
for (const node of nodeRegistry.listNodes()) {
|
|
208
|
+
const agentTypes = Array.isArray(node.agentTypes) ? node.agentTypes : [];
|
|
209
|
+
if (agentTypes.includes('claude_sdk'))
|
|
210
|
+
return node.nodeId;
|
|
211
|
+
}
|
|
212
|
+
return nodeRegistry.listNodes()[0]?.nodeId ?? null;
|
|
213
|
+
}
|
|
214
|
+
function pickLibraryNodeId(nodeRegistry) {
|
|
215
|
+
for (const node of nodeRegistry.listNodes()) {
|
|
216
|
+
const agentTypes = Array.isArray(node.agentTypes) ? node.agentTypes : [];
|
|
217
|
+
if (agentTypes.includes('claude_sdk'))
|
|
218
|
+
return node.nodeId;
|
|
219
|
+
}
|
|
220
|
+
return nodeRegistry.listNodes()[0]?.nodeId ?? null;
|
|
221
|
+
}
|
|
222
|
+
function plannerDedupeHash(value) {
|
|
223
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 24);
|
|
224
|
+
}
|
|
225
|
+
function normalizePlannerString(value, limit) {
|
|
226
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim().slice(0, limit);
|
|
227
|
+
}
|
|
228
|
+
function normalizePriority(value) {
|
|
229
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
230
|
+
if (normalized === 'high' || normalized === 'medium' || normalized === 'low')
|
|
231
|
+
return normalized;
|
|
232
|
+
return 'medium';
|
|
233
|
+
}
|
|
234
|
+
function isSafeEvidenceUrl(url) {
|
|
235
|
+
if (url.startsWith('/') && !url.startsWith('//'))
|
|
236
|
+
return true;
|
|
237
|
+
try {
|
|
238
|
+
const parsed = new URL(url);
|
|
239
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function normalizeEvidenceLinks(value) {
|
|
246
|
+
if (!Array.isArray(value))
|
|
247
|
+
return [];
|
|
248
|
+
return value.slice(0, 4)
|
|
249
|
+
.map((item) => {
|
|
250
|
+
if (!item || typeof item !== 'object')
|
|
251
|
+
return null;
|
|
252
|
+
const record = item;
|
|
253
|
+
const label = normalizePlannerString(record.label, 100);
|
|
254
|
+
const url = normalizePlannerString(record.url, 1000);
|
|
255
|
+
const summary = normalizePlannerString(record.summary, 240);
|
|
256
|
+
if (!label || !url || !isSafeEvidenceUrl(url))
|
|
257
|
+
return null;
|
|
258
|
+
return { label, url, ...(summary ? { summary } : {}) };
|
|
259
|
+
})
|
|
260
|
+
.filter((item) => item != null);
|
|
261
|
+
}
|
|
262
|
+
function normalizePlannerTarget(value) {
|
|
263
|
+
if (!value || typeof value !== 'object')
|
|
264
|
+
return { channelId: null, agentId: null, targetType: 'general' };
|
|
265
|
+
const record = value;
|
|
266
|
+
const type = normalizePlannerString(record.type, 40).toLowerCase();
|
|
267
|
+
const agentId = normalizePlannerString(record.agentId ?? record.agent_id, 200) || null;
|
|
268
|
+
const channelId = normalizePlannerString(record.channelId ?? record.channel_id, 200) || null;
|
|
269
|
+
if (type === 'dm' && agentId)
|
|
270
|
+
return { channelId: `dm:${agentId}`, agentId, targetType: 'dm' };
|
|
271
|
+
if (type === 'channel' && channelId)
|
|
272
|
+
return { channelId, agentId: null, targetType: 'channel' };
|
|
273
|
+
if (channelId?.startsWith('dm:'))
|
|
274
|
+
return { channelId, agentId: agentId ?? channelId.slice(3), targetType: 'dm' };
|
|
275
|
+
if (channelId)
|
|
276
|
+
return { channelId, agentId: null, targetType: 'channel' };
|
|
277
|
+
if (agentId)
|
|
278
|
+
return { channelId: `dm:${agentId}`, agentId, targetType: 'dm' };
|
|
279
|
+
return { channelId: null, agentId: null, targetType: type || 'general' };
|
|
280
|
+
}
|
|
281
|
+
function sanitizeDefinitionId(value) {
|
|
282
|
+
const trimmed = value.trim();
|
|
283
|
+
if (!trimmed)
|
|
284
|
+
throw new Error('Definition id is required.');
|
|
285
|
+
if (!/^[a-z0-9._-]+$/.test(trimmed))
|
|
286
|
+
throw new Error('Definition id may contain only lowercase letters, numbers, ".", "_" and "-".');
|
|
287
|
+
return trimmed;
|
|
288
|
+
}
|
|
289
|
+
function parseCuratorPlan(raw) {
|
|
290
|
+
const parsed = parseJsonRecord(raw);
|
|
291
|
+
if (!Array.isArray(parsed.operations)) {
|
|
292
|
+
throw new Error('Library curator JSON is missing operations array.');
|
|
293
|
+
}
|
|
294
|
+
if (!Array.isArray(parsed.processedSavedItemIds)) {
|
|
295
|
+
throw new Error('Library curator JSON is missing processedSavedItemIds array.');
|
|
296
|
+
}
|
|
297
|
+
const operations = parsed.operations.map((item) => {
|
|
298
|
+
if (!item || typeof item !== 'object') {
|
|
299
|
+
throw new Error('Library curator operation must be an object.');
|
|
300
|
+
}
|
|
301
|
+
const record = item;
|
|
302
|
+
const kind = record.kind === 'create' || record.kind === 'update' ? record.kind : null;
|
|
303
|
+
const relativePath = normalizePlannerString(record.relativePath, 400);
|
|
304
|
+
const title = normalizePlannerString(record.title, 200);
|
|
305
|
+
const summary = normalizePlannerString(record.summary, 400);
|
|
306
|
+
const content = typeof record.content === 'string' ? record.content.replace(/\r\n/g, '\n') : '';
|
|
307
|
+
const sourceSavedItemIds = Array.isArray(record.sourceSavedItemIds)
|
|
308
|
+
? record.sourceSavedItemIds.filter((value) => typeof value === 'string' && value.trim().length > 0).map((value) => value.trim())
|
|
309
|
+
: [];
|
|
310
|
+
if (!kind || !relativePath || !content.trim()) {
|
|
311
|
+
throw new Error('Library curator operation is missing required fields.');
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
kind,
|
|
315
|
+
relativePath,
|
|
316
|
+
title: title || path.posix.basename(relativePath, path.posix.extname(relativePath)),
|
|
317
|
+
summary,
|
|
318
|
+
content,
|
|
319
|
+
sourceSavedItemIds,
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
const processedSavedItemIds = parsed.processedSavedItemIds
|
|
323
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
324
|
+
.map((value) => value.trim());
|
|
325
|
+
return {
|
|
326
|
+
operations,
|
|
327
|
+
processedSavedItemIds: [...new Set(processedSavedItemIds)],
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
export class PlatformAgentService {
|
|
331
|
+
db;
|
|
332
|
+
conversationManager;
|
|
333
|
+
nodeRegistry;
|
|
334
|
+
workspaceBroker;
|
|
335
|
+
workspaceRoot;
|
|
336
|
+
libraryService;
|
|
337
|
+
suggestedPlannerService;
|
|
338
|
+
agentRuntimeCleanupBroker;
|
|
339
|
+
workflowHandlers;
|
|
340
|
+
constructor(db, conversationManager, nodeRegistry, workspaceBroker, workspaceRoot, libraryService, suggestedPlannerService, agentRuntimeCleanupBroker) {
|
|
341
|
+
this.db = db;
|
|
342
|
+
this.conversationManager = conversationManager;
|
|
343
|
+
this.nodeRegistry = nodeRegistry;
|
|
344
|
+
this.workspaceBroker = workspaceBroker;
|
|
345
|
+
this.workspaceRoot = workspaceRoot;
|
|
346
|
+
this.libraryService = libraryService;
|
|
347
|
+
this.suggestedPlannerService = suggestedPlannerService;
|
|
348
|
+
this.agentRuntimeCleanupBroker = agentRuntimeCleanupBroker;
|
|
349
|
+
this.workflowHandlers = {
|
|
350
|
+
planner_suggestions: {
|
|
351
|
+
workflowType: 'planner_suggestions',
|
|
352
|
+
defaultLabel: 'Planner Suggestions',
|
|
353
|
+
defaultDescription: 'Reviews operational context and proposes concrete follow-up opportunities.',
|
|
354
|
+
buildDefaultWorkflowConfig: () => ({ lookbackDays: DEFAULT_LOOKBACK_DAYS }),
|
|
355
|
+
buildPrompt: ({ definition, instance, snapshot }) => {
|
|
356
|
+
const lookbackDays = this.getPlannerLookbackDays(instance.workflowConfig);
|
|
357
|
+
const template = definition.promptTemplateOverride?.trim()
|
|
358
|
+
|| getPlatformWorkflowCatalogEntry('planner_suggestions').defaultPromptTemplate;
|
|
359
|
+
return [
|
|
360
|
+
'# Platform planner workflow run',
|
|
361
|
+
'',
|
|
362
|
+
`Definition: ${definition.label} (${definition.definitionId})`,
|
|
363
|
+
`Lookback days: ${lookbackDays}`,
|
|
364
|
+
'',
|
|
365
|
+
template,
|
|
366
|
+
'',
|
|
367
|
+
'Return only a JSON object. Do not include markdown, comments, or prose outside the JSON.',
|
|
368
|
+
'Schema:',
|
|
369
|
+
'{',
|
|
370
|
+
' "opportunities": [',
|
|
371
|
+
' {',
|
|
372
|
+
' "title": "short task candidate title",',
|
|
373
|
+
' "rationale": "why this likely matters now",',
|
|
374
|
+
' "suggestedAction": "what task should be created if the user accepts",',
|
|
375
|
+
' "priority": "high | medium | low",',
|
|
376
|
+
' "target": { "type": "dm | channel | general", "agentId": "optional agent id", "channelId": "optional channel id" },',
|
|
377
|
+
' "evidenceLinks": [{ "label": "source label", "url": "app-relative or absolute URL", "summary": "short source summary" }]',
|
|
378
|
+
' }',
|
|
379
|
+
' ]',
|
|
380
|
+
'}',
|
|
381
|
+
'',
|
|
382
|
+
'Rules:',
|
|
383
|
+
'- Include at most 8 opportunities.',
|
|
384
|
+
'- Prefer concrete follow-ups backed by the provided resources.',
|
|
385
|
+
'- Use target.type "dm" only when the target agent id is known; use "channel" only when channel id is known; otherwise use "general".',
|
|
386
|
+
'- If there is no meaningful next work, return {"opportunities":[]}.',
|
|
387
|
+
'',
|
|
388
|
+
'```json',
|
|
389
|
+
JSON.stringify({
|
|
390
|
+
workflowConfig: { lookbackDays },
|
|
391
|
+
resources: snapshot,
|
|
392
|
+
}, null, 2),
|
|
393
|
+
'```',
|
|
394
|
+
].join('\n');
|
|
395
|
+
},
|
|
396
|
+
finalize: async ({ userId, runId, rawOutputText }) => {
|
|
397
|
+
const parsed = parseSuggestedPlannerOutput(rawOutputText);
|
|
398
|
+
if (parsed.error)
|
|
399
|
+
throw new Error(parsed.error);
|
|
400
|
+
const now = Date.now();
|
|
401
|
+
const opportunities = parsed.opportunities.map((opportunity) => {
|
|
402
|
+
const target = this.resolveAccessiblePlannerTarget(userId, opportunity);
|
|
403
|
+
const dedupePayload = [
|
|
404
|
+
opportunity.title.toLowerCase(),
|
|
405
|
+
target.targetType,
|
|
406
|
+
target.channelId ?? '',
|
|
407
|
+
target.agentId ?? '',
|
|
408
|
+
opportunity.evidenceLinks.map((link) => link.url).join('|'),
|
|
409
|
+
].join('\n');
|
|
410
|
+
return {
|
|
411
|
+
dedupeKey: `planner:v2:${plannerDedupeHash(dedupePayload)}`,
|
|
412
|
+
opportunity,
|
|
413
|
+
target,
|
|
414
|
+
};
|
|
415
|
+
});
|
|
416
|
+
const seenKeys = new Set();
|
|
417
|
+
let generatedCount = 0;
|
|
418
|
+
for (const item of opportunities) {
|
|
419
|
+
if (seenKeys.has(item.dedupeKey))
|
|
420
|
+
continue;
|
|
421
|
+
seenKeys.add(item.dedupeKey);
|
|
422
|
+
insertPlannerTaskCandidateOpportunity(this.db, {
|
|
423
|
+
userId,
|
|
424
|
+
dedupeKey: item.dedupeKey,
|
|
425
|
+
title: item.opportunity.title,
|
|
426
|
+
rationale: item.opportunity.rationale,
|
|
427
|
+
channelId: item.target.channelId,
|
|
428
|
+
agentId: item.target.agentId,
|
|
429
|
+
evidence: {
|
|
430
|
+
source: 'suggested_planner',
|
|
431
|
+
plannerRunId: runId,
|
|
432
|
+
priority: item.opportunity.priority,
|
|
433
|
+
targetType: item.target.targetType,
|
|
434
|
+
channelId: item.target.channelId,
|
|
435
|
+
agentId: item.target.agentId,
|
|
436
|
+
requestedTargetType: item.opportunity.targetType,
|
|
437
|
+
requestedChannelId: item.opportunity.channelId,
|
|
438
|
+
requestedAgentId: item.opportunity.agentId,
|
|
439
|
+
suggestedTitle: item.opportunity.title,
|
|
440
|
+
suggestedDescription: item.opportunity.suggestedAction || item.opportunity.rationale,
|
|
441
|
+
suggestedAction: item.opportunity.suggestedAction,
|
|
442
|
+
},
|
|
443
|
+
evidenceLinks: item.opportunity.evidenceLinks,
|
|
444
|
+
now,
|
|
445
|
+
});
|
|
446
|
+
generatedCount += 1;
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
outputJson: { opportunities: parsed.opportunities },
|
|
450
|
+
resultJson: { generatedCount },
|
|
451
|
+
generatedCount,
|
|
452
|
+
};
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
library_curate: {
|
|
456
|
+
workflowType: 'library_curate',
|
|
457
|
+
defaultLabel: 'Library Curate',
|
|
458
|
+
defaultDescription: 'Organizes saved information into a personal markdown library.',
|
|
459
|
+
buildDefaultWorkflowConfig: () => ({ categories: [] }),
|
|
460
|
+
buildPrompt: ({ definition, instance, snapshot }) => {
|
|
461
|
+
const categories = this.getCuratorCategories(instance.workflowConfig);
|
|
462
|
+
const template = definition.promptTemplateOverride?.trim()
|
|
463
|
+
|| getPlatformWorkflowCatalogEntry('library_curate').defaultPromptTemplate;
|
|
464
|
+
return [
|
|
465
|
+
'# Platform library curate workflow run',
|
|
466
|
+
'',
|
|
467
|
+
`Definition: ${definition.label} (${definition.definitionId})`,
|
|
468
|
+
'',
|
|
469
|
+
template,
|
|
470
|
+
'',
|
|
471
|
+
'Return only a JSON object. Do not include markdown, comments, or prose outside the JSON.',
|
|
472
|
+
'Schema:',
|
|
473
|
+
'{',
|
|
474
|
+
' "operations": [',
|
|
475
|
+
' {',
|
|
476
|
+
' "kind": "create | update",',
|
|
477
|
+
' "relativePath": "category/topic/file.md",',
|
|
478
|
+
' "title": "document title",',
|
|
479
|
+
' "summary": "short summary",',
|
|
480
|
+
' "content": "# Markdown content",',
|
|
481
|
+
' "sourceSavedItemIds": ["saved-item-id"]',
|
|
482
|
+
' }',
|
|
483
|
+
' ],',
|
|
484
|
+
' "processedSavedItemIds": ["saved-item-id"]',
|
|
485
|
+
'}',
|
|
486
|
+
'',
|
|
487
|
+
'Rules:',
|
|
488
|
+
'- Only create or update markdown documents under the personal library.',
|
|
489
|
+
'- Prefer updating an existing document before creating a new one.',
|
|
490
|
+
'- Do not rename, move, or delete documents.',
|
|
491
|
+
'- Do not target reserved/system paths such as `.meta/*` or `INDEX.md`.',
|
|
492
|
+
'- Every processedSavedItemId must come from the provided inbox items.',
|
|
493
|
+
'',
|
|
494
|
+
'```json',
|
|
495
|
+
JSON.stringify({
|
|
496
|
+
workflowConfig: { categories },
|
|
497
|
+
resources: snapshot,
|
|
498
|
+
}, null, 2),
|
|
499
|
+
'```',
|
|
500
|
+
].join('\n');
|
|
501
|
+
},
|
|
502
|
+
shouldSkip: async ({ snapshot }) => {
|
|
503
|
+
const savedItems = Array.isArray(snapshot.savedInboxItems) ? snapshot.savedInboxItems : [];
|
|
504
|
+
if (savedItems.length === 0)
|
|
505
|
+
return { skipped: true, reason: 'No inbox items to organize' };
|
|
506
|
+
return null;
|
|
507
|
+
},
|
|
508
|
+
finalize: async ({ userId, runId, rawOutputText, snapshot }) => {
|
|
509
|
+
const plan = parseCuratorPlan(rawOutputText);
|
|
510
|
+
const savedItems = Array.isArray(snapshot.savedInboxItems) ? snapshot.savedInboxItems : [];
|
|
511
|
+
const result = await this.libraryService.applyStructuredCuratorPlan({
|
|
512
|
+
userId,
|
|
513
|
+
runId,
|
|
514
|
+
plan,
|
|
515
|
+
savedItems,
|
|
516
|
+
});
|
|
517
|
+
return {
|
|
518
|
+
outputJson: plan,
|
|
519
|
+
resultJson: result,
|
|
520
|
+
generatedCount: result.writtenDocuments,
|
|
521
|
+
};
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
async ensureBuiltinPlatformAgentsForUser(params) {
|
|
527
|
+
await this.suggestedPlannerService.ensureConfigForUser(params);
|
|
528
|
+
await this.libraryService.ensureConfigForUser(params);
|
|
529
|
+
await this.syncBuiltinSuggestedPlannerInstance(params);
|
|
530
|
+
await this.syncBuiltinLibraryCuratorInstance(params);
|
|
531
|
+
}
|
|
532
|
+
getDefinition(definitionId) {
|
|
533
|
+
const row = this.db.prepare(`SELECT definition_id as definitionId,
|
|
534
|
+
workflow_type as workflowType,
|
|
535
|
+
label,
|
|
536
|
+
description,
|
|
537
|
+
resource_scopes_json as resourceScopesJson,
|
|
538
|
+
resource_config_json as resourceConfigJson,
|
|
539
|
+
prompt_template_override as promptTemplateOverride,
|
|
540
|
+
tool_policy_json as toolPolicyJson,
|
|
541
|
+
enabled,
|
|
542
|
+
created_at as createdAt,
|
|
543
|
+
updated_at as updatedAt
|
|
544
|
+
FROM platform_agent_definitions
|
|
545
|
+
WHERE definition_id = ?`).get(definitionId);
|
|
546
|
+
return row ? mapDefinitionRow(row) : null;
|
|
547
|
+
}
|
|
548
|
+
listDefinitions() {
|
|
549
|
+
const rows = this.db.prepare(`SELECT definition_id as definitionId,
|
|
550
|
+
workflow_type as workflowType,
|
|
551
|
+
label,
|
|
552
|
+
description,
|
|
553
|
+
resource_scopes_json as resourceScopesJson,
|
|
554
|
+
resource_config_json as resourceConfigJson,
|
|
555
|
+
prompt_template_override as promptTemplateOverride,
|
|
556
|
+
tool_policy_json as toolPolicyJson,
|
|
557
|
+
enabled,
|
|
558
|
+
created_at as createdAt,
|
|
559
|
+
updated_at as updatedAt
|
|
560
|
+
FROM platform_agent_definitions
|
|
561
|
+
ORDER BY definition_id ASC`).all();
|
|
562
|
+
return rows.map(mapDefinitionRow);
|
|
563
|
+
}
|
|
564
|
+
getWorkflowCatalog() {
|
|
565
|
+
return getPlatformWorkflowCatalog();
|
|
566
|
+
}
|
|
567
|
+
getInstance(userId, definitionId) {
|
|
568
|
+
const row = this.db.prepare(`SELECT user_id as userId,
|
|
569
|
+
definition_id as definitionId,
|
|
570
|
+
agent_id as agentId,
|
|
571
|
+
enabled,
|
|
572
|
+
schedule_interval_ms as scheduleIntervalMs,
|
|
573
|
+
workflow_config_json as workflowConfigJson,
|
|
574
|
+
runtime_config_json as runtimeConfigJson,
|
|
575
|
+
last_run_at as lastRunAt,
|
|
576
|
+
next_run_at as nextRunAt,
|
|
577
|
+
created_at as createdAt,
|
|
578
|
+
updated_at as updatedAt,
|
|
579
|
+
deleted_at as deletedAt
|
|
580
|
+
FROM platform_agent_instances
|
|
581
|
+
WHERE user_id = ? AND definition_id = ? AND deleted_at IS NULL`).get(userId, definitionId);
|
|
582
|
+
return row ? mapInstanceRow(row) : null;
|
|
583
|
+
}
|
|
584
|
+
getInstanceRow(userId, definitionId) {
|
|
585
|
+
return this.db.prepare(`SELECT user_id as userId,
|
|
586
|
+
definition_id as definitionId,
|
|
587
|
+
agent_id as agentId,
|
|
588
|
+
enabled,
|
|
589
|
+
schedule_interval_ms as scheduleIntervalMs,
|
|
590
|
+
workflow_config_json as workflowConfigJson,
|
|
591
|
+
runtime_config_json as runtimeConfigJson,
|
|
592
|
+
last_run_at as lastRunAt,
|
|
593
|
+
next_run_at as nextRunAt,
|
|
594
|
+
created_at as createdAt,
|
|
595
|
+
updated_at as updatedAt,
|
|
596
|
+
deleted_at as deletedAt
|
|
597
|
+
FROM platform_agent_instances
|
|
598
|
+
WHERE user_id = ? AND definition_id = ?`).get(userId, definitionId);
|
|
599
|
+
}
|
|
600
|
+
getLastRun(userId, definitionId) {
|
|
601
|
+
if (definitionId === 'suggested_planner') {
|
|
602
|
+
this.purgeLegacySuggestedPlannerWorkflowRuns({ userId });
|
|
603
|
+
}
|
|
604
|
+
const row = this.db.prepare(`SELECT run_id as runId,
|
|
605
|
+
user_id as userId,
|
|
606
|
+
definition_id as definitionId,
|
|
607
|
+
mode,
|
|
608
|
+
status,
|
|
609
|
+
created_at as createdAt,
|
|
610
|
+
completed_at as completedAt,
|
|
611
|
+
error,
|
|
612
|
+
generated_count as generatedCount,
|
|
613
|
+
resources_accessed_json as resourcesAccessedJson
|
|
614
|
+
FROM platform_agent_runs
|
|
615
|
+
WHERE user_id = ? AND definition_id = ?
|
|
616
|
+
ORDER BY created_at DESC
|
|
617
|
+
LIMIT 1`).get(userId, definitionId);
|
|
618
|
+
return row ? mapRunRow(row) : null;
|
|
619
|
+
}
|
|
620
|
+
getPlatformRunContextForAgent(agentId) {
|
|
621
|
+
const row = this.db.prepare(`SELECT pai.definition_id as definitionId,
|
|
622
|
+
pad.workflow_type as workflowType
|
|
623
|
+
FROM platform_agent_instances pai
|
|
624
|
+
JOIN platform_agent_definitions pad ON pad.definition_id = pai.definition_id
|
|
625
|
+
WHERE pai.agent_id = ?
|
|
626
|
+
LIMIT 1`).get(agentId);
|
|
627
|
+
return row ?? null;
|
|
628
|
+
}
|
|
629
|
+
async listPlatformAgentsForUser(params) {
|
|
630
|
+
this.purgeLegacySuggestedPlannerWorkflowRuns({ userId: params.userId });
|
|
631
|
+
await this.ensureBuiltinPlatformAgentsForUser(params);
|
|
632
|
+
for (const definition of this.listDefinitions()) {
|
|
633
|
+
const row = this.getInstanceRow(params.userId, definition.definitionId);
|
|
634
|
+
if (row?.deletedAt)
|
|
635
|
+
continue; // skip deleted instances; do not auto-recreate
|
|
636
|
+
await this.ensureInstanceForUser({ ...params, definitionId: definition.definitionId });
|
|
637
|
+
}
|
|
638
|
+
const result = [];
|
|
639
|
+
for (const definition of this.listDefinitions()) {
|
|
640
|
+
const instance = this.getInstance(params.userId, definition.definitionId);
|
|
641
|
+
if (!instance)
|
|
642
|
+
continue;
|
|
643
|
+
result.push({
|
|
644
|
+
definition,
|
|
645
|
+
instance,
|
|
646
|
+
agent: this.conversationManager.getAgent(instance.agentId),
|
|
647
|
+
lastRun: this.getLastRun(params.userId, definition.definitionId),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
return result;
|
|
651
|
+
}
|
|
652
|
+
async createDefinition(params) {
|
|
653
|
+
const definitionId = sanitizeDefinitionId(params.definitionId);
|
|
654
|
+
if (this.getDefinition(definitionId)) {
|
|
655
|
+
throw new Error(`Platform definition "${definitionId}" already exists.`);
|
|
656
|
+
}
|
|
657
|
+
const now = Date.now();
|
|
658
|
+
const toolPolicy = normalizePlatformAgentToolPolicy(params.workflowType, params.toolPolicy);
|
|
659
|
+
const resourceScopes = normalizePlatformResourceScopes(params.workflowType, params.resourceScopes);
|
|
660
|
+
this.db.prepare(`INSERT INTO platform_agent_definitions(
|
|
661
|
+
definition_id, workflow_type, label, description, resource_scopes_json,
|
|
662
|
+
resource_config_json, prompt_template_override, tool_policy_json,
|
|
663
|
+
enabled, created_at, updated_at
|
|
664
|
+
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(definitionId, params.workflowType, params.label.trim(), params.description.trim(), JSON.stringify(resourceScopes), JSON.stringify(params.resourceConfig ?? {}), params.promptTemplateOverride?.trim() || null, JSON.stringify(toolPolicy), params.enabled === false ? 0 : 1, now, now);
|
|
665
|
+
const users = this.db.prepare(`SELECT id as userId, username FROM users`).all();
|
|
666
|
+
for (const user of users) {
|
|
667
|
+
await this.ensureInstanceForUser({ userId: user.userId, username: user.username, definitionId });
|
|
668
|
+
}
|
|
669
|
+
return this.getDefinition(definitionId);
|
|
670
|
+
}
|
|
671
|
+
async updateDefinition(definitionId, patch) {
|
|
672
|
+
const existing = this.getDefinition(definitionId);
|
|
673
|
+
if (!existing)
|
|
674
|
+
throw new Error('Platform definition not found.');
|
|
675
|
+
const now = Date.now();
|
|
676
|
+
const nextWorkflowType = existing.workflowType;
|
|
677
|
+
const nextLabel = patch.label?.trim() || existing.label;
|
|
678
|
+
const nextDescription = patch.description?.trim() || existing.description;
|
|
679
|
+
const nextResourceScopes = patch.resourceScopes
|
|
680
|
+
? normalizePlatformResourceScopes(nextWorkflowType, patch.resourceScopes)
|
|
681
|
+
: existing.resourceScopes;
|
|
682
|
+
const nextResourceConfig = Object.prototype.hasOwnProperty.call(patch, 'resourceConfig')
|
|
683
|
+
? (patch.resourceConfig ?? {})
|
|
684
|
+
: (existing.resourceConfig ?? {});
|
|
685
|
+
const nextPromptTemplate = Object.prototype.hasOwnProperty.call(patch, 'promptTemplateOverride')
|
|
686
|
+
? patch.promptTemplateOverride?.trim() || null
|
|
687
|
+
: existing.promptTemplateOverride ?? null;
|
|
688
|
+
const nextToolPolicy = patch.toolPolicy
|
|
689
|
+
? normalizePlatformAgentToolPolicy(nextWorkflowType, patch.toolPolicy)
|
|
690
|
+
: normalizePlatformAgentToolPolicy(nextWorkflowType, existing.toolPolicy);
|
|
691
|
+
const nextEnabled = patch.enabled ?? existing.enabled;
|
|
692
|
+
this.db.prepare(`UPDATE platform_agent_definitions
|
|
693
|
+
SET label = ?,
|
|
694
|
+
description = ?,
|
|
695
|
+
resource_scopes_json = ?,
|
|
696
|
+
resource_config_json = ?,
|
|
697
|
+
prompt_template_override = ?,
|
|
698
|
+
tool_policy_json = ?,
|
|
699
|
+
enabled = ?,
|
|
700
|
+
updated_at = ?
|
|
701
|
+
WHERE definition_id = ?`).run(nextLabel, nextDescription, JSON.stringify(nextResourceScopes), JSON.stringify(nextResourceConfig), nextPromptTemplate, JSON.stringify(nextToolPolicy), nextEnabled ? 1 : 0, now, definitionId);
|
|
702
|
+
if (nextEnabled && !existing.enabled) {
|
|
703
|
+
const users = this.db.prepare(`SELECT id as userId, username FROM users`).all();
|
|
704
|
+
for (const user of users) {
|
|
705
|
+
const row = this.getInstanceRow(user.userId, definitionId);
|
|
706
|
+
if (row?.deletedAt)
|
|
707
|
+
continue; // do not auto-recreate deleted instances
|
|
708
|
+
await this.ensureInstanceForUser({ userId: user.userId, username: user.username, definitionId });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
await this.syncInstancesForDefinition(definitionId);
|
|
712
|
+
if (!nextEnabled && existing.enabled) {
|
|
713
|
+
await this.cleanupPlatformAgentDefinitionRuntime({
|
|
714
|
+
definitionId,
|
|
715
|
+
reason: 'Platform agent definition disabled.',
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
return this.getDefinition(definitionId);
|
|
719
|
+
}
|
|
720
|
+
async ensureInstanceForUser(params) {
|
|
721
|
+
const existingRow = this.getInstanceRow(params.userId, params.definitionId);
|
|
722
|
+
if (existingRow?.deletedAt) {
|
|
723
|
+
throw new Error('Platform agent instance has been deleted.');
|
|
724
|
+
}
|
|
725
|
+
if (params.definitionId === 'suggested_planner') {
|
|
726
|
+
return this.syncBuiltinSuggestedPlannerInstance(params);
|
|
727
|
+
}
|
|
728
|
+
if (params.definitionId === 'library_curator') {
|
|
729
|
+
return this.syncBuiltinLibraryCuratorInstance(params);
|
|
730
|
+
}
|
|
731
|
+
const definition = this.getDefinition(params.definitionId);
|
|
732
|
+
if (!definition)
|
|
733
|
+
throw new Error('Platform definition not found.');
|
|
734
|
+
const existing = this.getInstance(params.userId, params.definitionId);
|
|
735
|
+
if (existing) {
|
|
736
|
+
return this.syncExistingCustomInstance({ ...params, definition, existing });
|
|
737
|
+
}
|
|
738
|
+
const now = Date.now();
|
|
739
|
+
const workspacePath = this.buildCustomWorkspacePath(definition, params.username);
|
|
740
|
+
fs.mkdirSync(workspacePath, { recursive: true });
|
|
741
|
+
const runtimeConfig = this.buildDefaultRuntimeConfig(definition.workflowType);
|
|
742
|
+
const nodeId = this.pickNodeId(definition.workflowType);
|
|
743
|
+
const toolPolicy = normalizePlatformAgentToolPolicy(definition.workflowType, definition.toolPolicy);
|
|
744
|
+
const agentId = randomUUID();
|
|
745
|
+
this.db.prepare(`INSERT INTO agents(
|
|
746
|
+
agent_id, name, agent_type, agent_role, model, reasoning_effort, claude_permission_mode,
|
|
747
|
+
codex_mode, codex_service_tier, channel_id, system_prompt, description, memory,
|
|
748
|
+
env_vars, disabled_tool_kinds, node_id, workspace_path, project_path, skill_roots,
|
|
749
|
+
created_at, updated_at
|
|
750
|
+
) VALUES(?, ?, ?, 'platform_agent', ?, ?, ?, ?, ?, ?, '', ?, '', ?, ?, ?, ?, NULL, NULL, ?, ?)`).run(agentId, definition.label, PLATFORM_AGENT_TYPE, runtimeConfig.model ?? 'deepseek-v4-pro', runtimeConfig.reasoningEffort ?? null, 'bypassPermissions', null, null, 'default', definition.description, Object.keys(runtimeConfig.envVars ?? {}).length > 0 ? JSON.stringify(runtimeConfig.envVars) : null, JSON.stringify(toolPolicy.disabledToolKinds ?? []), nodeId, workspacePath, now, now);
|
|
751
|
+
this.db.prepare(`INSERT OR IGNORE INTO agent_channel_memberships(agent_id, channel_id, is_home, joined_at)
|
|
752
|
+
VALUES(?, 'default', 1, ?)`).run(agentId, now);
|
|
753
|
+
this.db.prepare(`INSERT OR IGNORE INTO user_agent_access(user_id, agent_id, granted_at) VALUES(?, ?, ?)`).run(params.userId, agentId, now);
|
|
754
|
+
const instance = this.upsertInstance({
|
|
755
|
+
userId: params.userId,
|
|
756
|
+
definitionId: params.definitionId,
|
|
757
|
+
agentId,
|
|
758
|
+
enabled: definition.enabled,
|
|
759
|
+
scheduleIntervalMs: DEFAULT_PLATFORM_AGENT_INTERVAL_MS,
|
|
760
|
+
workflowConfig: this.workflowHandlers[definition.workflowType].buildDefaultWorkflowConfig(),
|
|
761
|
+
runtimeConfig,
|
|
762
|
+
lastRunAt: null,
|
|
763
|
+
nextRunAt: now + DEFAULT_PLATFORM_AGENT_INTERVAL_MS,
|
|
764
|
+
createdAt: now,
|
|
765
|
+
updatedAt: now,
|
|
766
|
+
});
|
|
767
|
+
return instance;
|
|
768
|
+
}
|
|
769
|
+
async openConversation(userId, username, definitionId, mode) {
|
|
770
|
+
const definition = this.getDefinition(definitionId);
|
|
771
|
+
if (!definition?.enabled)
|
|
772
|
+
return null;
|
|
773
|
+
const instance = await this.ensureInstanceForUser({ userId, username, definitionId });
|
|
774
|
+
if (!instance.enabled)
|
|
775
|
+
return null;
|
|
776
|
+
const title = getPlatformAgentConversationTitle({ definitionId, mode });
|
|
777
|
+
return this.conversationManager.openNamedAgentPrimaryThread?.({
|
|
778
|
+
agentId: instance.agentId,
|
|
779
|
+
title,
|
|
780
|
+
userId,
|
|
781
|
+
legacyTitles: mode === 'debug' ? getPlatformAgentLegacyConversationTitles(definitionId) : undefined,
|
|
782
|
+
}) ?? this.conversationManager.openAgentThread(instance.agentId, userId);
|
|
783
|
+
}
|
|
784
|
+
async updateInstance(params) {
|
|
785
|
+
const definition = this.getDefinition(params.definitionId);
|
|
786
|
+
if (!definition)
|
|
787
|
+
throw new Error('Platform definition not found.');
|
|
788
|
+
let instance = await this.ensureInstanceForUser(params);
|
|
789
|
+
if (isBuiltinPlatformAgentDefinitionId(params.definitionId)) {
|
|
790
|
+
instance = await this.updateBuiltinInstance({
|
|
791
|
+
...params,
|
|
792
|
+
definitionId: params.definitionId,
|
|
793
|
+
}, definition);
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
instance = this.updateCustomInstance({
|
|
797
|
+
definition,
|
|
798
|
+
instance,
|
|
799
|
+
enabled: params.enabled,
|
|
800
|
+
scheduleIntervalMs: params.scheduleIntervalMs,
|
|
801
|
+
workflowConfig: params.workflowConfig,
|
|
802
|
+
runtimeConfig: params.runtimeConfig,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
if (params.enabled === false) {
|
|
806
|
+
await this.cleanupPlatformAgentInstanceRuntime({
|
|
807
|
+
userId: params.userId,
|
|
808
|
+
definitionId: params.definitionId,
|
|
809
|
+
agentId: instance.agentId,
|
|
810
|
+
reason: 'Platform agent instance disabled.',
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
definition,
|
|
815
|
+
instance,
|
|
816
|
+
agent: this.conversationManager.getAgent(instance.agentId),
|
|
817
|
+
lastRun: this.getLastRun(params.userId, params.definitionId),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
deleteInstance(userId, definitionId) {
|
|
821
|
+
const definition = this.getDefinition(definitionId);
|
|
822
|
+
if (!definition)
|
|
823
|
+
return { ok: false, error: 'Platform definition not found' };
|
|
824
|
+
const existing = this.getInstanceRow(userId, definitionId);
|
|
825
|
+
if (!existing || existing.deletedAt) {
|
|
826
|
+
return { ok: false, error: 'Platform agent instance not found or already deleted' };
|
|
827
|
+
}
|
|
828
|
+
const now = Date.now();
|
|
829
|
+
const result = this.db.prepare(`UPDATE platform_agent_instances
|
|
830
|
+
SET deleted_at = ?,
|
|
831
|
+
enabled = 0,
|
|
832
|
+
next_run_at = NULL,
|
|
833
|
+
updated_at = ?
|
|
834
|
+
WHERE user_id = ? AND definition_id = ? AND deleted_at IS NULL`).run(now, now, userId, definitionId);
|
|
835
|
+
if (result.changes === 0) {
|
|
836
|
+
return { ok: false, error: 'Platform agent instance not found or already deleted' };
|
|
837
|
+
}
|
|
838
|
+
this.syncBuiltinLegacyMirrorFromRow({
|
|
839
|
+
...existing,
|
|
840
|
+
enabled: 0,
|
|
841
|
+
nextRunAt: null,
|
|
842
|
+
updatedAt: now,
|
|
843
|
+
deletedAt: now,
|
|
844
|
+
});
|
|
845
|
+
void this.cleanupPlatformAgentInstanceRuntime({
|
|
846
|
+
userId,
|
|
847
|
+
definitionId,
|
|
848
|
+
agentId: existing.agentId,
|
|
849
|
+
reason: 'Platform agent instance deleted.',
|
|
850
|
+
});
|
|
851
|
+
return { ok: true };
|
|
852
|
+
}
|
|
853
|
+
async cleanupPlatformAgentInstanceRuntime(params) {
|
|
854
|
+
const targets = this.db.prepare(`SELECT id as conversationId,
|
|
855
|
+
node_id as nodeId,
|
|
856
|
+
agent_type as agentType,
|
|
857
|
+
session_key as sessionKey
|
|
858
|
+
FROM conversations
|
|
859
|
+
WHERE agent_id = ?`).all(params.agentId);
|
|
860
|
+
const uniqueTargets = new Map();
|
|
861
|
+
for (const target of targets) {
|
|
862
|
+
const hostKey = `conversation:${target.conversationId}:${target.agentType}`;
|
|
863
|
+
uniqueTargets.set(hostKey, {
|
|
864
|
+
conversationId: target.conversationId,
|
|
865
|
+
nodeId: target.nodeId,
|
|
866
|
+
hostKey,
|
|
867
|
+
sessionKey: target.sessionKey,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
const now = Date.now();
|
|
871
|
+
this.db.transaction(() => {
|
|
872
|
+
for (const target of uniqueTargets.values()) {
|
|
873
|
+
this.db.prepare(`DELETE FROM node_dispatch_queue
|
|
874
|
+
WHERE host_key = ?
|
|
875
|
+
OR session_key = ?
|
|
876
|
+
OR conversation_id = ?`).run(target.hostKey, target.sessionKey, target.conversationId);
|
|
877
|
+
this.db.prepare(`DELETE FROM node_host_snapshots
|
|
878
|
+
WHERE host_key = ?
|
|
879
|
+
OR session_key = ?`).run(target.hostKey, target.sessionKey);
|
|
880
|
+
this.db.prepare(`DELETE FROM conversation_prompt_queue WHERE conversation_id = ?`).run(target.conversationId);
|
|
881
|
+
this.db.prepare(`UPDATE conversations
|
|
882
|
+
SET status = CASE
|
|
883
|
+
WHEN status IN ('queued', 'active', 'recovering', 'awaiting_approval') THEN 'idle'
|
|
884
|
+
ELSE status
|
|
885
|
+
END
|
|
886
|
+
WHERE id = ?`).run(target.conversationId);
|
|
887
|
+
this.db.prepare(`UPDATE runs
|
|
888
|
+
SET ended_at = COALESCE(ended_at, ?),
|
|
889
|
+
stop_reason = COALESCE(stop_reason, 'error'),
|
|
890
|
+
error = COALESCE(error, ?)
|
|
891
|
+
WHERE session_key = ?
|
|
892
|
+
AND ended_at IS NULL`).run(now, params.reason, target.sessionKey);
|
|
893
|
+
}
|
|
894
|
+
this.db.prepare(`UPDATE platform_agent_runs
|
|
895
|
+
SET status = 'failed',
|
|
896
|
+
completed_at = COALESCE(completed_at, ?),
|
|
897
|
+
error = COALESCE(error, ?)
|
|
898
|
+
WHERE user_id = ?
|
|
899
|
+
AND definition_id = ?
|
|
900
|
+
AND status = 'running'`).run(now, params.reason, params.userId, params.definitionId);
|
|
901
|
+
})();
|
|
902
|
+
if (!this.agentRuntimeCleanupBroker)
|
|
903
|
+
return;
|
|
904
|
+
const targetsByNode = new Map();
|
|
905
|
+
for (const target of uniqueTargets.values()) {
|
|
906
|
+
if (!target.nodeId)
|
|
907
|
+
continue;
|
|
908
|
+
const entry = targetsByNode.get(target.nodeId) ?? { hostKeys: new Set(), sessionKeys: new Set() };
|
|
909
|
+
entry.hostKeys.add(target.hostKey);
|
|
910
|
+
entry.sessionKeys.add(target.sessionKey);
|
|
911
|
+
targetsByNode.set(target.nodeId, entry);
|
|
912
|
+
}
|
|
913
|
+
for (const [nodeId, target] of targetsByNode.entries()) {
|
|
914
|
+
try {
|
|
915
|
+
await this.agentRuntimeCleanupBroker.cleanupAgentRuntime({
|
|
916
|
+
nodeId,
|
|
917
|
+
agentId: params.agentId,
|
|
918
|
+
hostKeys: [...target.hostKeys],
|
|
919
|
+
sessionKeys: [...target.sessionKeys],
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
log.warn('[platform-agent] runtime cleanup failed; stale node state will be rejected on reconnect', {
|
|
924
|
+
userId: params.userId,
|
|
925
|
+
definitionId: params.definitionId,
|
|
926
|
+
agentId: params.agentId,
|
|
927
|
+
nodeId,
|
|
928
|
+
error: String(error?.message ?? error),
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async cleanupPlatformAgentDefinitionRuntime(params) {
|
|
934
|
+
const rows = this.db.prepare(`SELECT user_id as userId,
|
|
935
|
+
definition_id as definitionId,
|
|
936
|
+
agent_id as agentId,
|
|
937
|
+
enabled,
|
|
938
|
+
schedule_interval_ms as scheduleIntervalMs,
|
|
939
|
+
workflow_config_json as workflowConfigJson,
|
|
940
|
+
runtime_config_json as runtimeConfigJson,
|
|
941
|
+
last_run_at as lastRunAt,
|
|
942
|
+
next_run_at as nextRunAt,
|
|
943
|
+
created_at as createdAt,
|
|
944
|
+
updated_at as updatedAt,
|
|
945
|
+
deleted_at as deletedAt
|
|
946
|
+
FROM platform_agent_instances
|
|
947
|
+
WHERE definition_id = ?
|
|
948
|
+
AND deleted_at IS NULL`).all(params.definitionId);
|
|
949
|
+
for (const row of rows) {
|
|
950
|
+
await this.cleanupPlatformAgentInstanceRuntime({
|
|
951
|
+
userId: row.userId,
|
|
952
|
+
definitionId: row.definitionId,
|
|
953
|
+
agentId: row.agentId,
|
|
954
|
+
reason: params.reason,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
syncBuiltinLegacyMirrorFromRow(row) {
|
|
959
|
+
this.syncBuiltinLegacyMirror(mapInstanceRow(row));
|
|
960
|
+
}
|
|
961
|
+
syncBuiltinLegacyMirror(instance) {
|
|
962
|
+
if (instance.definitionId === 'suggested_planner') {
|
|
963
|
+
this.syncSuggestedPlannerLegacyMirror(instance);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (instance.definitionId === 'library_curator') {
|
|
967
|
+
this.syncLibraryCuratorLegacyMirror(instance);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
syncSuggestedPlannerLegacyMirror(instance) {
|
|
971
|
+
try {
|
|
972
|
+
const now = Date.now();
|
|
973
|
+
this.db.prepare(`UPDATE suggested_planner_configs
|
|
974
|
+
SET enabled = ?,
|
|
975
|
+
schedule_interval_ms = ?,
|
|
976
|
+
lookback_days = ?,
|
|
977
|
+
last_run_at = ?,
|
|
978
|
+
next_run_at = ?,
|
|
979
|
+
updated_at = ?
|
|
980
|
+
WHERE user_id = ?`).run(instance.enabled ? 1 : 0, Math.max(60_000, instance.scheduleIntervalMs), this.getPlannerLookbackDays(instance.workflowConfig), instance.lastRunAt, instance.enabled ? instance.nextRunAt : null, now, instance.userId);
|
|
981
|
+
}
|
|
982
|
+
catch {
|
|
983
|
+
// Legacy mirror may be absent in narrow tests or future schemas.
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
syncLibraryCuratorLegacyMirror(instance) {
|
|
987
|
+
try {
|
|
988
|
+
const now = Date.now();
|
|
989
|
+
this.db.prepare(`UPDATE library_curator_configs
|
|
990
|
+
SET enabled = ?,
|
|
991
|
+
schedule_interval_ms = ?,
|
|
992
|
+
categories_json = ?,
|
|
993
|
+
last_run_at = ?,
|
|
994
|
+
next_run_at = ?,
|
|
995
|
+
updated_at = ?
|
|
996
|
+
WHERE user_id = ?`).run(instance.enabled ? 1 : 0, Math.max(60_000, instance.scheduleIntervalMs), JSON.stringify(this.getCuratorCategories(instance.workflowConfig)), instance.lastRunAt, instance.enabled ? instance.nextRunAt : null, now, instance.userId);
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
// Legacy mirror may be absent in narrow tests or future schemas.
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
restoreInstance(userId, definitionId) {
|
|
1003
|
+
const definition = this.getDefinition(definitionId);
|
|
1004
|
+
if (!definition)
|
|
1005
|
+
throw new Error('Platform definition not found.');
|
|
1006
|
+
const now = Date.now();
|
|
1007
|
+
const row = this.getInstanceRow(userId, definitionId);
|
|
1008
|
+
if (!row || !row.deletedAt) {
|
|
1009
|
+
throw new Error('Platform agent instance not found or not deleted');
|
|
1010
|
+
}
|
|
1011
|
+
const scheduleIntervalMs = row.scheduleIntervalMs ?? DEFAULT_PLATFORM_AGENT_INTERVAL_MS;
|
|
1012
|
+
const result = this.db.prepare(`UPDATE platform_agent_instances
|
|
1013
|
+
SET deleted_at = NULL,
|
|
1014
|
+
enabled = 1,
|
|
1015
|
+
next_run_at = ?,
|
|
1016
|
+
updated_at = ?
|
|
1017
|
+
WHERE user_id = ? AND definition_id = ? AND deleted_at IS NOT NULL`).run(now + scheduleIntervalMs, now, userId, definitionId);
|
|
1018
|
+
if (result.changes === 0) {
|
|
1019
|
+
throw new Error('Platform agent instance not found or not deleted');
|
|
1020
|
+
}
|
|
1021
|
+
const restored = this.getInstance(userId, definitionId);
|
|
1022
|
+
if (!restored) {
|
|
1023
|
+
throw new Error('Platform agent instance restore failed');
|
|
1024
|
+
}
|
|
1025
|
+
this.syncBuiltinLegacyMirror(restored);
|
|
1026
|
+
return restored;
|
|
1027
|
+
}
|
|
1028
|
+
async dispatchWorkflowRun(params) {
|
|
1029
|
+
if (params.definitionId === 'suggested_planner') {
|
|
1030
|
+
this.purgeLegacySuggestedPlannerWorkflowRuns({ userId: params.userId });
|
|
1031
|
+
}
|
|
1032
|
+
const definition = this.getDefinition(params.definitionId);
|
|
1033
|
+
if (!definition)
|
|
1034
|
+
return { ok: false, error: 'Platform definition not found' };
|
|
1035
|
+
const instance = await this.ensureInstanceForUser(params);
|
|
1036
|
+
if (!definition.enabled) {
|
|
1037
|
+
return { ok: true, skipped: true, reason: 'Platform definition is disabled' };
|
|
1038
|
+
}
|
|
1039
|
+
if (!instance.enabled) {
|
|
1040
|
+
return { ok: true, skipped: true, reason: 'Platform instance is disabled' };
|
|
1041
|
+
}
|
|
1042
|
+
this.reconcileTerminalPlatformRunRuntimeState(params.userId, params.definitionId);
|
|
1043
|
+
await this.reconcileCompletedRunningRuns(params.userId, params.definitionId);
|
|
1044
|
+
const running = this.db.prepare(`SELECT run_id as runId
|
|
1045
|
+
FROM platform_agent_runs
|
|
1046
|
+
WHERE user_id = ? AND definition_id = ? AND status = 'running'
|
|
1047
|
+
ORDER BY created_at DESC
|
|
1048
|
+
LIMIT 1`).get(params.userId, params.definitionId);
|
|
1049
|
+
if (running) {
|
|
1050
|
+
return { ok: true, skipped: true, queued: true, runId: running.runId, reason: 'Platform agent run already in progress' };
|
|
1051
|
+
}
|
|
1052
|
+
const handler = this.workflowHandlers[definition.workflowType];
|
|
1053
|
+
const snapshot = await this.buildResourceSnapshot({
|
|
1054
|
+
userId: params.userId,
|
|
1055
|
+
username: params.username,
|
|
1056
|
+
definition,
|
|
1057
|
+
instance,
|
|
1058
|
+
});
|
|
1059
|
+
const dispatchInput = {
|
|
1060
|
+
definition,
|
|
1061
|
+
instance,
|
|
1062
|
+
userId: params.userId,
|
|
1063
|
+
username: params.username,
|
|
1064
|
+
snapshot,
|
|
1065
|
+
};
|
|
1066
|
+
const skip = await handler.shouldSkip?.(dispatchInput);
|
|
1067
|
+
if (skip) {
|
|
1068
|
+
this.scheduleNextRun(params.userId, params.definitionId, Date.now(), instance.scheduleIntervalMs);
|
|
1069
|
+
return { ok: true, skipped: true, reason: skip.reason };
|
|
1070
|
+
}
|
|
1071
|
+
const conversation = await this.openConversation(params.userId, params.username, params.definitionId, 'workflow');
|
|
1072
|
+
if (!conversation)
|
|
1073
|
+
return { ok: false, error: 'Failed to open platform workflow conversation' };
|
|
1074
|
+
const prompt = handler.buildPrompt(dispatchInput);
|
|
1075
|
+
const activationMetadata = {
|
|
1076
|
+
platformRun: {
|
|
1077
|
+
definitionId: definition.definitionId,
|
|
1078
|
+
workflowType: definition.workflowType,
|
|
1079
|
+
mode: 'workflow',
|
|
1080
|
+
},
|
|
1081
|
+
suppressReplyContract: true,
|
|
1082
|
+
};
|
|
1083
|
+
const result = await this.conversationManager.submitPrompt(conversation.id, prompt, {
|
|
1084
|
+
recordAsUserMessage: false,
|
|
1085
|
+
activationMetadata,
|
|
1086
|
+
suppressReplyContract: true,
|
|
1087
|
+
});
|
|
1088
|
+
const now = Date.now();
|
|
1089
|
+
if (!result.runId) {
|
|
1090
|
+
this.scheduleNextRun(params.userId, params.definitionId, now, instance.scheduleIntervalMs);
|
|
1091
|
+
return { ok: false, error: 'Failed to start platform agent run', queued: result.queued };
|
|
1092
|
+
}
|
|
1093
|
+
const existingRun = this.db.prepare(`SELECT status
|
|
1094
|
+
FROM platform_agent_runs
|
|
1095
|
+
WHERE run_id = ?`).get(result.runId);
|
|
1096
|
+
if (existingRun) {
|
|
1097
|
+
this.reconcileTerminalPlatformRunRuntimeState(params.userId, params.definitionId);
|
|
1098
|
+
return {
|
|
1099
|
+
ok: true,
|
|
1100
|
+
skipped: true,
|
|
1101
|
+
queued: true,
|
|
1102
|
+
runId: result.runId,
|
|
1103
|
+
reason: existingRun.status === 'running'
|
|
1104
|
+
? 'Platform agent run already in progress'
|
|
1105
|
+
: 'Platform agent runtime returned an already recorded run',
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const resourcesAccessed = {
|
|
1109
|
+
savedItems: Array.isArray(snapshot.savedInboxItems) ? snapshot.savedInboxItems.length : 0,
|
|
1110
|
+
tasks: Array.isArray(snapshot.openTasks) ? snapshot.openTasks.length : 0,
|
|
1111
|
+
messages: Array.isArray(snapshot.recentMessages) ? snapshot.recentMessages.length : 0,
|
|
1112
|
+
};
|
|
1113
|
+
this.db.prepare(`INSERT INTO platform_agent_runs(
|
|
1114
|
+
run_id, user_id, definition_id, agent_id, mode, status, created_at,
|
|
1115
|
+
input_snapshot_json, generated_count, resources_accessed_json
|
|
1116
|
+
) VALUES(?, ?, ?, ?, 'workflow', 'running', ?, ?, 0, ?)`).run(result.runId, params.userId, definition.definitionId, instance.agentId, now, JSON.stringify(snapshot), JSON.stringify(resourcesAccessed));
|
|
1117
|
+
this.db.prepare(`UPDATE platform_agent_instances
|
|
1118
|
+
SET last_run_at = ?,
|
|
1119
|
+
updated_at = ?
|
|
1120
|
+
WHERE user_id = ? AND definition_id = ?`).run(now, now, params.userId, definition.definitionId);
|
|
1121
|
+
await this.reconcileRunStateAfterInsert({
|
|
1122
|
+
runId: result.runId,
|
|
1123
|
+
definitionId: definition.definitionId,
|
|
1124
|
+
});
|
|
1125
|
+
return { ok: true, runId: result.runId, queued: result.queued, generatedCount: 0 };
|
|
1126
|
+
}
|
|
1127
|
+
async finalizeRun(params) {
|
|
1128
|
+
const row = this.db.prepare(`SELECT run_id as runId,
|
|
1129
|
+
user_id as userId,
|
|
1130
|
+
definition_id as definitionId,
|
|
1131
|
+
mode,
|
|
1132
|
+
status,
|
|
1133
|
+
created_at as createdAt,
|
|
1134
|
+
completed_at as completedAt,
|
|
1135
|
+
error,
|
|
1136
|
+
generated_count as generatedCount,
|
|
1137
|
+
resources_accessed_json as resourcesAccessedJson
|
|
1138
|
+
FROM platform_agent_runs
|
|
1139
|
+
WHERE run_id = ?`).get(params.runId);
|
|
1140
|
+
if (!row || row.status !== 'running')
|
|
1141
|
+
return;
|
|
1142
|
+
const definition = this.getDefinition(row.definitionId);
|
|
1143
|
+
const instance = definition ? this.getInstance(row.userId, row.definitionId) : null;
|
|
1144
|
+
if (!definition || !instance)
|
|
1145
|
+
return;
|
|
1146
|
+
const now = Date.now();
|
|
1147
|
+
const persistedError = params.error?.trim() || null;
|
|
1148
|
+
if (persistedError) {
|
|
1149
|
+
this.db.prepare(`UPDATE platform_agent_runs
|
|
1150
|
+
SET status = 'failed',
|
|
1151
|
+
completed_at = ?,
|
|
1152
|
+
error = ?
|
|
1153
|
+
WHERE run_id = ?`).run(now, persistedError, params.runId);
|
|
1154
|
+
this.scheduleNextRun(row.userId, row.definitionId, now, instance.scheduleIntervalMs);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
const rawOutputText = this.getRunOutputText(params.runId);
|
|
1159
|
+
const snapshotRow = this.db.prepare(`SELECT input_snapshot_json as inputSnapshotJson
|
|
1160
|
+
FROM platform_agent_runs
|
|
1161
|
+
WHERE run_id = ?`).get(params.runId);
|
|
1162
|
+
if (definition.definitionId === 'library_curator' && !snapshotRow?.inputSnapshotJson?.trim()) {
|
|
1163
|
+
const legacy = await this.finalizeLegacyCuratorRun(params.runId, row.userId, now);
|
|
1164
|
+
this.db.prepare(`UPDATE platform_agent_runs
|
|
1165
|
+
SET status = ?,
|
|
1166
|
+
completed_at = ?,
|
|
1167
|
+
raw_output_text = ?,
|
|
1168
|
+
error = ?,
|
|
1169
|
+
generated_count = ?
|
|
1170
|
+
WHERE run_id = ?`).run(legacy.status, legacy.completedAt, rawOutputText || null, legacy.error, legacy.generatedCount, params.runId);
|
|
1171
|
+
this.scheduleNextRun(row.userId, row.definitionId, now, instance.scheduleIntervalMs);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
const handler = this.workflowHandlers[definition.workflowType];
|
|
1175
|
+
const result = await handler.finalize({
|
|
1176
|
+
definition,
|
|
1177
|
+
instance,
|
|
1178
|
+
userId: row.userId,
|
|
1179
|
+
username: this.getUsernameForUserId(row.userId) ?? 'user',
|
|
1180
|
+
runId: params.runId,
|
|
1181
|
+
rawOutputText,
|
|
1182
|
+
snapshot: parseJsonRecord(snapshotRow?.inputSnapshotJson),
|
|
1183
|
+
});
|
|
1184
|
+
this.db.prepare(`UPDATE platform_agent_runs
|
|
1185
|
+
SET status = 'completed',
|
|
1186
|
+
completed_at = ?,
|
|
1187
|
+
raw_output_text = ?,
|
|
1188
|
+
output_json = ?,
|
|
1189
|
+
result_json = ?,
|
|
1190
|
+
generated_count = ?
|
|
1191
|
+
WHERE run_id = ?`).run(now, rawOutputText || null, result.outputJson ? JSON.stringify(result.outputJson) : null, result.resultJson ? JSON.stringify(result.resultJson) : null, result.generatedCount ?? 0, params.runId);
|
|
1192
|
+
}
|
|
1193
|
+
catch (error) {
|
|
1194
|
+
this.db.prepare(`UPDATE platform_agent_runs
|
|
1195
|
+
SET status = 'failed',
|
|
1196
|
+
completed_at = ?,
|
|
1197
|
+
raw_output_text = ?,
|
|
1198
|
+
error = ?
|
|
1199
|
+
WHERE run_id = ?`).run(now, this.getRunOutputText(params.runId) || null, String(error?.message ?? error), params.runId);
|
|
1200
|
+
}
|
|
1201
|
+
this.scheduleNextRun(row.userId, row.definitionId, now, instance.scheduleIntervalMs);
|
|
1202
|
+
}
|
|
1203
|
+
listDueInstances(now) {
|
|
1204
|
+
const rows = this.db.prepare(`SELECT pai.user_id as userId,
|
|
1205
|
+
pai.definition_id as definitionId,
|
|
1206
|
+
pai.agent_id as agentId,
|
|
1207
|
+
pai.enabled,
|
|
1208
|
+
pai.schedule_interval_ms as scheduleIntervalMs,
|
|
1209
|
+
pai.workflow_config_json as workflowConfigJson,
|
|
1210
|
+
pai.runtime_config_json as runtimeConfigJson,
|
|
1211
|
+
pai.last_run_at as lastRunAt,
|
|
1212
|
+
pai.next_run_at as nextRunAt,
|
|
1213
|
+
pai.created_at as createdAt,
|
|
1214
|
+
pai.updated_at as updatedAt,
|
|
1215
|
+
pai.deleted_at as deletedAt
|
|
1216
|
+
FROM platform_agent_instances pai
|
|
1217
|
+
JOIN platform_agent_definitions pad ON pad.definition_id = pai.definition_id
|
|
1218
|
+
WHERE pai.enabled = 1
|
|
1219
|
+
AND pad.enabled = 1
|
|
1220
|
+
AND pai.next_run_at IS NOT NULL
|
|
1221
|
+
AND pai.next_run_at <= ?
|
|
1222
|
+
AND pai.deleted_at IS NULL`).all(now);
|
|
1223
|
+
return rows.map(mapInstanceRow);
|
|
1224
|
+
}
|
|
1225
|
+
purgeLegacySuggestedPlannerWorkflowRuns(params) {
|
|
1226
|
+
const filters = [
|
|
1227
|
+
`r.ended_at IS NULL`,
|
|
1228
|
+
`r.prompt_text LIKE ?`,
|
|
1229
|
+
`(spr.run_id IS NOT NULL OR (par.run_id IS NOT NULL AND par.definition_id = 'suggested_planner') OR c.title = ?)`,
|
|
1230
|
+
];
|
|
1231
|
+
const args = [
|
|
1232
|
+
`${LEGACY_SUGGESTED_PLANNER_PROMPT_PREFIX}%`,
|
|
1233
|
+
getPlatformAgentConversationTitle({ definitionId: 'suggested_planner', mode: 'workflow' }),
|
|
1234
|
+
];
|
|
1235
|
+
if (params?.userId) {
|
|
1236
|
+
filters.push(`COALESCE(spr.user_id, par.user_id, c.user_id) = ?`);
|
|
1237
|
+
args.push(params.userId);
|
|
1238
|
+
}
|
|
1239
|
+
if (params?.runIds?.length) {
|
|
1240
|
+
filters.push(`r.run_id IN (${params.runIds.map(() => '?').join(', ')})`);
|
|
1241
|
+
args.push(...params.runIds);
|
|
1242
|
+
}
|
|
1243
|
+
const rows = this.db.prepare(`SELECT r.run_id as runId,
|
|
1244
|
+
COALESCE(spr.user_id, par.user_id, c.user_id) as userId,
|
|
1245
|
+
r.session_key as sessionKey,
|
|
1246
|
+
c.id as conversationId,
|
|
1247
|
+
c.agent_type as agentType,
|
|
1248
|
+
c.node_id as nodeId,
|
|
1249
|
+
h.current_run_id as snapshotRunId,
|
|
1250
|
+
h.host_key as hostKey,
|
|
1251
|
+
COALESCE(spc.schedule_interval_ms, pai.schedule_interval_ms, ?) as scheduleIntervalMs
|
|
1252
|
+
FROM runs r
|
|
1253
|
+
LEFT JOIN suggested_planner_runs spr ON spr.run_id = r.run_id
|
|
1254
|
+
LEFT JOIN platform_agent_runs par ON par.run_id = r.run_id
|
|
1255
|
+
LEFT JOIN conversations c ON c.session_key = r.session_key
|
|
1256
|
+
LEFT JOIN node_host_snapshots h
|
|
1257
|
+
ON h.node_id = c.node_id
|
|
1258
|
+
AND h.host_key = 'conversation:' || c.id || ':' || c.agent_type
|
|
1259
|
+
AND h.state = 'active'
|
|
1260
|
+
AND h.current_run_id = r.run_id
|
|
1261
|
+
LEFT JOIN suggested_planner_configs spc ON spc.user_id = COALESCE(spr.user_id, par.user_id, c.user_id)
|
|
1262
|
+
LEFT JOIN platform_agent_instances pai
|
|
1263
|
+
ON pai.user_id = COALESCE(spr.user_id, par.user_id, c.user_id)
|
|
1264
|
+
AND pai.definition_id = 'suggested_planner'
|
|
1265
|
+
WHERE ${filters.join(' AND ')}
|
|
1266
|
+
ORDER BY r.started_at ASC`).all(DEFAULT_PLATFORM_AGENT_INTERVAL_MS, ...args);
|
|
1267
|
+
if (rows.length === 0)
|
|
1268
|
+
return [];
|
|
1269
|
+
for (const row of rows) {
|
|
1270
|
+
if (!row.conversationId)
|
|
1271
|
+
continue;
|
|
1272
|
+
this.conversationManager.cancelConversationRun(row.conversationId, row.runId, {
|
|
1273
|
+
strictExpectedRunId: true,
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
const now = Date.now();
|
|
1277
|
+
const transaction = this.db.transaction(() => {
|
|
1278
|
+
for (const row of rows) {
|
|
1279
|
+
const nextRunAt = now + Math.max(5_000, row.scheduleIntervalMs ?? DEFAULT_PLATFORM_AGENT_INTERVAL_MS);
|
|
1280
|
+
this.db.prepare(`UPDATE runs
|
|
1281
|
+
SET ended_at = COALESCE(ended_at, ?),
|
|
1282
|
+
stop_reason = COALESCE(stop_reason, 'error'),
|
|
1283
|
+
error = COALESCE(error, ?)
|
|
1284
|
+
WHERE run_id = ?
|
|
1285
|
+
AND ended_at IS NULL`).run(now, LEGACY_SUGGESTED_PLANNER_CLEANUP_ERROR, row.runId);
|
|
1286
|
+
this.db.prepare(`UPDATE platform_agent_runs
|
|
1287
|
+
SET status = 'failed',
|
|
1288
|
+
completed_at = COALESCE(completed_at, ?),
|
|
1289
|
+
error = COALESCE(error, ?)
|
|
1290
|
+
WHERE run_id = ?
|
|
1291
|
+
AND status = 'running'`).run(now, LEGACY_SUGGESTED_PLANNER_CLEANUP_ERROR, row.runId);
|
|
1292
|
+
this.db.prepare(`UPDATE suggested_planner_runs
|
|
1293
|
+
SET status = 'failed',
|
|
1294
|
+
completed_at = COALESCE(completed_at, ?),
|
|
1295
|
+
error = COALESCE(error, ?)
|
|
1296
|
+
WHERE run_id = ?
|
|
1297
|
+
AND status = 'running'`).run(now, LEGACY_SUGGESTED_PLANNER_CLEANUP_ERROR, row.runId);
|
|
1298
|
+
if (row.userId) {
|
|
1299
|
+
this.db.prepare(`UPDATE suggested_planner_configs
|
|
1300
|
+
SET next_run_at = ?,
|
|
1301
|
+
updated_at = ?
|
|
1302
|
+
WHERE user_id = ?`).run(nextRunAt, now, row.userId);
|
|
1303
|
+
this.db.prepare(`UPDATE platform_agent_instances
|
|
1304
|
+
SET next_run_at = ?,
|
|
1305
|
+
updated_at = ?
|
|
1306
|
+
WHERE user_id = ?
|
|
1307
|
+
AND definition_id = 'suggested_planner'`).run(nextRunAt, now, row.userId);
|
|
1308
|
+
}
|
|
1309
|
+
this.db.prepare(`DELETE FROM node_dispatch_queue WHERE run_id = ?`).run(row.runId);
|
|
1310
|
+
if (row.conversationId) {
|
|
1311
|
+
this.db.prepare(`DELETE FROM conversation_prompt_queue WHERE conversation_id = ?`).run(row.conversationId);
|
|
1312
|
+
this.db.prepare(`UPDATE conversations
|
|
1313
|
+
SET status = 'idle',
|
|
1314
|
+
updated_at = ?
|
|
1315
|
+
WHERE id = ?
|
|
1316
|
+
AND status IN ('queued', 'active', 'recovering', 'awaiting_approval')`).run(now, row.conversationId);
|
|
1317
|
+
}
|
|
1318
|
+
if (row.nodeId && row.hostKey) {
|
|
1319
|
+
this.db.prepare(`DELETE FROM node_host_snapshots
|
|
1320
|
+
WHERE node_id = ?
|
|
1321
|
+
AND host_key = ?
|
|
1322
|
+
AND current_run_id = ?`).run(row.nodeId, row.hostKey, row.runId);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
transaction();
|
|
1327
|
+
return rows.map((row) => row.runId);
|
|
1328
|
+
}
|
|
1329
|
+
getUsernameForUserId(userId) {
|
|
1330
|
+
const row = this.db.prepare(`SELECT username FROM users WHERE id = ?`).get(userId);
|
|
1331
|
+
return row?.username?.trim() || null;
|
|
1332
|
+
}
|
|
1333
|
+
async syncBuiltinSuggestedPlannerInstance(params, override) {
|
|
1334
|
+
const config = await this.suggestedPlannerService.ensureConfigForUser(params);
|
|
1335
|
+
const definition = this.getDefinition('suggested_planner') ?? getBuiltinPlatformDefinition({ definitionId: 'suggested_planner' });
|
|
1336
|
+
const agent = this.suggestedPlannerService.getPlannerAgent(params.userId);
|
|
1337
|
+
const existing = this.getInstance(params.userId, 'suggested_planner');
|
|
1338
|
+
const defaultRuntimeConfig = {
|
|
1339
|
+
model: agent?.model ?? null,
|
|
1340
|
+
reasoningEffort: agent?.reasoningEffort ?? null,
|
|
1341
|
+
envVars: agent?.envVars ?? {},
|
|
1342
|
+
};
|
|
1343
|
+
if (agent?.agentRole !== 'platform_agent') {
|
|
1344
|
+
this.db.prepare(`UPDATE agents
|
|
1345
|
+
SET agent_role = 'platform_agent',
|
|
1346
|
+
description = ?,
|
|
1347
|
+
disabled_tool_kinds = ?,
|
|
1348
|
+
updated_at = ?
|
|
1349
|
+
WHERE agent_id = ?`).run(definition.description, JSON.stringify(normalizePlatformAgentToolPolicy(definition.workflowType, definition.toolPolicy).disabledToolKinds ?? []), Date.now(), config.plannerAgentId);
|
|
1350
|
+
}
|
|
1351
|
+
const workflowConfig = override?.workflowConfig
|
|
1352
|
+
?? existing?.workflowConfig
|
|
1353
|
+
?? { lookbackDays: config.lookbackDays };
|
|
1354
|
+
const runtimeConfig = override?.runtimeConfig
|
|
1355
|
+
?? existing?.runtimeConfig
|
|
1356
|
+
?? defaultRuntimeConfig;
|
|
1357
|
+
const instance = this.upsertInstance({
|
|
1358
|
+
userId: params.userId,
|
|
1359
|
+
definitionId: 'suggested_planner',
|
|
1360
|
+
agentId: config.plannerAgentId,
|
|
1361
|
+
enabled: override?.enabled ?? existing?.enabled ?? config.enabled,
|
|
1362
|
+
scheduleIntervalMs: override?.scheduleIntervalMs ?? existing?.scheduleIntervalMs ?? config.scheduleIntervalMs,
|
|
1363
|
+
workflowConfig,
|
|
1364
|
+
runtimeConfig,
|
|
1365
|
+
lastRunAt: override && Object.prototype.hasOwnProperty.call(override, 'lastRunAt')
|
|
1366
|
+
? override.lastRunAt ?? null
|
|
1367
|
+
: existing
|
|
1368
|
+
? existing.lastRunAt
|
|
1369
|
+
: config.lastRunAt,
|
|
1370
|
+
nextRunAt: override && Object.prototype.hasOwnProperty.call(override, 'nextRunAt')
|
|
1371
|
+
? override.nextRunAt ?? null
|
|
1372
|
+
: existing
|
|
1373
|
+
? existing.nextRunAt
|
|
1374
|
+
: config.nextRunAt,
|
|
1375
|
+
createdAt: config.createdAt,
|
|
1376
|
+
updatedAt: Date.now(),
|
|
1377
|
+
});
|
|
1378
|
+
this.syncSuggestedPlannerLegacyMirror(instance);
|
|
1379
|
+
return instance;
|
|
1380
|
+
}
|
|
1381
|
+
async syncBuiltinLibraryCuratorInstance(params, override) {
|
|
1382
|
+
const config = await this.libraryService.ensureConfigForUser(params);
|
|
1383
|
+
const definition = this.getDefinition('library_curator') ?? getBuiltinPlatformDefinition({ definitionId: 'library_curator' });
|
|
1384
|
+
const agent = this.libraryService.getCuratorAgent(params.userId);
|
|
1385
|
+
const existing = this.getInstance(params.userId, 'library_curator');
|
|
1386
|
+
const defaultRuntimeConfig = {
|
|
1387
|
+
model: agent?.model ?? null,
|
|
1388
|
+
reasoningEffort: agent?.reasoningEffort ?? null,
|
|
1389
|
+
envVars: agent?.envVars ?? {},
|
|
1390
|
+
};
|
|
1391
|
+
if (agent?.agentRole !== 'platform_agent') {
|
|
1392
|
+
this.db.prepare(`UPDATE agents
|
|
1393
|
+
SET agent_role = 'platform_agent',
|
|
1394
|
+
description = ?,
|
|
1395
|
+
disabled_tool_kinds = ?,
|
|
1396
|
+
updated_at = ?
|
|
1397
|
+
WHERE agent_id = ?`).run(definition.description, JSON.stringify(normalizePlatformAgentToolPolicy(definition.workflowType, definition.toolPolicy).disabledToolKinds ?? []), Date.now(), config.curatorAgentId);
|
|
1398
|
+
}
|
|
1399
|
+
const workflowConfig = override?.workflowConfig
|
|
1400
|
+
?? existing?.workflowConfig
|
|
1401
|
+
?? {
|
|
1402
|
+
categories: config.categories,
|
|
1403
|
+
libraryRootPath: config.libraryRootPath,
|
|
1404
|
+
};
|
|
1405
|
+
const runtimeConfig = override?.runtimeConfig
|
|
1406
|
+
?? existing?.runtimeConfig
|
|
1407
|
+
?? defaultRuntimeConfig;
|
|
1408
|
+
const instance = this.upsertInstance({
|
|
1409
|
+
userId: params.userId,
|
|
1410
|
+
definitionId: 'library_curator',
|
|
1411
|
+
agentId: config.curatorAgentId,
|
|
1412
|
+
enabled: override?.enabled ?? existing?.enabled ?? config.enabled,
|
|
1413
|
+
scheduleIntervalMs: override?.scheduleIntervalMs ?? existing?.scheduleIntervalMs ?? config.scheduleIntervalMs,
|
|
1414
|
+
workflowConfig,
|
|
1415
|
+
runtimeConfig,
|
|
1416
|
+
lastRunAt: override && Object.prototype.hasOwnProperty.call(override, 'lastRunAt')
|
|
1417
|
+
? override.lastRunAt ?? null
|
|
1418
|
+
: existing
|
|
1419
|
+
? existing.lastRunAt
|
|
1420
|
+
: config.lastRunAt,
|
|
1421
|
+
nextRunAt: override && Object.prototype.hasOwnProperty.call(override, 'nextRunAt')
|
|
1422
|
+
? override.nextRunAt ?? null
|
|
1423
|
+
: existing
|
|
1424
|
+
? existing.nextRunAt
|
|
1425
|
+
: config.nextRunAt,
|
|
1426
|
+
createdAt: config.createdAt,
|
|
1427
|
+
updatedAt: Date.now(),
|
|
1428
|
+
});
|
|
1429
|
+
this.syncLibraryCuratorLegacyMirror(instance);
|
|
1430
|
+
return instance;
|
|
1431
|
+
}
|
|
1432
|
+
async updateBuiltinInstance(params, definition) {
|
|
1433
|
+
if (params.definitionId === 'suggested_planner') {
|
|
1434
|
+
const existing = await this.suggestedPlannerService.ensureConfigForUser(params);
|
|
1435
|
+
const existingInstance = this.getInstance(params.userId, 'suggested_planner');
|
|
1436
|
+
const workflowConfig = {
|
|
1437
|
+
lookbackDays: this.getPlannerLookbackDays(params.workflowConfig ?? existingInstance?.workflowConfig ?? { lookbackDays: existing.lookbackDays }),
|
|
1438
|
+
};
|
|
1439
|
+
const runtimeConfig = {
|
|
1440
|
+
...(existingInstance?.runtimeConfig ?? { model: null, reasoningEffort: null, envVars: {} }),
|
|
1441
|
+
...(params.runtimeConfig ?? {}),
|
|
1442
|
+
envVars: Object.prototype.hasOwnProperty.call(params.runtimeConfig ?? {}, 'envVars')
|
|
1443
|
+
? params.runtimeConfig?.envVars ?? {}
|
|
1444
|
+
: existingInstance?.runtimeConfig.envVars ?? {},
|
|
1445
|
+
};
|
|
1446
|
+
const scheduleIntervalMs = Math.max(60_000, params.scheduleIntervalMs ?? existingInstance?.scheduleIntervalMs ?? existing.scheduleIntervalMs);
|
|
1447
|
+
const enabled = params.enabled ?? existingInstance?.enabled ?? existing.enabled;
|
|
1448
|
+
const now = Date.now();
|
|
1449
|
+
const nextRunAt = enabled
|
|
1450
|
+
? existingInstance?.nextRunAt ?? existing.nextRunAt ?? now + scheduleIntervalMs
|
|
1451
|
+
: null;
|
|
1452
|
+
this.suggestedPlannerService.updateConfig(params.userId, {
|
|
1453
|
+
enabled,
|
|
1454
|
+
scheduleIntervalMs,
|
|
1455
|
+
lookbackDays: this.getPlannerLookbackDays(workflowConfig),
|
|
1456
|
+
model: Object.prototype.hasOwnProperty.call(params.runtimeConfig ?? {}, 'model') ? params.runtimeConfig?.model ?? null : undefined,
|
|
1457
|
+
reasoningEffort: Object.prototype.hasOwnProperty.call(params.runtimeConfig ?? {}, 'reasoningEffort') ? params.runtimeConfig?.reasoningEffort ?? null : undefined,
|
|
1458
|
+
envVars: Object.prototype.hasOwnProperty.call(params.runtimeConfig ?? {}, 'envVars') ? params.runtimeConfig?.envVars ?? {} : undefined,
|
|
1459
|
+
});
|
|
1460
|
+
return this.syncBuiltinSuggestedPlannerInstance(params, {
|
|
1461
|
+
enabled,
|
|
1462
|
+
scheduleIntervalMs,
|
|
1463
|
+
workflowConfig,
|
|
1464
|
+
runtimeConfig,
|
|
1465
|
+
nextRunAt,
|
|
1466
|
+
lastRunAt: existingInstance?.lastRunAt ?? existing.lastRunAt,
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
const existing = await this.libraryService.ensureConfigForUser(params);
|
|
1470
|
+
const existingInstance = this.getInstance(params.userId, 'library_curator');
|
|
1471
|
+
const workflowConfig = {
|
|
1472
|
+
categories: this.getCuratorCategories(params.workflowConfig ?? existingInstance?.workflowConfig ?? { categories: existing.categories }),
|
|
1473
|
+
libraryRootPath: typeof existingInstance?.workflowConfig.libraryRootPath === 'string'
|
|
1474
|
+
? existingInstance.workflowConfig.libraryRootPath
|
|
1475
|
+
: existing.libraryRootPath,
|
|
1476
|
+
};
|
|
1477
|
+
const runtimeConfig = {
|
|
1478
|
+
...(existingInstance?.runtimeConfig ?? { model: null, reasoningEffort: null, envVars: {} }),
|
|
1479
|
+
...(params.runtimeConfig ?? {}),
|
|
1480
|
+
envVars: Object.prototype.hasOwnProperty.call(params.runtimeConfig ?? {}, 'envVars')
|
|
1481
|
+
? params.runtimeConfig?.envVars ?? {}
|
|
1482
|
+
: existingInstance?.runtimeConfig.envVars ?? {},
|
|
1483
|
+
};
|
|
1484
|
+
const scheduleIntervalMs = Math.max(60_000, params.scheduleIntervalMs ?? existingInstance?.scheduleIntervalMs ?? existing.scheduleIntervalMs);
|
|
1485
|
+
const enabled = params.enabled ?? existingInstance?.enabled ?? existing.enabled;
|
|
1486
|
+
const now = Date.now();
|
|
1487
|
+
const nextRunAt = enabled
|
|
1488
|
+
? existingInstance?.nextRunAt ?? existing.nextRunAt ?? now + scheduleIntervalMs
|
|
1489
|
+
: null;
|
|
1490
|
+
this.libraryService.updateConfig(params.userId, {
|
|
1491
|
+
enabled,
|
|
1492
|
+
scheduleIntervalMs,
|
|
1493
|
+
categories: workflowConfig.categories,
|
|
1494
|
+
});
|
|
1495
|
+
if (params.runtimeConfig) {
|
|
1496
|
+
this.libraryService.updateCuratorAgentRuntime(params.userId, params.runtimeConfig);
|
|
1497
|
+
}
|
|
1498
|
+
return this.syncBuiltinLibraryCuratorInstance(params, {
|
|
1499
|
+
enabled,
|
|
1500
|
+
scheduleIntervalMs,
|
|
1501
|
+
workflowConfig,
|
|
1502
|
+
runtimeConfig,
|
|
1503
|
+
nextRunAt,
|
|
1504
|
+
lastRunAt: existingInstance?.lastRunAt ?? existing.lastRunAt,
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
updateCustomInstance(params) {
|
|
1508
|
+
const now = Date.now();
|
|
1509
|
+
const workflowConfig = this.sanitizeWorkflowConfig(params.definition.workflowType, params.workflowConfig ?? params.instance.workflowConfig);
|
|
1510
|
+
const runtimeConfig = {
|
|
1511
|
+
...params.instance.runtimeConfig,
|
|
1512
|
+
...(params.runtimeConfig ?? {}),
|
|
1513
|
+
envVars: params.runtimeConfig?.envVars ? { ...params.runtimeConfig.envVars } : (params.instance.runtimeConfig.envVars ?? {}),
|
|
1514
|
+
};
|
|
1515
|
+
const enabled = params.enabled ?? params.instance.enabled;
|
|
1516
|
+
const scheduleIntervalMs = Math.max(60_000, params.scheduleIntervalMs ?? params.instance.scheduleIntervalMs);
|
|
1517
|
+
const nextRunAt = enabled ? params.instance.nextRunAt ?? now + scheduleIntervalMs : null;
|
|
1518
|
+
this.db.prepare(`UPDATE platform_agent_instances
|
|
1519
|
+
SET enabled = ?,
|
|
1520
|
+
schedule_interval_ms = ?,
|
|
1521
|
+
workflow_config_json = ?,
|
|
1522
|
+
runtime_config_json = ?,
|
|
1523
|
+
next_run_at = ?,
|
|
1524
|
+
updated_at = ?
|
|
1525
|
+
WHERE user_id = ? AND definition_id = ?`).run(enabled ? 1 : 0, scheduleIntervalMs, JSON.stringify(workflowConfig), JSON.stringify(runtimeConfig), nextRunAt, now, params.instance.userId, params.instance.definitionId);
|
|
1526
|
+
const toolPolicy = normalizePlatformAgentToolPolicy(params.definition.workflowType, params.definition.toolPolicy);
|
|
1527
|
+
this.db.prepare(`UPDATE agents
|
|
1528
|
+
SET name = ?,
|
|
1529
|
+
description = ?,
|
|
1530
|
+
model = ?,
|
|
1531
|
+
reasoning_effort = ?,
|
|
1532
|
+
env_vars = ?,
|
|
1533
|
+
disabled_tool_kinds = ?,
|
|
1534
|
+
updated_at = ?
|
|
1535
|
+
WHERE agent_id = ?`).run(params.definition.label, params.definition.description, runtimeConfig.model ?? null, runtimeConfig.reasoningEffort ?? null, Object.keys(runtimeConfig.envVars ?? {}).length > 0 ? JSON.stringify(runtimeConfig.envVars) : null, JSON.stringify(toolPolicy.disabledToolKinds ?? []), now, params.instance.agentId);
|
|
1536
|
+
return this.getInstance(params.instance.userId, params.instance.definitionId);
|
|
1537
|
+
}
|
|
1538
|
+
async syncExistingCustomInstance(params) {
|
|
1539
|
+
const now = Date.now();
|
|
1540
|
+
const desiredWorkspacePath = this.buildCustomWorkspacePath(params.definition, params.username);
|
|
1541
|
+
fs.mkdirSync(desiredWorkspacePath, { recursive: true });
|
|
1542
|
+
const runtimeConfig = {
|
|
1543
|
+
...params.existing.runtimeConfig,
|
|
1544
|
+
envVars: params.existing.runtimeConfig.envVars ?? {},
|
|
1545
|
+
};
|
|
1546
|
+
const toolPolicy = normalizePlatformAgentToolPolicy(params.definition.workflowType, params.definition.toolPolicy);
|
|
1547
|
+
const nodeId = this.pickNodeId(params.definition.workflowType);
|
|
1548
|
+
this.db.prepare(`UPDATE agents
|
|
1549
|
+
SET agent_role = 'platform_agent',
|
|
1550
|
+
name = ?,
|
|
1551
|
+
description = ?,
|
|
1552
|
+
node_id = ?,
|
|
1553
|
+
workspace_path = ?,
|
|
1554
|
+
model = ?,
|
|
1555
|
+
reasoning_effort = ?,
|
|
1556
|
+
env_vars = ?,
|
|
1557
|
+
disabled_tool_kinds = ?,
|
|
1558
|
+
updated_at = ?
|
|
1559
|
+
WHERE agent_id = ?`).run(params.definition.label, params.definition.description, nodeId, desiredWorkspacePath, runtimeConfig.model ?? null, runtimeConfig.reasoningEffort ?? null, Object.keys(runtimeConfig.envVars ?? {}).length > 0 ? JSON.stringify(runtimeConfig.envVars) : null, JSON.stringify(toolPolicy.disabledToolKinds ?? []), now, params.existing.agentId);
|
|
1560
|
+
this.db.prepare(`UPDATE conversations
|
|
1561
|
+
SET node_id = ?,
|
|
1562
|
+
workspace_path = ?,
|
|
1563
|
+
env_vars = ?,
|
|
1564
|
+
updated_at = ?
|
|
1565
|
+
WHERE agent_id = ?`).run(nodeId, desiredWorkspacePath, Object.keys(runtimeConfig.envVars ?? {}).length > 0 ? JSON.stringify(runtimeConfig.envVars) : null, now, params.existing.agentId);
|
|
1566
|
+
return this.getInstance(params.userId, params.definition.definitionId) ?? params.existing;
|
|
1567
|
+
}
|
|
1568
|
+
upsertInstance(params) {
|
|
1569
|
+
this.db.prepare(`INSERT INTO platform_agent_instances(
|
|
1570
|
+
user_id, definition_id, agent_id, enabled, schedule_interval_ms,
|
|
1571
|
+
workflow_config_json, runtime_config_json, last_run_at, next_run_at,
|
|
1572
|
+
created_at, updated_at
|
|
1573
|
+
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1574
|
+
ON CONFLICT(user_id, definition_id) DO UPDATE SET
|
|
1575
|
+
agent_id = excluded.agent_id,
|
|
1576
|
+
enabled = excluded.enabled,
|
|
1577
|
+
schedule_interval_ms = excluded.schedule_interval_ms,
|
|
1578
|
+
workflow_config_json = excluded.workflow_config_json,
|
|
1579
|
+
runtime_config_json = excluded.runtime_config_json,
|
|
1580
|
+
last_run_at = excluded.last_run_at,
|
|
1581
|
+
next_run_at = excluded.next_run_at,
|
|
1582
|
+
updated_at = excluded.updated_at`).run(params.userId, params.definitionId, params.agentId, params.enabled ? 1 : 0, Math.max(60_000, params.scheduleIntervalMs), JSON.stringify(this.sanitizeWorkflowConfig(this.getDefinition(params.definitionId)?.workflowType ?? 'planner_suggestions', params.workflowConfig)), JSON.stringify({
|
|
1583
|
+
model: params.runtimeConfig.model ?? null,
|
|
1584
|
+
reasoningEffort: params.runtimeConfig.reasoningEffort ?? null,
|
|
1585
|
+
envVars: params.runtimeConfig.envVars ?? {},
|
|
1586
|
+
}), params.lastRunAt, params.nextRunAt, params.createdAt, params.updatedAt);
|
|
1587
|
+
return this.getInstance(params.userId, params.definitionId);
|
|
1588
|
+
}
|
|
1589
|
+
async syncInstancesForDefinition(definitionId) {
|
|
1590
|
+
const definition = this.getDefinition(definitionId);
|
|
1591
|
+
if (!definition)
|
|
1592
|
+
return;
|
|
1593
|
+
if (isBuiltinPlatformAgentDefinitionId(definitionId))
|
|
1594
|
+
return;
|
|
1595
|
+
const rows = this.db.prepare(`SELECT user_id as userId FROM platform_agent_instances WHERE definition_id = ? AND deleted_at IS NULL`).all(definitionId);
|
|
1596
|
+
for (const row of rows) {
|
|
1597
|
+
const username = this.getUsernameForUserId(row.userId);
|
|
1598
|
+
if (!username)
|
|
1599
|
+
continue;
|
|
1600
|
+
await this.ensureInstanceForUser({ userId: row.userId, username, definitionId });
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
async reconcileRunStateAfterInsert(params) {
|
|
1604
|
+
const row = this.db.prepare(`SELECT r.stop_reason as stopReason,
|
|
1605
|
+
r.error as error,
|
|
1606
|
+
pad.workflow_type as workflowType,
|
|
1607
|
+
r.ended_at as endedAt
|
|
1608
|
+
FROM runs r
|
|
1609
|
+
JOIN platform_agent_definitions pad ON pad.definition_id = ?
|
|
1610
|
+
WHERE r.run_id = ?`).get(params.definitionId, params.runId);
|
|
1611
|
+
if (!row?.endedAt)
|
|
1612
|
+
return;
|
|
1613
|
+
await this.finalizeRun({
|
|
1614
|
+
runId: params.runId,
|
|
1615
|
+
error: getPlatformWorkflowTerminalError(row.workflowType, row.stopReason, row.error),
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
async reconcileCompletedRunningRuns(userId, definitionId) {
|
|
1619
|
+
const filters = [`par.status = 'running'`, `r.ended_at IS NOT NULL`];
|
|
1620
|
+
const args = [];
|
|
1621
|
+
if (userId) {
|
|
1622
|
+
filters.push(`par.user_id = ?`);
|
|
1623
|
+
args.push(userId);
|
|
1624
|
+
}
|
|
1625
|
+
if (definitionId) {
|
|
1626
|
+
filters.push(`par.definition_id = ?`);
|
|
1627
|
+
args.push(definitionId);
|
|
1628
|
+
}
|
|
1629
|
+
const rows = this.db.prepare(`SELECT par.run_id as runId,
|
|
1630
|
+
par.user_id as userId,
|
|
1631
|
+
pad.workflow_type as workflowType,
|
|
1632
|
+
r.stop_reason as stopReason,
|
|
1633
|
+
r.error as error
|
|
1634
|
+
FROM platform_agent_runs par
|
|
1635
|
+
JOIN runs r ON r.run_id = par.run_id
|
|
1636
|
+
JOIN platform_agent_definitions pad ON pad.definition_id = par.definition_id
|
|
1637
|
+
WHERE ${filters.join(' AND ')}
|
|
1638
|
+
ORDER BY par.created_at ASC`).all(...args);
|
|
1639
|
+
for (const row of rows) {
|
|
1640
|
+
await this.finalizeRun({
|
|
1641
|
+
runId: row.runId,
|
|
1642
|
+
error: getPlatformWorkflowTerminalError(row.workflowType, row.stopReason, row.error),
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
reconcileTerminalPlatformRunRuntimeState(userId, definitionId) {
|
|
1647
|
+
const filters = [`par.status <> 'running'`, `r.ended_at IS NULL`];
|
|
1648
|
+
const args = [];
|
|
1649
|
+
if (userId) {
|
|
1650
|
+
filters.push(`par.user_id = ?`);
|
|
1651
|
+
args.push(userId);
|
|
1652
|
+
}
|
|
1653
|
+
if (definitionId) {
|
|
1654
|
+
filters.push(`par.definition_id = ?`);
|
|
1655
|
+
args.push(definitionId);
|
|
1656
|
+
}
|
|
1657
|
+
const rows = this.db.prepare(`SELECT par.run_id as runId,
|
|
1658
|
+
par.status as status,
|
|
1659
|
+
par.completed_at as completedAt,
|
|
1660
|
+
par.error as error,
|
|
1661
|
+
c.id as conversationId
|
|
1662
|
+
FROM platform_agent_runs par
|
|
1663
|
+
JOIN runs r ON r.run_id = par.run_id
|
|
1664
|
+
LEFT JOIN conversations c ON c.session_key = r.session_key
|
|
1665
|
+
WHERE ${filters.join(' AND ')}
|
|
1666
|
+
ORDER BY par.created_at ASC`).all(...args);
|
|
1667
|
+
if (rows.length === 0)
|
|
1668
|
+
return;
|
|
1669
|
+
const now = Date.now();
|
|
1670
|
+
const transaction = this.db.transaction(() => {
|
|
1671
|
+
for (const row of rows) {
|
|
1672
|
+
const endedAt = row.completedAt ?? now;
|
|
1673
|
+
const completed = row.status === 'completed';
|
|
1674
|
+
this.db.prepare(`UPDATE runs
|
|
1675
|
+
SET ended_at = COALESCE(ended_at, ?),
|
|
1676
|
+
stop_reason = COALESCE(stop_reason, ?),
|
|
1677
|
+
error = CASE
|
|
1678
|
+
WHEN ? IS NULL THEN error
|
|
1679
|
+
ELSE COALESCE(error, ?)
|
|
1680
|
+
END
|
|
1681
|
+
WHERE run_id = ?
|
|
1682
|
+
AND ended_at IS NULL`).run(endedAt, completed ? 'completed' : 'error', completed ? null : row.error ?? 'Platform agent run already ended.', completed ? null : row.error ?? 'Platform agent run already ended.', row.runId);
|
|
1683
|
+
this.db.prepare(`DELETE FROM node_dispatch_queue WHERE run_id = ?`).run(row.runId);
|
|
1684
|
+
this.db.prepare(`DELETE FROM node_host_snapshots WHERE current_run_id = ?`).run(row.runId);
|
|
1685
|
+
if (row.conversationId) {
|
|
1686
|
+
this.db.prepare(`DELETE FROM conversation_prompt_queue WHERE conversation_id = ?`).run(row.conversationId);
|
|
1687
|
+
this.db.prepare(`UPDATE conversations
|
|
1688
|
+
SET status = 'idle',
|
|
1689
|
+
updated_at = ?
|
|
1690
|
+
WHERE id = ?
|
|
1691
|
+
AND status IN ('queued', 'active', 'recovering', 'awaiting_approval')`).run(now, row.conversationId);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
});
|
|
1695
|
+
transaction();
|
|
1696
|
+
}
|
|
1697
|
+
async finalizeLegacyCuratorRun(runId, userId, now) {
|
|
1698
|
+
await this.libraryService.finalizeOrganizeRun({ runId, userId });
|
|
1699
|
+
const row = this.db.prepare(`SELECT status,
|
|
1700
|
+
completed_at as completedAt,
|
|
1701
|
+
error
|
|
1702
|
+
FROM library_curator_runs
|
|
1703
|
+
WHERE run_id = ?`).get(runId);
|
|
1704
|
+
if (!row) {
|
|
1705
|
+
return {
|
|
1706
|
+
status: 'failed',
|
|
1707
|
+
completedAt: now,
|
|
1708
|
+
error: 'Legacy library curator run was not found during platform finalization.',
|
|
1709
|
+
generatedCount: 0,
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
if (row.status === 'completed') {
|
|
1713
|
+
return {
|
|
1714
|
+
status: 'completed',
|
|
1715
|
+
completedAt: row.completedAt ?? now,
|
|
1716
|
+
error: null,
|
|
1717
|
+
generatedCount: 0,
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
return {
|
|
1721
|
+
status: 'failed',
|
|
1722
|
+
completedAt: row.completedAt ?? now,
|
|
1723
|
+
error: row.error ?? 'Legacy library curator run did not complete successfully.',
|
|
1724
|
+
generatedCount: 0,
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
buildCustomWorkspacePath(definition, username) {
|
|
1728
|
+
if (definition.definitionId === 'suggested_planner')
|
|
1729
|
+
return buildPlannerWorkspacePath(this.workspaceRoot, username);
|
|
1730
|
+
if (definition.definitionId === 'library_curator')
|
|
1731
|
+
return buildCuratorWorkspacePath(this.workspaceRoot, username);
|
|
1732
|
+
return buildGenericPlatformWorkspacePath(this.workspaceRoot, definition.definitionId, username);
|
|
1733
|
+
}
|
|
1734
|
+
buildDefaultRuntimeConfig(workflowType) {
|
|
1735
|
+
const envVars = workflowType === 'library_curate' ? buildCuratorEnvVars() : buildPlannerEnvVars();
|
|
1736
|
+
return {
|
|
1737
|
+
model: envVars.ANTHROPIC_MODEL ?? 'deepseek-v4-pro',
|
|
1738
|
+
reasoningEffort: null,
|
|
1739
|
+
envVars,
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
pickNodeId(workflowType) {
|
|
1743
|
+
return workflowType === 'library_curate'
|
|
1744
|
+
? pickLibraryNodeId(this.nodeRegistry)
|
|
1745
|
+
: pickPlannerNodeId(this.nodeRegistry);
|
|
1746
|
+
}
|
|
1747
|
+
getPlannerLookbackDays(workflowConfig) {
|
|
1748
|
+
const raw = workflowConfig.lookbackDays;
|
|
1749
|
+
const value = typeof raw === 'number' ? raw : Number(raw);
|
|
1750
|
+
if (!Number.isFinite(value))
|
|
1751
|
+
return DEFAULT_LOOKBACK_DAYS;
|
|
1752
|
+
return Math.max(1, Math.min(90, Math.round(value)));
|
|
1753
|
+
}
|
|
1754
|
+
getCuratorCategories(workflowConfig) {
|
|
1755
|
+
const raw = workflowConfig.categories;
|
|
1756
|
+
if (!Array.isArray(raw))
|
|
1757
|
+
return [];
|
|
1758
|
+
return raw.filter((item) => typeof item === 'string' && item.trim().length > 0).map((item) => item.trim());
|
|
1759
|
+
}
|
|
1760
|
+
sanitizeWorkflowConfig(workflowType, workflowConfig) {
|
|
1761
|
+
if (workflowType === 'planner_suggestions') {
|
|
1762
|
+
return { lookbackDays: this.getPlannerLookbackDays(workflowConfig) };
|
|
1763
|
+
}
|
|
1764
|
+
return { categories: this.getCuratorCategories(workflowConfig) };
|
|
1765
|
+
}
|
|
1766
|
+
async buildResourceSnapshot(params) {
|
|
1767
|
+
const snapshot = {};
|
|
1768
|
+
const lookbackDays = this.getPlannerLookbackDays(params.instance.workflowConfig);
|
|
1769
|
+
const resourceConfig = params.definition.resourceConfig ?? {};
|
|
1770
|
+
for (const scope of params.definition.resourceScopes) {
|
|
1771
|
+
if (scope === 'saved_inbox') {
|
|
1772
|
+
snapshot.savedInboxItems = this.libraryService.listInboxSavedItems(params.userId);
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
if (scope === 'open_tasks') {
|
|
1776
|
+
snapshot.openTasks = this.listAccessibleOpenTasks(params.userId, params.username);
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
if (scope === 'recent_messages') {
|
|
1780
|
+
snapshot.recentMessages = this.listRecentMessages(params.userId, params.username, lookbackDays, resourceConfig);
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
if (scope === 'conversation_summaries') {
|
|
1784
|
+
snapshot.conversationSummaries = this.listConversationSummaries(params.userId, lookbackDays, resourceConfig);
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
if (scope === 'agent_memory') {
|
|
1788
|
+
snapshot.agentMemory = await this.readAccessibleAgentMemory(params.userId, resourceConfig);
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
if (scope === 'agent_notes') {
|
|
1792
|
+
snapshot.agentNotes = await this.readAccessibleAgentNotes(params.userId, resourceConfig);
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
if (scope === 'personal_library_inventory') {
|
|
1796
|
+
snapshot.personalLibraryInventory = await this.libraryService.listDocuments(params.userId, undefined, 'personal');
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
if (scope === 'personal_library_documents') {
|
|
1800
|
+
snapshot.personalLibraryDocuments = await this.readPersonalLibraryDocuments(params.userId, resourceConfig);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
return snapshot;
|
|
1804
|
+
}
|
|
1805
|
+
listAccessibleOpenTasks(userId, username) {
|
|
1806
|
+
const directTarget = `dm:@${username}`;
|
|
1807
|
+
return this.db.prepare(`SELECT t.task_id as taskId, t.channel_id as channelId, t.task_number as taskNumber,
|
|
1808
|
+
t.title, t.description, t.status, t.updated_at as updatedAt,
|
|
1809
|
+
t.claimed_by_name as assigneeName, c.name as channelName,
|
|
1810
|
+
CASE WHEN t.channel_id LIKE 'dm:%' THEN 'dm' ELSE 'channel' END as sourceType
|
|
1811
|
+
FROM tasks t
|
|
1812
|
+
LEFT JOIN channel_messages cm ON cm.message_id = t.message_id
|
|
1813
|
+
LEFT JOIN channels c ON c.channel_id = t.channel_id
|
|
1814
|
+
WHERE t.status != 'done'
|
|
1815
|
+
AND (
|
|
1816
|
+
(t.channel_id NOT LIKE 'dm:%' AND EXISTS (
|
|
1817
|
+
SELECT 1 FROM user_channel_access uca WHERE uca.channel_id = t.channel_id AND uca.user_id = ?
|
|
1818
|
+
))
|
|
1819
|
+
OR (t.channel_id LIKE 'dm:%' AND EXISTS (
|
|
1820
|
+
SELECT 1 FROM user_agent_access uaa WHERE uaa.user_id = ? AND uaa.agent_id = substr(t.channel_id, 4)
|
|
1821
|
+
) AND COALESCE(NULLIF(t.dm_target, ''), cm.target) = ?)
|
|
1822
|
+
)
|
|
1823
|
+
ORDER BY t.updated_at DESC
|
|
1824
|
+
LIMIT 40`).all(userId, userId, directTarget);
|
|
1825
|
+
}
|
|
1826
|
+
listRecentMessages(userId, username, lookbackDays, resourceConfig) {
|
|
1827
|
+
const directTarget = `dm:@${username}`;
|
|
1828
|
+
const directThreadTargetGlob = `${directTarget}:*`;
|
|
1829
|
+
const since = Date.now() - lookbackDays * 24 * 60 * 60 * 1000;
|
|
1830
|
+
const limit = Math.max(5, Math.min(100, Number(resourceConfig.maxRecentMessages ?? 30) || 30));
|
|
1831
|
+
return this.db.prepare(`SELECT message_id as messageId, channel_id as channelId, sender_name as senderName,
|
|
1832
|
+
target, content, created_at as createdAt
|
|
1833
|
+
FROM channel_messages
|
|
1834
|
+
WHERE created_at >= ?
|
|
1835
|
+
AND (
|
|
1836
|
+
(channel_id NOT LIKE 'dm:%' AND EXISTS (
|
|
1837
|
+
SELECT 1 FROM user_channel_access uca WHERE uca.channel_id = channel_messages.channel_id AND uca.user_id = ?
|
|
1838
|
+
))
|
|
1839
|
+
OR (channel_id LIKE 'dm:%' AND EXISTS (
|
|
1840
|
+
SELECT 1 FROM user_agent_access uaa WHERE uaa.user_id = ? AND uaa.agent_id = substr(channel_id, 4)
|
|
1841
|
+
) AND (target = ? OR target GLOB ?))
|
|
1842
|
+
)
|
|
1843
|
+
ORDER BY created_at DESC
|
|
1844
|
+
LIMIT ?`).all(since, userId, userId, directTarget, directThreadTargetGlob, limit);
|
|
1845
|
+
}
|
|
1846
|
+
listConversationSummaries(userId, lookbackDays, resourceConfig) {
|
|
1847
|
+
const since = Date.now() - lookbackDays * 24 * 60 * 60 * 1000;
|
|
1848
|
+
const limit = Math.max(5, Math.min(50, Number(resourceConfig.maxConversationSummaries ?? 20) || 20));
|
|
1849
|
+
const rows = this.db.prepare(`SELECT cs.conversation_id as conversationId, cs.reply_target as replyTarget,
|
|
1850
|
+
cs.summary_json as summaryJson, cs.updated_at as updatedAt
|
|
1851
|
+
FROM conversation_summaries cs
|
|
1852
|
+
JOIN conversations conv ON conv.id = cs.conversation_id
|
|
1853
|
+
WHERE conv.user_id = ?
|
|
1854
|
+
AND cs.updated_at >= ?
|
|
1855
|
+
ORDER BY cs.updated_at DESC
|
|
1856
|
+
LIMIT ?`).all(userId, since, limit);
|
|
1857
|
+
return rows.map((summary) => ({
|
|
1858
|
+
conversationId: summary.conversationId,
|
|
1859
|
+
replyTarget: summary.replyTarget,
|
|
1860
|
+
updatedAt: summary.updatedAt,
|
|
1861
|
+
summary: parseJsonRecord(summary.summaryJson),
|
|
1862
|
+
}));
|
|
1863
|
+
}
|
|
1864
|
+
async readAccessibleAgentMemory(userId, resourceConfig) {
|
|
1865
|
+
const agents = this.listAccessibleSnapshotAgents(userId, resourceConfig);
|
|
1866
|
+
const results = [];
|
|
1867
|
+
for (const agent of agents) {
|
|
1868
|
+
const content = await this.readWorkspaceTextFile(agent, 'MEMORY.md');
|
|
1869
|
+
if (!content)
|
|
1870
|
+
continue;
|
|
1871
|
+
results.push({
|
|
1872
|
+
agentId: agent.agentId,
|
|
1873
|
+
agentName: agent.name,
|
|
1874
|
+
content,
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
return results;
|
|
1878
|
+
}
|
|
1879
|
+
async readAccessibleAgentNotes(userId, resourceConfig) {
|
|
1880
|
+
const agents = this.listAccessibleSnapshotAgents(userId, resourceConfig);
|
|
1881
|
+
const results = [];
|
|
1882
|
+
const maxNotesPerAgent = Math.max(1, Math.min(10, Number(resourceConfig.maxNotesPerAgent ?? 5) || 5));
|
|
1883
|
+
for (const agent of agents) {
|
|
1884
|
+
if (!agent.workspacePath?.trim())
|
|
1885
|
+
continue;
|
|
1886
|
+
try {
|
|
1887
|
+
const listing = agent.nodeId
|
|
1888
|
+
? await this.workspaceBroker.listDirectory(agent.nodeId, agent.workspacePath, 'notes', { scaffold: false, agentName: agent.name, limit: 50 })
|
|
1889
|
+
: null;
|
|
1890
|
+
const files = (listing?.entries ?? [])
|
|
1891
|
+
.filter((entry) => entry.kind === 'file' && entry.path.toLowerCase().endsWith('.md'))
|
|
1892
|
+
.slice(0, maxNotesPerAgent);
|
|
1893
|
+
const notes = [];
|
|
1894
|
+
for (const file of files) {
|
|
1895
|
+
const content = await this.readWorkspaceTextFile(agent, file.path);
|
|
1896
|
+
if (!content)
|
|
1897
|
+
continue;
|
|
1898
|
+
notes.push({ relativePath: file.path, content });
|
|
1899
|
+
}
|
|
1900
|
+
if (notes.length > 0) {
|
|
1901
|
+
results.push({
|
|
1902
|
+
agentId: agent.agentId,
|
|
1903
|
+
agentName: agent.name,
|
|
1904
|
+
notes,
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
catch {
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
return results;
|
|
1913
|
+
}
|
|
1914
|
+
async readPersonalLibraryDocuments(userId, resourceConfig) {
|
|
1915
|
+
const docs = await this.libraryService.listDocuments(userId, undefined, 'personal');
|
|
1916
|
+
const maxDocuments = Math.max(1, Math.min(50, Number(resourceConfig.maxLibraryDocuments ?? 20) || 20));
|
|
1917
|
+
const selected = docs.slice(0, maxDocuments);
|
|
1918
|
+
const results = [];
|
|
1919
|
+
for (const doc of selected) {
|
|
1920
|
+
try {
|
|
1921
|
+
const file = await this.libraryService.readLibraryFile(userId, doc.relativePath, 'personal');
|
|
1922
|
+
results.push({
|
|
1923
|
+
title: doc.title,
|
|
1924
|
+
category: doc.category,
|
|
1925
|
+
topicPath: doc.topicPath,
|
|
1926
|
+
relativePath: doc.relativePath,
|
|
1927
|
+
summary: doc.summary,
|
|
1928
|
+
content: file.content,
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
catch {
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
return results;
|
|
1936
|
+
}
|
|
1937
|
+
listAccessibleSnapshotAgents(userId, resourceConfig) {
|
|
1938
|
+
const requestedAgentIds = Array.isArray(resourceConfig.agentIds)
|
|
1939
|
+
? resourceConfig.agentIds.filter((value) => typeof value === 'string' && value.trim().length > 0).map((value) => value.trim())
|
|
1940
|
+
: [];
|
|
1941
|
+
const rows = this.db.prepare(`SELECT a.agent_id as agentId
|
|
1942
|
+
FROM agents a
|
|
1943
|
+
JOIN user_agent_access uaa ON uaa.agent_id = a.agent_id
|
|
1944
|
+
WHERE uaa.user_id = ?
|
|
1945
|
+
AND a.deleted_at IS NULL
|
|
1946
|
+
ORDER BY a.updated_at DESC`).all(userId);
|
|
1947
|
+
const agents = rows
|
|
1948
|
+
.map((row) => this.conversationManager.getAgent(row.agentId))
|
|
1949
|
+
.filter((agent) => {
|
|
1950
|
+
if (!agent)
|
|
1951
|
+
return false;
|
|
1952
|
+
return agent.agentRole !== 'platform_agent' && agent.agentRole !== 'library_curator' && agent.agentRole !== 'suggested_planner';
|
|
1953
|
+
});
|
|
1954
|
+
if (requestedAgentIds.length > 0) {
|
|
1955
|
+
const allowed = new Set(requestedAgentIds);
|
|
1956
|
+
return agents.filter((agent) => allowed.has(agent.agentId));
|
|
1957
|
+
}
|
|
1958
|
+
return agents.slice(0, 6);
|
|
1959
|
+
}
|
|
1960
|
+
async readWorkspaceTextFile(agent, relativePath) {
|
|
1961
|
+
if (!agent.workspacePath?.trim())
|
|
1962
|
+
return null;
|
|
1963
|
+
try {
|
|
1964
|
+
if (agent.nodeId) {
|
|
1965
|
+
const result = await this.workspaceBroker.readFile(agent.nodeId, agent.workspacePath, relativePath, {
|
|
1966
|
+
scaffold: false,
|
|
1967
|
+
agentName: agent.name,
|
|
1968
|
+
});
|
|
1969
|
+
return result.content;
|
|
1970
|
+
}
|
|
1971
|
+
const absolutePath = path.join(agent.workspacePath, relativePath);
|
|
1972
|
+
return fs.existsSync(absolutePath) ? fs.readFileSync(absolutePath, 'utf8') : null;
|
|
1973
|
+
}
|
|
1974
|
+
catch {
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
resolveAccessiblePlannerTarget(userId, opportunity) {
|
|
1979
|
+
const agentId = opportunity.agentId?.trim() || null;
|
|
1980
|
+
const channelId = opportunity.channelId?.trim() || null;
|
|
1981
|
+
if (agentId && this.userCanAccessAgent(userId, agentId)) {
|
|
1982
|
+
return { channelId: `dm:${agentId}`, agentId, targetType: 'dm' };
|
|
1983
|
+
}
|
|
1984
|
+
if (channelId?.startsWith('dm:')) {
|
|
1985
|
+
const dmAgentId = channelId.slice(3).trim();
|
|
1986
|
+
if (dmAgentId && this.userCanAccessAgent(userId, dmAgentId)) {
|
|
1987
|
+
return { channelId, agentId: dmAgentId, targetType: 'dm' };
|
|
1988
|
+
}
|
|
1989
|
+
return { channelId: null, agentId: null, targetType: 'general' };
|
|
1990
|
+
}
|
|
1991
|
+
if (channelId && this.userCanAccessChannel(userId, channelId)) {
|
|
1992
|
+
return { channelId, agentId: null, targetType: 'channel' };
|
|
1993
|
+
}
|
|
1994
|
+
return { channelId: null, agentId: null, targetType: 'general' };
|
|
1995
|
+
}
|
|
1996
|
+
userCanAccessAgent(userId, agentId) {
|
|
1997
|
+
return Boolean(this.db.prepare(`SELECT 1
|
|
1998
|
+
FROM agents a
|
|
1999
|
+
JOIN user_agent_access uaa ON uaa.agent_id = a.agent_id
|
|
2000
|
+
WHERE a.agent_id = ?
|
|
2001
|
+
AND a.deleted_at IS NULL
|
|
2002
|
+
AND uaa.user_id = ?
|
|
2003
|
+
LIMIT 1`).get(agentId, userId));
|
|
2004
|
+
}
|
|
2005
|
+
userCanAccessChannel(userId, channelId) {
|
|
2006
|
+
return Boolean(this.db.prepare(`SELECT 1
|
|
2007
|
+
FROM channels c
|
|
2008
|
+
JOIN user_channel_access uca ON uca.channel_id = c.channel_id
|
|
2009
|
+
WHERE c.channel_id = ?
|
|
2010
|
+
AND uca.user_id = ?
|
|
2011
|
+
LIMIT 1`).get(channelId, userId));
|
|
2012
|
+
}
|
|
2013
|
+
getRunOutputText(runId) {
|
|
2014
|
+
const rows = this.db.prepare(`SELECT payload_json as payloadJson
|
|
2015
|
+
FROM events
|
|
2016
|
+
WHERE run_id = ?
|
|
2017
|
+
AND method = 'node/event'
|
|
2018
|
+
AND json_valid(payload_json)
|
|
2019
|
+
AND json_extract(payload_json, '$.type') IN ('content.delta', 'activity.delta')
|
|
2020
|
+
ORDER BY seq ASC`).all(runId);
|
|
2021
|
+
const byType = { 'content.delta': [], 'activity.delta': [] };
|
|
2022
|
+
for (const row of rows) {
|
|
2023
|
+
try {
|
|
2024
|
+
const payload = JSON.parse(row.payloadJson);
|
|
2025
|
+
if (typeof payload.type === 'string' && typeof payload.text === 'string') {
|
|
2026
|
+
byType[payload.type]?.push(payload.text);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
catch {
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
return byType['content.delta'].join('').trim() || byType['activity.delta'].join('').trim();
|
|
2034
|
+
}
|
|
2035
|
+
scheduleNextRun(userId, definitionId, now, scheduleIntervalMs) {
|
|
2036
|
+
this.db.prepare(`UPDATE platform_agent_instances
|
|
2037
|
+
SET next_run_at = ?,
|
|
2038
|
+
updated_at = ?
|
|
2039
|
+
WHERE user_id = ? AND definition_id = ?`).run(now + scheduleIntervalMs, now, userId, definitionId);
|
|
2040
|
+
}
|
|
2041
|
+
}
|