neoagent 2.5.2-beta.8 → 3.0.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.
Files changed (37) hide show
  1. package/README.md +5 -1
  2. package/docs/integrations.md +11 -7
  3. package/package.json +2 -2
  4. package/server/public/.last_build_id +1 -1
  5. package/server/public/flutter_bootstrap.js +1 -1
  6. package/server/public/main.dart.js +4 -4
  7. package/server/services/ai/deliverables/artifact_helpers.js +108 -16
  8. package/server/services/ai/deliverables/selector.js +17 -0
  9. package/server/services/ai/engine.js +2 -4724
  10. package/server/services/ai/loop/agent_engine_core.js +1590 -0
  11. package/server/services/ai/loop/blank_recovery.js +37 -0
  12. package/server/services/ai/loop/callbacks.js +151 -0
  13. package/server/services/ai/loop/completion_judge.js +252 -0
  14. package/server/services/ai/loop/conversation_loop.js +2447 -0
  15. package/server/services/ai/loop/delivery_state.js +27 -0
  16. package/server/services/ai/loop/error_recovery.js +38 -0
  17. package/server/services/ai/loop/iteration_budget.js +24 -0
  18. package/server/services/ai/loop/messaging_delivery.js +296 -0
  19. package/server/services/ai/loop/model_io.js +258 -0
  20. package/server/services/ai/loop/progress_classification.js +178 -0
  21. package/server/services/ai/loop/progress_monitor.js +81 -0
  22. package/server/services/ai/loop/run_state.js +356 -0
  23. package/server/services/ai/loop/tool_dispatch.js +231 -0
  24. package/server/services/ai/loopPolicy.js +15 -6
  25. package/server/services/ai/messagingFallback.js +23 -1
  26. package/server/services/ai/preModelCompaction.js +31 -0
  27. package/server/services/ai/repetitionGuard.js +47 -2
  28. package/server/services/ai/systemPrompt.js +6 -0
  29. package/server/services/ai/taskAnalysis.js +10 -0
  30. package/server/services/ai/toolEvidence.js +15 -3
  31. package/server/services/ai/toolResult.js +29 -0
  32. package/server/services/ai/toolSelector.js +20 -3
  33. package/server/services/ai/tools.js +196 -26
  34. package/server/services/integrations/github/common.js +2 -2
  35. package/server/services/integrations/github/repos.js +123 -55
  36. package/server/services/runtime/docker-vm-manager.js +21 -1
  37. package/server/services/workspace/manager.js +56 -0
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const { summarizeForLog } = require('../logFormat');
4
+
5
+ function latestFailedToolExecution(toolExecutions = []) {
6
+ return [...toolExecutions].reverse().find((item) => item && item.ok === false) || null;
7
+ }
8
+
9
+ function shouldContinueAfterBlankToolFailure({
10
+ lastContent = '',
11
+ failedStepCount = 0,
12
+ remainingIterations = 0,
13
+ toolExecutions = [],
14
+ } = {}) {
15
+ return !String(lastContent || '').trim()
16
+ && Number(failedStepCount || 0) > 0
17
+ && Number(remainingIterations || 0) > 0
18
+ && Boolean(latestFailedToolExecution(toolExecutions));
19
+ }
20
+
21
+ function buildBlankAfterToolFailureGuidance(toolExecutions = []) {
22
+ const failedExecution = latestFailedToolExecution(toolExecutions);
23
+ if (!failedExecution) return '';
24
+ const toolName = failedExecution.toolName || failedExecution.tool || 'the previous tool';
25
+ const failure = failedExecution.error || failedExecution.summary || failedExecution.status || 'unknown failure';
26
+ return [
27
+ `The previous tool "${toolName}" failed with: ${summarizeForLog(failure, 240)}.`,
28
+ 'The latest assistant turn returned no user-facing answer and no tool call, so the task is not terminal.',
29
+ 'Continue with the next safe recovery action: retry with corrected arguments, use another available tool, verify from existing evidence, or report a real blocker only if no autonomous path remains.',
30
+ ].join(' ');
31
+ }
32
+
33
+ module.exports = {
34
+ buildBlankAfterToolFailureGuidance,
35
+ latestFailedToolExecution,
36
+ shouldContinueAfterBlankToolFailure,
37
+ };
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ const db = require('../../../db/database');
4
+ const {
5
+ buildInterimMetadata,
6
+ buildInterimSignature,
7
+ normalizeInterimKind,
8
+ } = require('../interim');
9
+ const { normalizeInterimText } = require('../messagingFallback');
10
+ const { requireSuccessfulMessagingDelivery } = require('./messaging_delivery');
11
+
12
+ async function publishInterimUpdate(engine, {
13
+ userId,
14
+ runId,
15
+ agentId = null,
16
+ triggerSource = 'web',
17
+ conversationId = null,
18
+ platform = null,
19
+ chatId = null,
20
+ content,
21
+ kind,
22
+ expectsReply = false,
23
+ deferFollowUp = false,
24
+ } = {}) {
25
+ const runMeta = engine.getRunMeta(runId);
26
+ if (!runMeta || runMeta.aborted) {
27
+ return { sent: false, skipped: true, reason: 'Run is no longer active.' };
28
+ }
29
+
30
+ const normalizedKind = normalizeInterimKind(kind);
31
+ const normalizedContent = normalizeInterimText(
32
+ content,
33
+ triggerSource === 'messaging' ? platform : null
34
+ );
35
+ if (!normalizedContent || normalizedContent.toUpperCase() === '[NO RESPONSE]') {
36
+ return { sent: false, skipped: true, reason: 'Interim content must be non-empty.' };
37
+ }
38
+
39
+ const signature = buildInterimSignature({
40
+ content: normalizedContent,
41
+ kind: normalizedKind,
42
+ expectsReply,
43
+ platform: triggerSource === 'messaging' ? platform : 'web',
44
+ });
45
+ if (runMeta.interimSignatures?.has(signature)) {
46
+ return { sent: false, skipped: true, duplicate: true };
47
+ }
48
+
49
+ const metadata = buildInterimMetadata({
50
+ kind: normalizedKind,
51
+ expectsReply,
52
+ });
53
+ if (deferFollowUp === true) {
54
+ metadata.defer_follow_up = true;
55
+ }
56
+ const createdAt = new Date().toISOString();
57
+
58
+ if (triggerSource === 'messaging') {
59
+ if (!platform || !chatId || !engine.messagingManager) {
60
+ return { sent: false, skipped: true, reason: 'Messaging context is not available.' };
61
+ }
62
+ const deliveryResult = await engine.messagingManager.sendMessage(userId, platform, chatId, normalizedContent, {
63
+ agentId,
64
+ runId,
65
+ persistConversation: true,
66
+ metadata,
67
+ deliveryKind: 'interim',
68
+ });
69
+ requireSuccessfulMessagingDelivery(deliveryResult, 'Interim messaging delivery');
70
+ } else if (triggerSource === 'voice_live') {
71
+ const voiceSessionId = runMeta.voiceSessionId || null;
72
+ const manager = engine.voiceRuntimeManager || engine.app?.locals?.voiceRuntimeManager || null;
73
+ if (!voiceSessionId || !manager || typeof manager.publishInterimUpdate !== 'function') {
74
+ return { sent: false, skipped: true, reason: 'Voice session context is not available.' };
75
+ }
76
+ await manager.publishInterimUpdate({
77
+ sessionId: voiceSessionId,
78
+ content: normalizedContent,
79
+ kind: normalizedKind,
80
+ expectsReply,
81
+ deferFollowUp,
82
+ });
83
+ } else {
84
+ db.prepare(
85
+ 'INSERT INTO conversation_history (user_id, agent_id, agent_run_id, role, content, metadata) VALUES (?, ?, ?, ?, ?, ?)'
86
+ ).run(userId, agentId, runId, 'assistant', normalizedContent, JSON.stringify(metadata));
87
+
88
+ if (conversationId) {
89
+ db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
90
+ .run(conversationId, 'assistant', normalizedContent);
91
+ }
92
+ }
93
+
94
+ if (!runMeta.interimSignatures) runMeta.interimSignatures = new Set();
95
+ if (!Array.isArray(runMeta.interimMessages)) runMeta.interimMessages = [];
96
+ runMeta.interimSignatures.add(signature);
97
+ runMeta.interimMessages.push({
98
+ content: normalizedContent,
99
+ kind: normalizedKind,
100
+ expectsReply: expectsReply === true,
101
+ deferFollowUp: deferFollowUp === true,
102
+ createdAt,
103
+ });
104
+ runMeta.lastInterimMessage = normalizedContent;
105
+ engine.markRunVisibleProgress(runId, createdAt);
106
+
107
+ engine.emit(userId, 'run:assistant_interim', {
108
+ runId,
109
+ content: normalizedContent,
110
+ kind: normalizedKind,
111
+ expectsReply: expectsReply === true,
112
+ deferFollowUp: deferFollowUp === true,
113
+ triggerSource,
114
+ platform: triggerSource === 'messaging' ? platform : 'web',
115
+ });
116
+
117
+ const terminalInterim = expectsReply === true;
118
+ if (terminalInterim) {
119
+ runMeta.terminalInterim = {
120
+ kind: normalizedKind,
121
+ content: normalizedContent,
122
+ createdAt,
123
+ };
124
+ }
125
+ engine.persistRunMetadata(runId, {
126
+ latestInterim: {
127
+ kind: normalizedKind,
128
+ expectsReply: expectsReply === true,
129
+ deferFollowUp: deferFollowUp === true,
130
+ content: normalizedContent,
131
+ createdAt,
132
+ },
133
+ progressLedger: engine.buildProgressLedgerSnapshot(runMeta),
134
+ terminalInterim: terminalInterim
135
+ ? { kind: normalizedKind, content: normalizedContent, createdAt }
136
+ : null,
137
+ });
138
+
139
+ return {
140
+ sent: true,
141
+ kind: normalizedKind,
142
+ expectsReply: expectsReply === true,
143
+ deferFollowUp: deferFollowUp === true,
144
+ content: normalizedContent,
145
+ terminal: terminalInterim,
146
+ };
147
+ }
148
+
149
+ module.exports = {
150
+ publishInterimUpdate,
151
+ };
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ const { normalizeCompletionConfidence } = require('../completion');
4
+ const { normalizeOutgoingMessage } = require('../messagingFallback');
5
+ const {
6
+ summarizeAvailableTools,
7
+ summarizeToolExecutions,
8
+ } = require('../toolEvidence');
9
+
10
+ const GOAL_CONTRACT_SUCCESS_CRITERIA_LIMIT = 12;
11
+
12
+ function normalizeGoalCriteria(value) {
13
+ if (!Array.isArray(value)) return [];
14
+ const seen = new Set();
15
+ const items = [];
16
+ for (const entry of value) {
17
+ const text = String(entry || '').trim();
18
+ if (!text) continue;
19
+ const signature = text.toLowerCase();
20
+ if (seen.has(signature)) continue;
21
+ seen.add(signature);
22
+ items.push(text);
23
+ if (items.length >= GOAL_CONTRACT_SUCCESS_CRITERIA_LIMIT) break;
24
+ }
25
+ return items;
26
+ }
27
+
28
+ function normalizeGoalContract(raw = null) {
29
+ if (!raw || typeof raw !== 'object') return null;
30
+ const goal = String(raw.goal || '').trim();
31
+ const successCriteria = normalizeGoalCriteria(
32
+ raw.successCriteria || raw.success_criteria || [],
33
+ );
34
+ const rawCompletionConfidence = String(
35
+ raw.completionConfidenceRequired || raw.completion_confidence_required || '',
36
+ ).trim();
37
+ const completionConfidenceRequired = rawCompletionConfidence
38
+ ? normalizeCompletionConfidence(rawCompletionConfidence)
39
+ : '';
40
+ const progressUpdatePolicy = ['none', 'optional', 'required'].includes(String(
41
+ raw.progressUpdatePolicy || raw.progress_update_policy || '',
42
+ ).trim().toLowerCase())
43
+ ? String(raw.progressUpdatePolicy || raw.progress_update_policy || '').trim().toLowerCase()
44
+ : '';
45
+ const autonomyLevel = ['minimal', 'normal', 'high'].includes(String(
46
+ raw.autonomyLevel || raw.autonomy_level || '',
47
+ ).trim().toLowerCase())
48
+ ? String(raw.autonomyLevel || raw.autonomy_level || '').trim().toLowerCase()
49
+ : '';
50
+ const complexity = ['simple', 'standard', 'complex'].includes(String(
51
+ raw.complexity || '',
52
+ ).trim().toLowerCase())
53
+ ? String(raw.complexity || '').trim().toLowerCase()
54
+ : '';
55
+
56
+ if (
57
+ !goal
58
+ && successCriteria.length === 0
59
+ && !completionConfidenceRequired
60
+ && !progressUpdatePolicy
61
+ && !autonomyLevel
62
+ && !complexity
63
+ ) {
64
+ return null;
65
+ }
66
+
67
+ return {
68
+ goal,
69
+ successCriteria,
70
+ completionConfidenceRequired,
71
+ progressUpdatePolicy: progressUpdatePolicy || '',
72
+ autonomyLevel: autonomyLevel || '',
73
+ complexity: complexity || '',
74
+ };
75
+ }
76
+
77
+ function mergeGoalContracts(existing = null, patch = null) {
78
+ const current = normalizeGoalContract(existing) || null;
79
+ const nextPatch = normalizeGoalContract(patch) || null;
80
+ if (!current && !nextPatch) return null;
81
+
82
+ const goal = String(current?.goal || nextPatch?.goal || '').trim();
83
+ const successCriteria = normalizeGoalCriteria([
84
+ ...(current?.successCriteria || []),
85
+ ...(nextPatch?.successCriteria || []),
86
+ ]);
87
+ const completionConfidenceRequired = nextPatch?.completionConfidenceRequired
88
+ || current?.completionConfidenceRequired
89
+ || 'medium';
90
+ const progressUpdatePolicy = nextPatch?.progressUpdatePolicy
91
+ || current?.progressUpdatePolicy
92
+ || '';
93
+ const autonomyLevel = nextPatch?.autonomyLevel
94
+ || current?.autonomyLevel
95
+ || '';
96
+ const complexity = nextPatch?.complexity
97
+ || current?.complexity
98
+ || '';
99
+
100
+ return normalizeGoalContract({
101
+ goal,
102
+ successCriteria,
103
+ completionConfidenceRequired,
104
+ progressUpdatePolicy,
105
+ autonomyLevel,
106
+ complexity,
107
+ });
108
+ }
109
+
110
+ function goalContractFromAnalysis(analysis = null) {
111
+ if (!analysis || typeof analysis !== 'object') return null;
112
+ return normalizeGoalContract({
113
+ goal: analysis.goal,
114
+ successCriteria: analysis.success_criteria,
115
+ completionConfidenceRequired: analysis.completion_confidence_required,
116
+ progressUpdatePolicy: analysis.progress_update_policy,
117
+ autonomyLevel: analysis.autonomy_level,
118
+ complexity: analysis.complexity,
119
+ });
120
+ }
121
+
122
+ function goalContractFromPlan(plan = null) {
123
+ if (!plan || typeof plan !== 'object') return null;
124
+ return normalizeGoalContract({
125
+ successCriteria: plan.success_criteria,
126
+ });
127
+ }
128
+
129
+ function buildResolvedGoalContract(runMeta, analysis = null, plan = null) {
130
+ let contract = mergeGoalContracts(runMeta?.goalContract || null, goalContractFromAnalysis(analysis));
131
+ contract = mergeGoalContracts(contract, goalContractFromPlan(plan));
132
+ return contract;
133
+ }
134
+
135
+ function buildGoalContractPrompt(contract, label = 'Persistent run goal') {
136
+ const normalized = normalizeGoalContract(contract);
137
+ if (!normalized) return '';
138
+ const lines = [];
139
+ if (normalized.goal) {
140
+ lines.push(`${label}: ${normalized.goal}`);
141
+ }
142
+ if (normalized.successCriteria.length > 0) {
143
+ lines.push(`Persistent success criteria:\n- ${normalized.successCriteria.join('\n- ')}`);
144
+ }
145
+ const contractLine = [
146
+ normalized.complexity ? `complexity=${normalized.complexity}` : '',
147
+ normalized.autonomyLevel ? `autonomy_level=${normalized.autonomyLevel}` : '',
148
+ normalized.progressUpdatePolicy ? `progress_update_policy=${normalized.progressUpdatePolicy}` : '',
149
+ normalized.completionConfidenceRequired ? `completion_confidence_required=${normalized.completionConfidenceRequired}` : '',
150
+ ].filter(Boolean).join('; ');
151
+ if (contractLine) {
152
+ lines.push(`Persistent autonomy contract: ${contractLine}`);
153
+ }
154
+ return lines.join('\n');
155
+ }
156
+
157
+ function resolveRunGoalContext(runMeta, analysis = null, plan = null) {
158
+ const goalContract = buildResolvedGoalContract(runMeta, analysis, plan);
159
+ const successCriteria = goalContract?.successCriteria?.length
160
+ ? goalContract.successCriteria.slice(0, 6)
161
+ : (Array.isArray(plan?.success_criteria)
162
+ ? plan.success_criteria
163
+ .map((item) => String(item || '').trim())
164
+ .filter(Boolean)
165
+ .slice(0, 6)
166
+ : []);
167
+ const effectiveGoal = goalContract?.goal || analysis?.goal || '';
168
+ const effectiveComplexity = goalContract?.complexity || analysis?.complexity || 'standard';
169
+ const effectiveAutonomyLevel = goalContract?.autonomyLevel || analysis?.autonomy_level || 'normal';
170
+ const effectiveProgressPolicy = goalContract?.progressUpdatePolicy || analysis?.progress_update_policy || 'optional';
171
+ const effectiveCompletionConfidence = goalContract?.completionConfidenceRequired
172
+ || analysis?.completion_confidence_required
173
+ || 'medium';
174
+ const persistedGoalPrompt = buildGoalContractPrompt(goalContract);
175
+ return {
176
+ goalContract,
177
+ successCriteria,
178
+ effectiveGoal,
179
+ effectiveComplexity,
180
+ effectiveAutonomyLevel,
181
+ effectiveProgressPolicy,
182
+ effectiveCompletionConfidence,
183
+ persistedGoalPrompt,
184
+ };
185
+ }
186
+
187
+ function buildCompletionDecisionPrompt({
188
+ triggerSource,
189
+ messagingSent = false,
190
+ goalContext,
191
+ parallelWork = false,
192
+ tools,
193
+ toolExecutions,
194
+ lastReply,
195
+ iteration,
196
+ maxIterations,
197
+ }) {
198
+ const draftReply = normalizeOutgoingMessage(lastReply) || '';
199
+ const lines = [
200
+ 'Return JSON only.',
201
+ 'Decide whether this run should continue autonomously or stop now.',
202
+ 'Schema: {"status":"continue|complete|blocked","reason":"short concrete reason"}',
203
+ 'Rules:',
204
+ '- Use "continue" whenever any safe next step remains in this same run.',
205
+ '- Use "complete" only when the requested outcome is actually achieved and the latest draft is the finished user-facing answer.',
206
+ '- Use "blocked" only when a specific external dependency, missing user input, or permission outside this run is required and the latest draft is the blocker reply.',
207
+ '- If the latest draft asks the user for a missing required value, confirmation, or choice needed to proceed, use "blocked" so the run waits instead of repeating the same ask.',
208
+ '- A progress note, next-step note, apology, plan, or promise to investigate is "continue", not "complete".',
209
+ '- A single failed tool attempt is not blocked if another safe retry, verification step, or alternative path remains.',
210
+ '- A tool-specific API error, timeout, rate limit, or missing result inside this run is usually "continue", not "blocked", if any other available tool could still make progress.',
211
+ `- If completion_confidence_required is ${goalContext.effectiveCompletionConfidence} and the latest draft depends on unverified assumptions, use "continue" so the run can gather evidence, inspect state, or narrow the reply.`,
212
+ triggerSource === 'messaging' && messagingSent
213
+ ? '- A final reply was already delivered via send_message. Use "complete" unless concrete task work remains.'
214
+ : triggerSource === 'messaging'
215
+ ? '- For messaging, do not stop on a partial status message. Continue unless the task is actually complete or externally blocked.'
216
+ : '- Do not stop just because you wrote a status update. Continue unless the task is actually complete or externally blocked.',
217
+ ];
218
+
219
+ lines.push(
220
+ goalContext.effectiveGoal ? `Goal: ${goalContext.effectiveGoal}` : '',
221
+ goalContext.persistedGoalPrompt,
222
+ `Autonomy contract: complexity=${goalContext.effectiveComplexity}; autonomy_level=${goalContext.effectiveAutonomyLevel}; progress_update_policy=${goalContext.effectiveProgressPolicy}; parallel_work=${parallelWork === true}; completion_confidence_required=${goalContext.effectiveCompletionConfidence}.`,
223
+ goalContext.successCriteria.length > 0
224
+ ? `Success criteria:\n${goalContext.successCriteria.map((item, index) => `${index + 1}. ${item}`).join('\n')}`
225
+ : '',
226
+ `Current iteration: ${iteration} of ${maxIterations}.`,
227
+ `Available tools in this run: ${summarizeAvailableTools(tools) || 'none'}`,
228
+ `Recent tool evidence:\n${summarizeToolExecutions(toolExecutions, 8) || 'none'}`,
229
+ `Latest draft reply:\n${draftReply || '(empty)'}`,
230
+ );
231
+ return lines.filter(Boolean).join('\n');
232
+ }
233
+
234
+ function normalizeCompletionDecision(raw, fallbackStatus = 'continue') {
235
+ const allowed = new Set(['continue', 'complete', 'blocked']);
236
+ const requestedStatus = String(raw.status || '').trim().toLowerCase();
237
+ return {
238
+ status: allowed.has(requestedStatus) ? requestedStatus : fallbackStatus,
239
+ reason: String(raw.reason || '').trim().slice(0, 400),
240
+ };
241
+ }
242
+
243
+ module.exports = {
244
+ buildCompletionDecisionPrompt,
245
+ buildGoalContractPrompt,
246
+ goalContractFromAnalysis,
247
+ goalContractFromPlan,
248
+ mergeGoalContracts,
249
+ normalizeCompletionDecision,
250
+ normalizeGoalContract,
251
+ resolveRunGoalContext,
252
+ };