neoagent 2.4.3 → 2.4.4-beta.4
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/README.md +5 -3
- package/docs/capabilities.md +16 -7
- package/docs/index.md +1 -0
- package/docs/security-boundaries.md +122 -0
- package/docs/supermemory-memory-review.md +852 -0
- package/flutter_app/lib/features/memory/views/retrieval_inspector_view.dart +128 -0
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_app_shell.dart +22 -0
- package/flutter_app/lib/main_controller.dart +36 -1
- package/flutter_app/lib/main_operations.dart +13 -0
- package/flutter_app/lib/main_security.dart +971 -0
- package/flutter_app/lib/main_settings.dart +61 -0
- package/flutter_app/lib/src/backend_client.dart +60 -3
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
- package/flutter_app/pubspec.lock +32 -0
- package/flutter_app/pubspec.yaml +1 -0
- package/lib/schema_migrations.js +237 -0
- package/package.json +4 -2
- package/server/db/database.js +3 -0
- package/server/http/routes.js +2 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +86 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +80911 -79117
- package/server/routes/memory.js +39 -2
- package/server/routes/security.js +112 -0
- package/server/services/ai/engine.js +267 -10
- package/server/services/ai/systemPrompt.js +13 -2
- package/server/services/cli/shell_worker.js +135 -0
- package/server/services/cli/shell_worker_pool.js +125 -0
- package/server/services/manager.js +20 -1
- package/server/services/memory/consolidation.js +111 -0
- package/server/services/memory/embedding_index.js +175 -0
- package/server/services/memory/embeddings.js +22 -2
- package/server/services/memory/evaluation.js +187 -0
- package/server/services/memory/ingestion_chunking.js +191 -0
- package/server/services/memory/ingestion_documents.js +96 -26
- package/server/services/memory/intelligence.js +3 -1
- package/server/services/memory/manager.js +855 -40
- package/server/services/memory/policy.js +0 -40
- package/server/services/memory/retrieval_reasoning.js +191 -0
- package/server/services/runtime/manager.js +7 -0
- package/server/services/security/approval_gate_service.js +93 -0
- package/server/services/security/tool_categories.js +105 -0
- package/server/services/security/tool_policy_service.js +92 -0
- package/server/services/security/tool_security_hook.js +77 -0
package/server/routes/memory.js
CHANGED
|
@@ -28,6 +28,14 @@ const transferImportLimiter = rateLimit({
|
|
|
28
28
|
legacyHeaders: false,
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
const memorySearchLimiter = rateLimit({
|
|
32
|
+
windowMs: 1 * 60 * 1000,
|
|
33
|
+
max: 60,
|
|
34
|
+
message: { error: 'Too many memory search requests, try again later' },
|
|
35
|
+
standardHeaders: true,
|
|
36
|
+
legacyHeaders: false,
|
|
37
|
+
});
|
|
38
|
+
|
|
31
39
|
const MAX_TRANSFER_TEXT_BYTES = 60 * 1024;
|
|
32
40
|
|
|
33
41
|
function normalizeMemoryIds(value) {
|
|
@@ -378,20 +386,49 @@ router.post('/memories/bulk-archive', (req, res) => {
|
|
|
378
386
|
});
|
|
379
387
|
|
|
380
388
|
// Semantic recall (search)
|
|
381
|
-
router.post('/memories/recall', async (req, res) => {
|
|
389
|
+
router.post('/memories/recall', memorySearchLimiter, async (req, res) => {
|
|
382
390
|
const mm = req.app.locals.memoryManager;
|
|
383
391
|
const userId = req.session.userId;
|
|
384
392
|
const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
|
|
385
393
|
const { query, limit = 8 } = req.body;
|
|
386
394
|
if (!query) return res.status(400).json({ error: 'query is required' });
|
|
395
|
+
|
|
396
|
+
let parsedLimit = Number.parseInt(limit, 10);
|
|
397
|
+
if (!Number.isFinite(parsedLimit) || Number.isNaN(parsedLimit)) {
|
|
398
|
+
return res.status(400).json({ error: 'limit must be an integer between 1 and 100' });
|
|
399
|
+
}
|
|
400
|
+
parsedLimit = Math.max(1, Math.min(100, parsedLimit));
|
|
401
|
+
|
|
387
402
|
try {
|
|
388
|
-
const results = await mm.recallMemory(userId, query,
|
|
403
|
+
const results = await mm.recallMemory(userId, query, parsedLimit, { agentId });
|
|
389
404
|
res.json(results);
|
|
390
405
|
} catch (err) {
|
|
391
406
|
res.status(500).json({ error: sanitizeError(err) });
|
|
392
407
|
}
|
|
393
408
|
});
|
|
394
409
|
|
|
410
|
+
// Semantic recall inspection (UI debug)
|
|
411
|
+
router.post('/memories/inspect', memorySearchLimiter, async (req, res) => {
|
|
412
|
+
const mm = req.app.locals.memoryManager;
|
|
413
|
+
const userId = req.session.userId;
|
|
414
|
+
const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
|
|
415
|
+
const { query, limit = 20 } = req.body;
|
|
416
|
+
if (!query) return res.status(400).json({ error: 'query is required' });
|
|
417
|
+
|
|
418
|
+
let parsedLimit = Number.parseInt(limit, 10);
|
|
419
|
+
if (!Number.isFinite(parsedLimit) || Number.isNaN(parsedLimit)) {
|
|
420
|
+
return res.status(400).json({ error: 'limit must be an integer between 1 and 100' });
|
|
421
|
+
}
|
|
422
|
+
parsedLimit = Math.max(1, Math.min(100, parsedLimit));
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const results = await mm.recallMemory(userId, query, parsedLimit, { agentId });
|
|
426
|
+
res.json({ results });
|
|
427
|
+
} catch (err) {
|
|
428
|
+
res.status(500).json({ error: sanitizeError(err) });
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
395
432
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
396
433
|
// Core Memory
|
|
397
434
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const { requireAuth } = require('../middleware/auth');
|
|
6
|
+
const db = require('../db/database');
|
|
7
|
+
const { TOOL_CATEGORIES, getCategoryForTool } = require('../services/security/tool_categories');
|
|
8
|
+
|
|
9
|
+
router.use(requireAuth);
|
|
10
|
+
|
|
11
|
+
// ── Security mode ─────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
// GET /api/security/mode — current global security mode
|
|
14
|
+
router.get('/mode', (req, res) => {
|
|
15
|
+
const mode = req.app.locals.toolPolicyService.getSecurityMode(req.session.userId);
|
|
16
|
+
res.json({ mode });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// PUT /api/security/mode — update global security mode
|
|
20
|
+
router.put('/mode', (req, res) => {
|
|
21
|
+
const { mode } = req.body;
|
|
22
|
+
if (!mode) return res.status(400).json({ error: 'mode is required' });
|
|
23
|
+
try {
|
|
24
|
+
req.app.locals.toolPolicyService.setSecurityMode(req.session.userId, mode);
|
|
25
|
+
res.json({ ok: true, mode });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
res.status(400).json({ error: err.message });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ── Per-category policies ─────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
// GET /api/security/policies — all category policies + current mode
|
|
34
|
+
router.get('/policies', (req, res) => {
|
|
35
|
+
const { toolPolicyService } = req.app.locals;
|
|
36
|
+
const policies = toolPolicyService.getPolicies(req.session.userId);
|
|
37
|
+
const mode = toolPolicyService.getSecurityMode(req.session.userId);
|
|
38
|
+
res.json({ policies, mode, categories: Object.keys(TOOL_CATEGORIES) });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// PUT /api/security/policies — update a single category policy
|
|
42
|
+
router.put('/policies', (req, res) => {
|
|
43
|
+
const { category, policy } = req.body;
|
|
44
|
+
if (!category || !policy) {
|
|
45
|
+
return res.status(400).json({ error: 'category and policy are required' });
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
req.app.locals.toolPolicyService.setPolicy(req.session.userId, category, policy);
|
|
49
|
+
res.json({ ok: true, category, policy });
|
|
50
|
+
} catch (err) {
|
|
51
|
+
res.status(400).json({ error: err.message });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ── Approval decisions ────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
// POST /api/security/approvals/:approvalId — resolve a pending approval
|
|
58
|
+
router.post('/approvals/:approvalId', (req, res) => {
|
|
59
|
+
const { approvalId } = req.params;
|
|
60
|
+
const { decision, scope, runId, toolName, toolArgs } = req.body;
|
|
61
|
+
|
|
62
|
+
if (!decision || !['approved', 'denied'].includes(decision)) {
|
|
63
|
+
return res.status(400).json({ error: 'decision must be approved or denied' });
|
|
64
|
+
}
|
|
65
|
+
const normalizedScope = ['once', 'session', 'always'].includes(scope) ? scope : 'once';
|
|
66
|
+
|
|
67
|
+
// 'always' scope: also persist the policy so this category is allowed going forward
|
|
68
|
+
if (decision === 'approved' && normalizedScope === 'always' && toolName) {
|
|
69
|
+
try {
|
|
70
|
+
const category = getCategoryForTool(toolName, toolArgs ?? {});
|
|
71
|
+
if (category) {
|
|
72
|
+
req.app.locals.toolPolicyService.setPolicy(req.session.userId, category, 'allow');
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { approvalGateService } = req.app.locals;
|
|
78
|
+
const resolved = approvalGateService.resolve(
|
|
79
|
+
approvalId,
|
|
80
|
+
req.session.userId,
|
|
81
|
+
runId || null,
|
|
82
|
+
toolName || null,
|
|
83
|
+
toolArgs || {},
|
|
84
|
+
decision,
|
|
85
|
+
normalizedScope,
|
|
86
|
+
);
|
|
87
|
+
if (!resolved) {
|
|
88
|
+
return res.status(404).json({ error: 'Approval not found or already resolved' });
|
|
89
|
+
}
|
|
90
|
+
res.json({ ok: true, approvalId, decision, scope: normalizedScope });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── Audit log ─────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
// GET /api/security/approval-log — paginated audit log
|
|
96
|
+
router.get('/approval-log', (req, res) => {
|
|
97
|
+
const limit = Math.min(Number(req.query.limit) || 50, 200);
|
|
98
|
+
const offset = Number(req.query.offset) || 0;
|
|
99
|
+
const rows = db.prepare(`
|
|
100
|
+
SELECT id, run_id, tool_name, tool_args_json, decision, scope, decided_at
|
|
101
|
+
FROM tool_approval_log
|
|
102
|
+
WHERE user_id = ?
|
|
103
|
+
ORDER BY decided_at DESC
|
|
104
|
+
LIMIT ? OFFSET ?
|
|
105
|
+
`).all(req.session.userId, limit, offset);
|
|
106
|
+
const total = db.prepare(
|
|
107
|
+
'SELECT COUNT(*) as count FROM tool_approval_log WHERE user_id = ?'
|
|
108
|
+
).get(req.session.userId)?.count ?? 0;
|
|
109
|
+
res.json({ log: rows, total, limit, offset });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
module.exports = router;
|
|
@@ -76,6 +76,18 @@ const {
|
|
|
76
76
|
inferToolFailureMessage,
|
|
77
77
|
buildAutonomousRecoveryContext,
|
|
78
78
|
} = require('./toolEvidence');
|
|
79
|
+
const {
|
|
80
|
+
buildMemoryConsolidationInstructions,
|
|
81
|
+
normalizeMemoryCandidates,
|
|
82
|
+
} = require('../memory/consolidation');
|
|
83
|
+
const {
|
|
84
|
+
buildPlannerPrompt,
|
|
85
|
+
buildRerankerPrompt,
|
|
86
|
+
mergeRetrievalResults,
|
|
87
|
+
normalizeRerankResult,
|
|
88
|
+
normalizeRetrievalPlan,
|
|
89
|
+
shouldEnhanceRetrieval,
|
|
90
|
+
} = require('../memory/retrieval_reasoning');
|
|
79
91
|
|
|
80
92
|
function generateTitle(task) {
|
|
81
93
|
if (!task || typeof task !== 'string') return 'Untitled';
|
|
@@ -341,6 +353,198 @@ class AgentEngine {
|
|
|
341
353
|
};
|
|
342
354
|
}
|
|
343
355
|
|
|
356
|
+
async buildMemoryRecall({
|
|
357
|
+
memoryManager,
|
|
358
|
+
userId,
|
|
359
|
+
agentId,
|
|
360
|
+
query,
|
|
361
|
+
provider,
|
|
362
|
+
providerName,
|
|
363
|
+
model,
|
|
364
|
+
runId,
|
|
365
|
+
stepId = null,
|
|
366
|
+
options = {},
|
|
367
|
+
returnDetails = false,
|
|
368
|
+
}) {
|
|
369
|
+
const initial = await memoryManager.recallMemory(userId, query, 12, { agentId });
|
|
370
|
+
|
|
371
|
+
const pendingChunks = memoryManager.getPendingExtractionChunks?.(userId, agentId, 5) || [];
|
|
372
|
+
if (pendingChunks.length) {
|
|
373
|
+
this.extractPendingChunks(pendingChunks, {
|
|
374
|
+
userId,
|
|
375
|
+
agentId,
|
|
376
|
+
provider,
|
|
377
|
+
providerName,
|
|
378
|
+
model,
|
|
379
|
+
memoryManager,
|
|
380
|
+
}).catch((err) => console.warn('[Memory] Background chunk extraction failed:', err.message));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const decision = shouldEnhanceRetrieval(initial);
|
|
384
|
+
if (!decision.enhance) {
|
|
385
|
+
const message = await memoryManager.buildRecallMessage(userId, query, {
|
|
386
|
+
agentId,
|
|
387
|
+
recalled: initial.slice(0, 5),
|
|
388
|
+
});
|
|
389
|
+
return returnDetails
|
|
390
|
+
? { message, results: initial.slice(0, 12), enhanced: false, reason: decision.reason }
|
|
391
|
+
: message;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const stats = memoryManager.getMemoryStats?.(userId, { agentId })
|
|
395
|
+
|| { total: initial.length };
|
|
396
|
+
if (!Number(stats.total || 0)) {
|
|
397
|
+
return returnDetails
|
|
398
|
+
? { message: null, results: [], enhanced: false, reason: 'empty_memory' }
|
|
399
|
+
: null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const startedAt = Date.now();
|
|
403
|
+
let plan = null;
|
|
404
|
+
let merged = initial;
|
|
405
|
+
let reranked = initial;
|
|
406
|
+
try {
|
|
407
|
+
const planned = await this.requestStructuredJson({
|
|
408
|
+
provider,
|
|
409
|
+
providerName,
|
|
410
|
+
model,
|
|
411
|
+
messages: [],
|
|
412
|
+
prompt: buildPlannerPrompt(query, initial, new Date().toISOString()),
|
|
413
|
+
maxTokens: 650,
|
|
414
|
+
normalize: (raw) => normalizeRetrievalPlan(raw, query),
|
|
415
|
+
fallback: normalizeRetrievalPlan({}, query),
|
|
416
|
+
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
417
|
+
telemetry: { runId, stepId, userId, agentId },
|
|
418
|
+
phase: 'memory_retrieval_plan',
|
|
419
|
+
});
|
|
420
|
+
plan = planned.value;
|
|
421
|
+
const resultSets = [initial];
|
|
422
|
+
for (const variant of plan.queryVariants) {
|
|
423
|
+
if (variant === query && initial.length) continue;
|
|
424
|
+
resultSets.push(await memoryManager.recallMemory(userId, variant, 20, {
|
|
425
|
+
agentId,
|
|
426
|
+
validAt: plan.validAt,
|
|
427
|
+
includeHistory: plan.temporalMode === 'historical',
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
merged = mergeRetrievalResults(resultSets, 30);
|
|
431
|
+
if (merged.length > 1) {
|
|
432
|
+
const rerankResponse = await this.requestStructuredJson({
|
|
433
|
+
provider,
|
|
434
|
+
providerName,
|
|
435
|
+
model,
|
|
436
|
+
messages: [],
|
|
437
|
+
prompt: buildRerankerPrompt(query, plan, merged.slice(0, 24)),
|
|
438
|
+
maxTokens: 1200,
|
|
439
|
+
normalize: (raw) => normalizeRerankResult(raw, merged),
|
|
440
|
+
fallback: merged,
|
|
441
|
+
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
442
|
+
telemetry: { runId, stepId, userId, agentId },
|
|
443
|
+
phase: 'memory_retrieval_rerank',
|
|
444
|
+
});
|
|
445
|
+
reranked = rerankResponse.value;
|
|
446
|
+
} else {
|
|
447
|
+
reranked = merged;
|
|
448
|
+
}
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.warn('[Memory] Retrieval enhancement failed:', error.message);
|
|
451
|
+
plan = null;
|
|
452
|
+
merged = initial;
|
|
453
|
+
reranked = initial;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
memoryManager.recordRetrievalEnhancement?.(userId, {
|
|
457
|
+
query,
|
|
458
|
+
reason: decision.reason,
|
|
459
|
+
plan,
|
|
460
|
+
initialCount: initial.length,
|
|
461
|
+
mergedCount: merged.length,
|
|
462
|
+
resultIds: reranked.slice(0, 5).map((result) => result.id),
|
|
463
|
+
latencyMs: Date.now() - startedAt,
|
|
464
|
+
}, { agentId, runId });
|
|
465
|
+
|
|
466
|
+
const message = await memoryManager.buildRecallMessage(userId, query, {
|
|
467
|
+
agentId,
|
|
468
|
+
recalled: reranked.slice(0, 5),
|
|
469
|
+
});
|
|
470
|
+
return returnDetails
|
|
471
|
+
? {
|
|
472
|
+
message,
|
|
473
|
+
results: reranked.slice(0, 12),
|
|
474
|
+
enhanced: plan !== null,
|
|
475
|
+
reason: decision.reason,
|
|
476
|
+
plan,
|
|
477
|
+
}
|
|
478
|
+
: message;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async extractPendingChunks(chunks, {
|
|
482
|
+
userId,
|
|
483
|
+
agentId,
|
|
484
|
+
provider,
|
|
485
|
+
providerName,
|
|
486
|
+
model,
|
|
487
|
+
memoryManager,
|
|
488
|
+
}) {
|
|
489
|
+
const ids = chunks.map((c) => c.id);
|
|
490
|
+
memoryManager.markChunksExtracted?.(ids, { success: true });
|
|
491
|
+
|
|
492
|
+
const consolidationSchema = JSON.stringify({
|
|
493
|
+
memory_candidates: [{
|
|
494
|
+
memory: 'Concise standalone fact.',
|
|
495
|
+
subject: 'Canonical entity or person.',
|
|
496
|
+
predicate: 'Normalized relationship or attribute.',
|
|
497
|
+
object: 'Current atomic value.',
|
|
498
|
+
relation: 'new | updates | extends | derives',
|
|
499
|
+
category: 'identity | preferences | projects | contacts | events | tasks | episodic | assistant_self',
|
|
500
|
+
confidence: 0.8,
|
|
501
|
+
importance: 5,
|
|
502
|
+
is_static: false,
|
|
503
|
+
valid_from: null,
|
|
504
|
+
valid_to: null,
|
|
505
|
+
forget_after: null,
|
|
506
|
+
evidence: 'Short source-grounded quote.',
|
|
507
|
+
}],
|
|
508
|
+
}, null, 2);
|
|
509
|
+
|
|
510
|
+
for (const chunk of chunks) {
|
|
511
|
+
try {
|
|
512
|
+
const result = await this.requestStructuredJson({
|
|
513
|
+
provider,
|
|
514
|
+
providerName,
|
|
515
|
+
model,
|
|
516
|
+
messages: [],
|
|
517
|
+
prompt: [
|
|
518
|
+
'Return JSON only. Extract durable memory facts from the document chunk below.',
|
|
519
|
+
buildMemoryConsolidationInstructions(new Date().toISOString()),
|
|
520
|
+
`Source type: ${chunk.sourceType || 'document'}`,
|
|
521
|
+
chunk.title ? `Document title: ${chunk.title}` : '',
|
|
522
|
+
`Content:\n${String(chunk.content || '').slice(0, 2400)}`,
|
|
523
|
+
`Schema:\n${consolidationSchema}`,
|
|
524
|
+
].filter(Boolean).join('\n\n'),
|
|
525
|
+
maxTokens: 800,
|
|
526
|
+
normalize: (raw) => normalizeMemoryCandidates(raw?.memory_candidates || []),
|
|
527
|
+
fallback: [],
|
|
528
|
+
phase: 'document_extraction',
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const candidates = Array.isArray(result.value) ? result.value : [];
|
|
532
|
+
if (candidates.length) {
|
|
533
|
+
await memoryManager.consolidateMemoryCandidates(userId, candidates, {
|
|
534
|
+
agentId,
|
|
535
|
+
metadata: {
|
|
536
|
+
trustLevel: 'external_source',
|
|
537
|
+
sourceChunkMemoryId: chunk.id,
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
} catch (err) {
|
|
542
|
+
memoryManager.markChunksExtracted?.([chunk.id], { success: false });
|
|
543
|
+
console.warn('[Memory] Document chunk extraction failed:', err.message);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
344
548
|
persistRunMetadata(runId, patch = {}) {
|
|
345
549
|
if (!runId || !patch || typeof patch !== 'object') return;
|
|
346
550
|
const existing = db.prepare('SELECT metadata_json FROM agent_runs WHERE id = ?').get(runId);
|
|
@@ -888,6 +1092,7 @@ class AgentEngine {
|
|
|
888
1092
|
|
|
889
1093
|
async refreshConversationState({
|
|
890
1094
|
conversationId,
|
|
1095
|
+
runId,
|
|
891
1096
|
provider,
|
|
892
1097
|
providerName,
|
|
893
1098
|
model,
|
|
@@ -905,7 +1110,34 @@ class AgentEngine {
|
|
|
905
1110
|
const promptMessages = [
|
|
906
1111
|
{
|
|
907
1112
|
role: 'system',
|
|
908
|
-
content:
|
|
1113
|
+
content: [
|
|
1114
|
+
'Return JSON only. Distill the current thread working state. Keep it concise and concrete.',
|
|
1115
|
+
'Track summary, open_commitments, unresolved_questions, referenced_entities, and last_verified_facts. Do not invent facts.',
|
|
1116
|
+
buildMemoryConsolidationInstructions(new Date().toISOString()),
|
|
1117
|
+
'Schema:',
|
|
1118
|
+
JSON.stringify({
|
|
1119
|
+
summary: '',
|
|
1120
|
+
open_commitments: [],
|
|
1121
|
+
unresolved_questions: [],
|
|
1122
|
+
referenced_entities: [],
|
|
1123
|
+
last_verified_facts: [],
|
|
1124
|
+
memory_candidates: [{
|
|
1125
|
+
memory: 'Concise standalone fact for future context.',
|
|
1126
|
+
subject: 'Canonical entity or person.',
|
|
1127
|
+
predicate: 'Normalized relationship or attribute.',
|
|
1128
|
+
object: 'Current atomic value.',
|
|
1129
|
+
relation: 'new | updates | extends | derives',
|
|
1130
|
+
category: 'identity | preferences | projects | contacts | events | tasks | episodic | assistant_self',
|
|
1131
|
+
confidence: 0.9,
|
|
1132
|
+
importance: 7,
|
|
1133
|
+
is_static: false,
|
|
1134
|
+
valid_from: null,
|
|
1135
|
+
valid_to: null,
|
|
1136
|
+
forget_after: null,
|
|
1137
|
+
evidence: 'Short source-grounded evidence.',
|
|
1138
|
+
}],
|
|
1139
|
+
}, null, 2),
|
|
1140
|
+
].join('\n\n')
|
|
909
1141
|
},
|
|
910
1142
|
{
|
|
911
1143
|
role: 'user',
|
|
@@ -943,6 +1175,20 @@ class AgentEngine {
|
|
|
943
1175
|
}
|
|
944
1176
|
|
|
945
1177
|
memoryManager.updateConversationState(conversationId, nextState);
|
|
1178
|
+
const memoryCandidates = normalizeMemoryCandidates(parsed.memory_candidates);
|
|
1179
|
+
if (memoryCandidates.length) {
|
|
1180
|
+
await memoryManager.consolidateMemoryCandidates(
|
|
1181
|
+
options.userId,
|
|
1182
|
+
memoryCandidates,
|
|
1183
|
+
{
|
|
1184
|
+
agentId: options.agentId || null,
|
|
1185
|
+
conversationId,
|
|
1186
|
+
runId,
|
|
1187
|
+
},
|
|
1188
|
+
);
|
|
1189
|
+
const { invalidateSystemPromptCache } = require('./systemPrompt');
|
|
1190
|
+
invalidateSystemPromptCache(options.userId, options.agentId || null);
|
|
1191
|
+
}
|
|
946
1192
|
return nextState;
|
|
947
1193
|
}
|
|
948
1194
|
|
|
@@ -1107,7 +1353,7 @@ class AgentEngine {
|
|
|
1107
1353
|
toolArgs,
|
|
1108
1354
|
stepIndex: nextStepIndex,
|
|
1109
1355
|
blocked: true,
|
|
1110
|
-
result: { status: '
|
|
1356
|
+
result: { status: 'blocked', reason: hookResult.reason || 'Blocked by policy.', blocked_by: hookResult.blocked_by || 'policy' },
|
|
1111
1357
|
});
|
|
1112
1358
|
continue;
|
|
1113
1359
|
}
|
|
@@ -1726,7 +1972,17 @@ class AgentEngine {
|
|
|
1726
1972
|
const recallQuery = options.context?.rawUserMessage || userMessage;
|
|
1727
1973
|
const recallMsg = options.skipGlobalRecall === true
|
|
1728
1974
|
? null
|
|
1729
|
-
: await
|
|
1975
|
+
: await this.buildMemoryRecall({
|
|
1976
|
+
memoryManager,
|
|
1977
|
+
userId,
|
|
1978
|
+
agentId,
|
|
1979
|
+
query: recallQuery,
|
|
1980
|
+
provider,
|
|
1981
|
+
providerName,
|
|
1982
|
+
model,
|
|
1983
|
+
runId,
|
|
1984
|
+
options,
|
|
1985
|
+
});
|
|
1730
1986
|
|
|
1731
1987
|
let summaryMessage = null;
|
|
1732
1988
|
let historyMessages = [];
|
|
@@ -2428,13 +2684,14 @@ class AgentEngine {
|
|
|
2428
2684
|
const hookCtx = { toolName, toolArgs, runId, userId, agentId, iteration };
|
|
2429
2685
|
const hookResult = await globalHooks.run('before_tool_call', hookCtx);
|
|
2430
2686
|
if (hookResult.block) {
|
|
2431
|
-
|
|
2432
|
-
|
|
2687
|
+
const blockReason = hookResult.reason || 'Blocked by policy.';
|
|
2688
|
+
const blockedBy = hookResult.blocked_by || 'policy';
|
|
2689
|
+
console.warn(`[Run ${shortenRunId(runId)}] before_tool_call hook blocked tool=${toolName} reason="${blockReason}"`);
|
|
2433
2690
|
messages.push({
|
|
2434
2691
|
role: 'tool',
|
|
2435
2692
|
name: toolName,
|
|
2436
2693
|
tool_call_id: toolCall.id,
|
|
2437
|
-
content: JSON.stringify({ tool: toolName, status: '
|
|
2694
|
+
content: JSON.stringify({ tool: toolName, status: 'blocked', reason: blockReason, blocked_by: blockedBy }),
|
|
2438
2695
|
});
|
|
2439
2696
|
continue;
|
|
2440
2697
|
}
|
|
@@ -2857,8 +3114,9 @@ class AgentEngine {
|
|
|
2857
3114
|
refreshConversationSummary(conversationId, provider, model, historyWindow).catch((err) => {
|
|
2858
3115
|
console.error('[AI] Conversation summary refresh failed:', err.message);
|
|
2859
3116
|
});
|
|
2860
|
-
this.refreshConversationState({
|
|
3117
|
+
await this.refreshConversationState({
|
|
2861
3118
|
conversationId,
|
|
3119
|
+
runId,
|
|
2862
3120
|
provider,
|
|
2863
3121
|
providerName,
|
|
2864
3122
|
model,
|
|
@@ -2866,7 +3124,7 @@ class AgentEngine {
|
|
|
2866
3124
|
analysis,
|
|
2867
3125
|
verification,
|
|
2868
3126
|
historyWindow,
|
|
2869
|
-
options,
|
|
3127
|
+
options: { ...options, userId, agentId },
|
|
2870
3128
|
}).catch((err) => {
|
|
2871
3129
|
console.error('[AI] Conversation working state refresh failed:', err.message);
|
|
2872
3130
|
});
|
|
@@ -3166,9 +3424,8 @@ class AgentEngine {
|
|
|
3166
3424
|
let relevantMemories = [];
|
|
3167
3425
|
try {
|
|
3168
3426
|
relevantMemories = this.memoryManager
|
|
3169
|
-
? await this.memoryManager.recallMemory(userId, task, {
|
|
3427
|
+
? await this.memoryManager.recallMemory(userId, task, 4, {
|
|
3170
3428
|
agentId: options.agentId || null,
|
|
3171
|
-
limit: 4,
|
|
3172
3429
|
})
|
|
3173
3430
|
: [];
|
|
3174
3431
|
} catch {}
|
|
@@ -3,6 +3,13 @@ const os = require('os');
|
|
|
3
3
|
const PROMPT_CACHE_TTL = 30_000;
|
|
4
4
|
const promptCache = new Map();
|
|
5
5
|
|
|
6
|
+
function invalidateSystemPromptCache(userId, agentId = null) {
|
|
7
|
+
const prefix = `${String(userId || 'global')}:${String(agentId || 'main')}:`;
|
|
8
|
+
for (const key of promptCache.keys()) {
|
|
9
|
+
if (key.startsWith(prefix)) promptCache.delete(key);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
function clampSection(text, maxChars) {
|
|
7
14
|
const str = String(text || '').trim();
|
|
8
15
|
if (!str) return '';
|
|
@@ -284,7 +291,7 @@ async function buildSystemPromptSections(userId, context = {}, memoryManager) {
|
|
|
284
291
|
}
|
|
285
292
|
|
|
286
293
|
const memCtx = await memoryManager.buildContext(userId, { agentId });
|
|
287
|
-
const compactMemory = clampSection(memCtx,
|
|
294
|
+
const compactMemory = clampSection(memCtx, 1600);
|
|
288
295
|
if (compactMemory) {
|
|
289
296
|
dynamic.push(compactMemory);
|
|
290
297
|
}
|
|
@@ -344,4 +351,8 @@ async function buildSystemPrompt(userId, context = {}, memoryManager) {
|
|
|
344
351
|
return [sections.stable, sections.dynamic].filter(Boolean).join('\n\n');
|
|
345
352
|
}
|
|
346
353
|
|
|
347
|
-
module.exports = {
|
|
354
|
+
module.exports = {
|
|
355
|
+
buildSystemPrompt,
|
|
356
|
+
buildSystemPromptSections,
|
|
357
|
+
invalidateSystemPromptCache,
|
|
358
|
+
};
|