neoagent 2.3.1-beta.62 → 2.3.1-beta.64

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 (45) hide show
  1. package/.env.example +9 -0
  2. package/docs/configuration.md +11 -0
  3. package/flutter_app/lib/main.dart +2 -0
  4. package/flutter_app/lib/main_account_settings.dart +50 -22
  5. package/flutter_app/lib/main_admin.dart +24 -10
  6. package/flutter_app/lib/main_app_shell.dart +10 -9
  7. package/flutter_app/lib/main_chat.dart +433 -309
  8. package/flutter_app/lib/main_controller.dart +164 -6
  9. package/flutter_app/lib/main_integrations.dart +15 -7
  10. package/flutter_app/lib/main_models.dart +116 -7
  11. package/flutter_app/lib/main_navigation.dart +27 -18
  12. package/flutter_app/lib/main_operations.dart +162 -91
  13. package/flutter_app/lib/main_runtime.dart +22 -0
  14. package/flutter_app/lib/main_settings.dart +287 -75
  15. package/flutter_app/lib/main_shared.dart +165 -6
  16. package/flutter_app/lib/main_unified.dart +388 -0
  17. package/flutter_app/lib/src/analytics_service.dart +294 -0
  18. package/flutter_app/lib/src/backend_client.dart +4 -0
  19. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
  20. package/flutter_app/pubspec.lock +8 -0
  21. package/flutter_app/pubspec.yaml +1 -0
  22. package/flutter_app/web/index.html +1 -0
  23. package/package.json +1 -1
  24. package/server/config/analytics.js +30 -0
  25. package/server/db/database.js +52 -0
  26. package/server/http/routes.js +1 -0
  27. package/server/public/.last_build_id +1 -1
  28. package/server/public/assets/AssetManifest.bin +1 -1
  29. package/server/public/assets/AssetManifest.bin.json +1 -1
  30. package/server/public/assets/NOTICES +183 -0
  31. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  32. package/server/public/assets/packages/mixpanel_flutter/assets/mixpanel.js +3 -0
  33. package/server/public/flutter_bootstrap.js +1 -1
  34. package/server/public/index.html +1 -0
  35. package/server/public/main.dart.js +83685 -82046
  36. package/server/routes/agents.js +2 -1
  37. package/server/routes/memory.js +75 -3
  38. package/server/routes/runtime.js +14 -0
  39. package/server/routes/widgets.js +4 -4
  40. package/server/services/ai/engine.js +86 -0
  41. package/server/services/ai/runEvents.js +100 -0
  42. package/server/services/memory/manager.js +242 -26
  43. package/server/services/websocket.js +3 -1
  44. package/server/services/widgets/focus_widget.js +126 -0
  45. package/server/services/widgets/service.js +130 -2
@@ -4,6 +4,7 @@ const db = require('../db/database');
4
4
  const { requireAuth } = require('../middleware/auth');
5
5
  const { sanitizeError } = require('../utils/security');
6
6
  const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
7
+ const { listRunEvents } = require('../services/ai/runEvents');
7
8
  const { isInterimAssistantMetadata } = require('../services/ai/interim');
8
9
  const { buildAgentRunContext } = require('./_helpers/agentRunContext');
9
10
 
@@ -176,7 +177,7 @@ router.get('/:id/steps', (req, res) => {
176
177
  || run.final_response
177
178
  || null;
178
179
 
179
- res.json({ run, steps, response });
180
+ res.json({ run, steps, events: listRunEvents(run.id), response });
180
181
  });
181
182
 
182
183
  // Abort a run
@@ -33,6 +33,59 @@ function findOwnedMemoryIds(db, userId, agentId, ids) {
33
33
  ).all(userId, agentId, ...ids).map((row) => row.id);
34
34
  }
35
35
 
36
+ function parsePlainObject(input, fieldName) {
37
+ if (input == null) return null;
38
+ if (typeof input === 'string') {
39
+ try {
40
+ input = JSON.parse(input);
41
+ } catch {
42
+ throw new Error(`${fieldName} must be valid JSON.`);
43
+ }
44
+ }
45
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
46
+ throw new Error(`${fieldName} must be an object.`);
47
+ }
48
+ return input;
49
+ }
50
+
51
+ function normalizeOptionalStringField(value, fieldName, maxLength, pattern = null) {
52
+ if (value == null || value === '') return null;
53
+ if (typeof value !== 'string') {
54
+ throw new Error(`${fieldName} must be a string.`);
55
+ }
56
+ const normalized = value.trim().slice(0, maxLength);
57
+ if (!normalized) return null;
58
+ if (pattern && !pattern.test(normalized)) {
59
+ throw new Error(`${fieldName} has an invalid format.`);
60
+ }
61
+ return normalized;
62
+ }
63
+
64
+ function normalizeSourceRef(input) {
65
+ const raw = parsePlainObject(input, 'sourceRef');
66
+ if (!raw) return undefined;
67
+ return {
68
+ sourceType: normalizeOptionalStringField(raw.sourceType ?? raw.type, 'sourceRef.sourceType', 48, /^[a-z0-9_:-]+$/i),
69
+ sourceId: normalizeOptionalStringField(raw.sourceId ?? raw.id, 'sourceRef.sourceId', 128),
70
+ sourceLabel: normalizeOptionalStringField(raw.sourceLabel ?? raw.label, 'sourceRef.sourceLabel', 160),
71
+ };
72
+ }
73
+
74
+ function normalizeScope(input) {
75
+ const raw = parsePlainObject(input, 'scope');
76
+ if (!raw) return undefined;
77
+ const scopeType = normalizeOptionalStringField(raw.scopeType ?? raw.type, 'scope.scopeType', 32, /^(agent|conversation|task|channel|shared)$/i);
78
+ return {
79
+ scopeType: scopeType ? scopeType.toLowerCase() : null,
80
+ scopeId: normalizeOptionalStringField(raw.scopeId ?? raw.id, 'scope.scopeId', 128),
81
+ };
82
+ }
83
+
84
+ function normalizeMetadata(input) {
85
+ const raw = parsePlainObject(input, 'metadata');
86
+ return raw == null ? undefined : raw;
87
+ }
88
+
36
89
  // ─────────────────────────────────────────────────────────────────────────────
37
90
  // Overview (for initial page load)
38
91
  // ─────────────────────────────────────────────────────────────────────────────
@@ -46,6 +99,7 @@ router.get('/', (req, res) => {
46
99
  res.json({
47
100
  agentId,
48
101
  assistantBehaviorNotes: mm.getAssistantBehaviorNotes(userId, { agentId }),
102
+ assistantSelfState: mm.getAssistantSelfState(userId, { agentId }),
49
103
  dailyLogs: mm.listDailyLogs(7, userId),
50
104
  apiKeys: Object.keys(mm.readApiKeys(userId)),
51
105
  coreMemory
@@ -81,13 +135,31 @@ router.post('/memories', async (req, res) => {
81
135
  const mm = req.app.locals.memoryManager;
82
136
  const userId = req.session.userId;
83
137
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
84
- const { content, category = 'episodic', importance = 5 } = req.body;
138
+ const { content, category = 'episodic', importance = 5, sourceRef, scope, staleAfterDays, metadata } = req.body;
85
139
  if (!content || !content.trim()) return res.status(400).json({ error: 'content is required' });
86
140
  try {
87
- const id = await mm.saveMemory(userId, content, category, importance, { agentId });
141
+ let normalizedStaleAfterDays;
142
+ if (staleAfterDays != null && staleAfterDays !== '') {
143
+ normalizedStaleAfterDays = Number.parseInt(staleAfterDays, 10);
144
+ if (!Number.isInteger(normalizedStaleAfterDays) || normalizedStaleAfterDays <= 0) {
145
+ return res.status(400).json({ error: 'staleAfterDays must be a positive integer.' });
146
+ }
147
+ }
148
+
149
+ const id = await mm.saveMemory(userId, content, category, importance, {
150
+ agentId,
151
+ sourceRef: normalizeSourceRef(sourceRef),
152
+ scope: normalizeScope(scope),
153
+ staleAfterDays: normalizedStaleAfterDays,
154
+ metadata: normalizeMetadata(metadata),
155
+ });
88
156
  res.json({ success: true, id });
89
157
  } catch (err) {
90
- res.status(500).json({ error: sanitizeError(err) });
158
+ const message = sanitizeError(err);
159
+ if (/must be valid JSON|must be an object|must be a string|has an invalid format/i.test(message)) {
160
+ return res.status(400).json({ error: message });
161
+ }
162
+ res.status(500).json({ error: message });
91
163
  }
92
164
  });
93
165
 
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const { getAnalyticsConfig } = require('../config/analytics');
5
+
6
+ const router = express.Router();
7
+
8
+ router.get('/config', (req, res) => {
9
+ res.json({
10
+ analytics: getAnalyticsConfig(),
11
+ });
12
+ });
13
+
14
+ module.exports = router;
@@ -78,7 +78,7 @@ router.delete('/:id', (req, res) => {
78
78
  }
79
79
  });
80
80
 
81
- router.post('/:id/refresh', (req, res) => {
81
+ router.post('/:id/refresh', async (req, res) => {
82
82
  try {
83
83
  const service = widgetService(req);
84
84
  const taskRuntime = req.app?.locals?.taskRuntime;
@@ -89,10 +89,10 @@ router.post('/:id/refresh', (req, res) => {
89
89
  if (!widget) {
90
90
  return res.status(404).json({ error: 'Widget not found.' });
91
91
  }
92
- if (!widget.scheduledTaskId) {
93
- return res.status(400).json({ error: 'Widget is missing its refresh task.' });
92
+ if (widget.isSystem || !widget.scheduledTaskId) {
93
+ return res.json(await service.refreshWidget(req.session.userId, req.params.id));
94
94
  }
95
- res.json(taskRuntime.runTaskNow(widget.scheduledTaskId, req.session.userId));
95
+ res.json(await taskRuntime.runTaskNow(widget.scheduledTaskId, req.session.userId));
96
96
  } catch (err) {
97
97
  res.status(400).json({ error: sanitizeError(err) });
98
98
  }
@@ -37,6 +37,7 @@ const {
37
37
  buildInterimSignature,
38
38
  normalizeInterimKind,
39
39
  } = require('./interim');
40
+ const { recordRunEvent } = require('./runEvents');
40
41
 
41
42
  const MAX_CONSECUTIVE_TOOL_FAILURES = 5;
42
43
  const WIDGET_REFRESH_MAX_ITERATIONS = 30;
@@ -675,6 +676,22 @@ class AgentEngine {
675
676
  .run(JSON.stringify(next), runId);
676
677
  }
677
678
 
679
+ recordRunEvent(userId, runId, eventType, payload = {}, options = {}) {
680
+ try {
681
+ return recordRunEvent({
682
+ runId,
683
+ userId,
684
+ agentId: options.agentId || null,
685
+ eventType,
686
+ requestId: options.requestId || null,
687
+ stepId: options.stepId || null,
688
+ payload,
689
+ });
690
+ } catch {
691
+ return null;
692
+ }
693
+ }
694
+
678
695
  async publishInterimUpdate({
679
696
  userId,
680
697
  runId,
@@ -1533,6 +1550,12 @@ class AgentEngine {
1533
1550
  toolPids: new Set(),
1534
1551
  });
1535
1552
  this.emit(userId, 'run:start', { runId, agentId, title: runTitle, model, triggerType, triggerSource });
1553
+ this.recordRunEvent(userId, runId, 'run_started', {
1554
+ title: runTitle,
1555
+ model,
1556
+ triggerType,
1557
+ triggerSource,
1558
+ }, { agentId });
1536
1559
  console.info(
1537
1560
  `[Run ${shortenRunId(runId)}] started trigger=${triggerSource} type=${triggerType} model=${model} title=${summarizeForLog(runTitle, 120)}`
1538
1561
  );
@@ -1593,6 +1616,11 @@ class AgentEngine {
1593
1616
  if (threadStateMessage) {
1594
1617
  messages.push({ role: 'system', content: threadStateMessage });
1595
1618
  }
1619
+ this.recordRunEvent(userId, runId, 'memory_injected', {
1620
+ hasRecallContext: Boolean(recallMsg),
1621
+ hasThreadState: Boolean(threadStateMessage),
1622
+ recallPreview: recallMsg ? String(recallMsg).slice(0, 240) : '',
1623
+ }, { agentId });
1596
1624
  messages.push(this.buildUserMessage(userMessage, options));
1597
1625
  messages = sanitizeConversationMessages(messages);
1598
1626
 
@@ -1746,6 +1774,10 @@ class AgentEngine {
1746
1774
  promptMetrics = this.mergePromptMetrics(promptMetrics, metrics, iteration, tools.length);
1747
1775
  this.persistPromptMetrics(runId, promptMetrics).catch(() => { });
1748
1776
  this.emit(userId, 'run:thinking', { runId, iteration });
1777
+ this.recordRunEvent(userId, runId, 'model_turn_started', {
1778
+ iteration,
1779
+ toolCount: tools.length,
1780
+ }, { agentId });
1749
1781
 
1750
1782
  let response;
1751
1783
  let responseModel = model;
@@ -1879,6 +1911,12 @@ class AgentEngine {
1879
1911
  }
1880
1912
  }
1881
1913
 
1914
+ this.recordRunEvent(userId, runId, 'model_turn_completed', {
1915
+ iteration,
1916
+ toolCallCount: response.toolCalls?.length || 0,
1917
+ contentPreview: String(lastContent || streamContent || '').slice(0, 240),
1918
+ }, { agentId });
1919
+
1882
1920
  const assistantMessage = { role: 'assistant', content: lastContent };
1883
1921
  if (response.toolCalls?.length) assistantMessage.tool_calls = response.toolCalls;
1884
1922
  if (response.providerContentBlocks?.length) assistantMessage.providerContentBlocks = response.providerContentBlocks;
@@ -1972,6 +2010,12 @@ class AgentEngine {
1972
2010
  runId, stepId, stepIndex, toolName, toolArgs,
1973
2011
  type: this.getStepType(toolName)
1974
2012
  });
2013
+ this.recordRunEvent(userId, runId, 'tool_started', {
2014
+ stepIndex,
2015
+ toolName,
2016
+ toolArgs,
2017
+ type: this.getStepType(toolName),
2018
+ }, { agentId, stepId });
1975
2019
  console.info(
1976
2020
  `[Run ${shortenRunId(runId)}] step=${stepIndex} start tool=${toolName} args=${summarizeForLog(toolArgs)}`
1977
2021
  );
@@ -2006,11 +2050,24 @@ class AgentEngine {
2006
2050
  .run(stepStatus, JSON.stringify(toolResult).slice(0, 20000), toolErrorMessage || null, screenshotPath, stepId);
2007
2051
  if (toolErrorMessage) {
2008
2052
  this.emit(userId, 'run:tool_end', { runId, stepId, toolName, error: toolErrorMessage, result: toolResult, screenshotPath, status: stepStatus });
2053
+ this.recordRunEvent(userId, runId, 'tool_failed', {
2054
+ toolName,
2055
+ status: stepStatus,
2056
+ error: toolErrorMessage,
2057
+ durationMs: Date.now() - stepStartedAt,
2058
+ resultPreview: summarizeForLog(toolResult),
2059
+ }, { agentId, stepId });
2009
2060
  console.warn(
2010
2061
  `[Run ${shortenRunId(runId)}] step=${stepIndex} failed tool=${toolName} durationMs=${Date.now() - stepStartedAt} error=${summarizeForLog(toolErrorMessage, 160)}`
2011
2062
  );
2012
2063
  } else {
2013
2064
  this.emit(userId, 'run:tool_end', { runId, stepId, toolName, result: toolResult, screenshotPath, status: stepStatus });
2065
+ this.recordRunEvent(userId, runId, 'tool_completed', {
2066
+ toolName,
2067
+ status: stepStatus,
2068
+ durationMs: Date.now() - stepStartedAt,
2069
+ resultPreview: summarizeForLog(toolResult),
2070
+ }, { agentId, stepId });
2014
2071
  console.info(
2015
2072
  `[Run ${shortenRunId(runId)}] step=${stepIndex} done tool=${toolName} status=${stepStatus} durationMs=${Date.now() - stepStartedAt} result=${summarizeForLog(toolResult)}`
2016
2073
  );
@@ -2023,6 +2080,12 @@ class AgentEngine {
2023
2080
  db.prepare('UPDATE agent_steps SET status = ?, error = ?, completed_at = datetime(\'now\') WHERE id = ?')
2024
2081
  .run('failed', err.message, stepId);
2025
2082
  this.emit(userId, 'run:tool_end', { runId, stepId, toolName, error: err.message, status: 'failed' });
2083
+ this.recordRunEvent(userId, runId, 'tool_failed', {
2084
+ toolName,
2085
+ status: 'failed',
2086
+ error: err.message,
2087
+ durationMs: Date.now() - stepStartedAt,
2088
+ }, { agentId, stepId });
2026
2089
  console.warn(
2027
2090
  `[Run ${shortenRunId(runId)}] step=${stepIndex} failed tool=${toolName} durationMs=${Date.now() - stepStartedAt} error=${summarizeForLog(err.message, 160)}`
2028
2091
  );
@@ -2134,6 +2197,11 @@ class AgentEngine {
2134
2197
  );
2135
2198
  this.activeRuns.delete(runId);
2136
2199
  this.emit(userId, 'run:stopped', { runId, triggerSource });
2200
+ this.recordRunEvent(userId, runId, 'run_stopped', {
2201
+ triggerSource,
2202
+ totalTokens,
2203
+ iterations: iteration,
2204
+ }, { agentId });
2137
2205
  return { runId, content: '', totalTokens, iterations: iteration, status: 'stopped' };
2138
2206
  }
2139
2207
 
@@ -2337,6 +2405,14 @@ class AgentEngine {
2337
2405
  executionMode: analysis?.mode || 'execute',
2338
2406
  verificationStatus: verification?.status || 'skipped',
2339
2407
  });
2408
+ this.recordRunEvent(userId, runId, 'run_completed', {
2409
+ contentPreview: String(finalResponseText || lastContent || '').slice(0, 240),
2410
+ totalTokens,
2411
+ iterations: iteration,
2412
+ triggerSource,
2413
+ executionMode: analysis?.mode || 'execute',
2414
+ verificationStatus: verification?.status || 'skipped',
2415
+ }, { agentId });
2340
2416
 
2341
2417
  return { runId, content: lastContent, totalTokens, iterations: iteration, status: 'completed' };
2342
2418
  } catch (err) {
@@ -2349,6 +2425,11 @@ class AgentEngine {
2349
2425
  this.cleanupSubagentsForRun(runId, { cancelRunning: true });
2350
2426
  this.activeRuns.delete(runId);
2351
2427
  this.emit(userId, 'run:stopped', { runId, triggerSource });
2428
+ this.recordRunEvent(userId, runId, 'run_stopped', {
2429
+ triggerSource,
2430
+ totalTokens,
2431
+ iterations: iteration,
2432
+ }, { agentId });
2352
2433
  return { runId, content: '', totalTokens, iterations: iteration, status: 'stopped' };
2353
2434
  }
2354
2435
 
@@ -2478,6 +2559,11 @@ class AgentEngine {
2478
2559
  this.cleanupSubagentsForRun(runId, { cancelRunning: true });
2479
2560
  this.activeRuns.delete(runId);
2480
2561
  this.emit(userId, 'run:error', { runId, error: err.message });
2562
+ this.recordRunEvent(userId, runId, 'run_failed', {
2563
+ error: err.message,
2564
+ totalTokens,
2565
+ iterations: iteration,
2566
+ }, { agentId });
2481
2567
 
2482
2568
  if (messagingFailureContent) {
2483
2569
  return {
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ const db = require('../../db/database');
4
+
5
+ function parseJsonObject(value, fallback = {}) {
6
+ if (!value) return { ...fallback };
7
+ if (typeof value === 'object' && !Array.isArray(value)) return { ...value };
8
+ try {
9
+ const parsed = JSON.parse(String(value));
10
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
11
+ ? parsed
12
+ : { ...fallback };
13
+ } catch {
14
+ return { ...fallback };
15
+ }
16
+ }
17
+
18
+ function recordRunEvent({
19
+ runId,
20
+ userId,
21
+ agentId = null,
22
+ eventType,
23
+ requestId = null,
24
+ stepId = null,
25
+ sequenceIndex = null,
26
+ payload = {},
27
+ }) {
28
+ if (!runId || !userId || !eventType) return null;
29
+ const payloadJson = JSON.stringify(
30
+ payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {},
31
+ );
32
+ const row = db.transaction(() => {
33
+ const resolvedSequence = Number.isInteger(sequenceIndex) && sequenceIndex > 0
34
+ ? sequenceIndex
35
+ : Number(
36
+ db.prepare(
37
+ 'SELECT COALESCE(MAX(sequence_index), 0) AS max_sequence FROM agent_run_events WHERE run_id = ?'
38
+ ).get(runId)?.max_sequence || 0,
39
+ ) + 1;
40
+ const result = db.prepare(
41
+ `INSERT INTO agent_run_events (
42
+ run_id, user_id, agent_id, event_type, request_id, step_id, sequence_index, payload_json
43
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
44
+ ).run(
45
+ runId,
46
+ userId,
47
+ agentId || null,
48
+ eventType,
49
+ requestId || null,
50
+ stepId || null,
51
+ resolvedSequence,
52
+ payloadJson,
53
+ );
54
+ return db.prepare(
55
+ `SELECT id, run_id, user_id, agent_id, event_type, request_id, step_id, sequence_index, payload_json, created_at
56
+ FROM agent_run_events
57
+ WHERE id = ?`
58
+ ).get(result.lastInsertRowid);
59
+ })();
60
+
61
+ return row ? {
62
+ id: Number(row.id),
63
+ runId: row.run_id,
64
+ userId: row.user_id,
65
+ agentId: row.agent_id || null,
66
+ eventType: row.event_type,
67
+ requestId: row.request_id || null,
68
+ stepId: row.step_id || null,
69
+ sequenceIndex: Number(row.sequence_index || 0),
70
+ payload: parseJsonObject(row.payload_json, {}),
71
+ createdAt: row.created_at,
72
+ } : null;
73
+ }
74
+
75
+ function listRunEvents(runId) {
76
+ if (!runId) return [];
77
+ const rows = db.prepare(
78
+ `SELECT id, run_id, user_id, agent_id, event_type, request_id, step_id, sequence_index, payload_json, created_at
79
+ FROM agent_run_events
80
+ WHERE run_id = ?
81
+ ORDER BY sequence_index ASC, id ASC`
82
+ ).all(runId);
83
+ return rows.map((row) => ({
84
+ id: Number(row.id),
85
+ runId: row.run_id,
86
+ userId: row.user_id,
87
+ agentId: row.agent_id || null,
88
+ eventType: row.event_type,
89
+ requestId: row.request_id || null,
90
+ stepId: row.step_id || null,
91
+ sequenceIndex: Number(row.sequence_index || 0),
92
+ payload: parseJsonObject(row.payload_json, {}),
93
+ createdAt: row.created_at,
94
+ }));
95
+ }
96
+
97
+ module.exports = {
98
+ recordRunEvent,
99
+ listRunEvents,
100
+ };