neoagent 3.0.0 → 3.0.1-beta.1
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/flutter_app/lib/main_controller.dart +34 -0
- package/flutter_app/lib/main_security.dart +19 -0
- package/lib/schema_migrations.js +224 -0
- package/package.json +1 -1
- package/server/admin/admin.css +29 -0
- package/server/admin/admin.js +81 -1
- package/server/admin/index.html +97 -9
- package/server/db/database.js +57 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +32788 -32752
- package/server/routes/security.js +19 -0
- package/server/routes/settings.js +1 -0
- package/server/routes/skills.js +9 -5
- package/server/services/ai/hooks.js +3 -2
- package/server/services/ai/learning.js +8 -3
- package/server/services/ai/loop/agent_engine_core.js +67 -0
- package/server/services/ai/loop/conversation_loop.js +49 -13
- package/server/services/ai/settings.js +8 -0
- package/server/services/ai/systemPrompt.js +18 -0
- package/server/services/ai/toolEvidence.js +24 -0
- package/server/services/ai/toolRunner.js +262 -55
- package/server/services/ai/tools.js +5 -5
- package/server/services/cli/shell_worker_pool.js +87 -5
- package/server/services/manager.js +16 -0
- package/server/services/memory/manager.js +39 -24
- package/server/services/runtime/manager.js +6 -2
- package/server/services/security/approval_gate_service.js +108 -5
- package/server/services/security/tool_categories.js +113 -4
- package/server/services/security/tool_security_hook.js +7 -2
- package/server/services/skills/runtime.js +2 -0
|
@@ -85,6 +85,25 @@ router.post('/approvals/:approvalId', (req, res) => {
|
|
|
85
85
|
normalizedScope,
|
|
86
86
|
);
|
|
87
87
|
if (!resolved) {
|
|
88
|
+
const storedApproval = approvalGateService.getStoredApproval?.(
|
|
89
|
+
approvalId,
|
|
90
|
+
req.session.userId,
|
|
91
|
+
);
|
|
92
|
+
if (storedApproval?.status === 'expired' || storedApproval?.status === 'timeout') {
|
|
93
|
+
return res.status(410).json({
|
|
94
|
+
error: 'Approval expired because the run was interrupted or the server restarted.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (runId) {
|
|
98
|
+
const run = db.prepare(
|
|
99
|
+
'SELECT status FROM agent_runs WHERE id = ? AND user_id = ?'
|
|
100
|
+
).get(runId, req.session.userId);
|
|
101
|
+
if (run?.status === 'interrupted') {
|
|
102
|
+
return res.status(410).json({
|
|
103
|
+
error: 'Approval expired because the run was interrupted or the server restarted.',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
88
107
|
return res.status(404).json({ error: 'Approval not found or already resolved' });
|
|
89
108
|
}
|
|
90
109
|
res.json({ ok: true, approvalId, decision, scope: normalizedScope });
|
package/server/routes/skills.js
CHANGED
|
@@ -56,7 +56,7 @@ function parseSkillDocument(content) {
|
|
|
56
56
|
router.get('/', async (req, res) => {
|
|
57
57
|
try {
|
|
58
58
|
const runner = await getSkillRunner(req.app);
|
|
59
|
-
const skills = runner.getAll().map(serializeInstalledSkill);
|
|
59
|
+
const skills = runner.getAll(req.session.userId).map(serializeInstalledSkill);
|
|
60
60
|
res.json(skills.sort(sortInstalledSkills));
|
|
61
61
|
} catch (err) {
|
|
62
62
|
res.status(500).json({ error: sanitizeError(err) });
|
|
@@ -107,7 +107,7 @@ router.get('/audit/summary', (req, res) => {
|
|
|
107
107
|
router.get('/:name', async (req, res) => {
|
|
108
108
|
try {
|
|
109
109
|
const runner = await getSkillRunner(req.app);
|
|
110
|
-
const skill = runner.getSkill(req.params.name);
|
|
110
|
+
const skill = runner.getSkill(req.params.name, req.session.userId);
|
|
111
111
|
if (!skill) return res.status(404).json({ error: 'Skill not found' });
|
|
112
112
|
const fs = require('fs');
|
|
113
113
|
const content = fs.readFileSync(skill.filePath, 'utf-8');
|
|
@@ -132,6 +132,7 @@ router.post('/', async (req, res) => {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
const result = runner.createSkill(
|
|
135
|
+
req.session.userId,
|
|
135
136
|
req.body.filename || parsed.name,
|
|
136
137
|
parsed.description,
|
|
137
138
|
parsed.instructions,
|
|
@@ -149,7 +150,8 @@ router.put('/:name', async (req, res) => {
|
|
|
149
150
|
try {
|
|
150
151
|
const runner = await getSkillRunner(req.app);
|
|
151
152
|
if (typeof req.body.enabled === 'boolean' && !req.body.content) {
|
|
152
|
-
const result = runner.setSkillEnabled(req.params.name, req.body.enabled);
|
|
153
|
+
const result = runner.setSkillEnabled(req.session.userId, req.params.name, req.body.enabled);
|
|
154
|
+
if (result.code === 'forbidden') return res.status(403).json(result);
|
|
153
155
|
if (result.error) return res.status(404).json(result);
|
|
154
156
|
return res.json(result);
|
|
155
157
|
}
|
|
@@ -159,11 +161,12 @@ router.put('/:name', async (req, res) => {
|
|
|
159
161
|
if (parsed.metadata?.command && !isValidCommandTemplate(parsed.metadata.command)) {
|
|
160
162
|
return res.status(400).json({ error: 'Skill command template contains invalid characters' });
|
|
161
163
|
}
|
|
162
|
-
const result = runner.updateSkill(req.params.name, {
|
|
164
|
+
const result = runner.updateSkill(req.session.userId, req.params.name, {
|
|
163
165
|
description: parsed.description,
|
|
164
166
|
instructions: parsed.instructions,
|
|
165
167
|
metadata: parsed.metadata
|
|
166
168
|
});
|
|
169
|
+
if (result.code === 'forbidden') return res.status(403).json(result);
|
|
167
170
|
if (result.error) return res.status(404).json(result);
|
|
168
171
|
res.json(result);
|
|
169
172
|
} catch (err) {
|
|
@@ -174,7 +177,8 @@ router.put('/:name', async (req, res) => {
|
|
|
174
177
|
router.delete('/:name', async (req, res) => {
|
|
175
178
|
try {
|
|
176
179
|
const runner = await getSkillRunner(req.app);
|
|
177
|
-
const result = runner.deleteSkill(req.params.name);
|
|
180
|
+
const result = runner.deleteSkill(req.session.userId, req.params.name);
|
|
181
|
+
if (result.code === 'forbidden') return res.status(403).json(result);
|
|
178
182
|
if (result.error) return res.status(404).json(result);
|
|
179
183
|
res.json(result);
|
|
180
184
|
} catch (err) {
|
|
@@ -81,7 +81,8 @@ class AgentHooks {
|
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Run all handlers for an event, merging their return values.
|
|
84
|
-
* If any handler returns { block: true }, short-circuits and
|
|
84
|
+
* If any handler returns { block: true }, short-circuits and preserves the
|
|
85
|
+
* handler's metadata so the engine can explain why the tool was blocked.
|
|
85
86
|
*
|
|
86
87
|
* @param {string} event
|
|
87
88
|
* @param {object} ctx - Context passed to every handler
|
|
@@ -98,7 +99,7 @@ class AgentHooks {
|
|
|
98
99
|
console.warn(`[Hooks] Handler "${id}" for "${event}" threw:`, err.message);
|
|
99
100
|
continue; // don't let a bad hook crash the loop
|
|
100
101
|
}
|
|
101
|
-
if (result?.block === true) return {
|
|
102
|
+
if (result?.block === true) return { ...merged, ...result };
|
|
102
103
|
if (result && typeof result === 'object') {
|
|
103
104
|
merged = { ...merged, ...result };
|
|
104
105
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
const db = require('../../db/database');
|
|
3
|
+
const { getAiSettings } = require('./settings');
|
|
3
4
|
|
|
4
5
|
function sanitizeSkillName(input) {
|
|
5
6
|
const base = String(input || '')
|
|
@@ -111,6 +112,8 @@ class LearningManager {
|
|
|
111
112
|
if (!userId || !agentId || !runId || !task || !finalContent) return null;
|
|
112
113
|
if (triggerType && triggerType !== 'user') return null;
|
|
113
114
|
if (triggerSource && triggerSource !== 'web') return null;
|
|
115
|
+
const aiSettings = getAiSettings(userId, agentId);
|
|
116
|
+
if (!aiSettings.auto_skill_learning) return null;
|
|
114
117
|
|
|
115
118
|
const successfulSteps = Array.isArray(steps)
|
|
116
119
|
? steps.filter((step) => step.status === 'completed' && step.tool_name)
|
|
@@ -145,12 +148,13 @@ class LearningManager {
|
|
|
145
148
|
).get(userId, agentId, draft.metadata.workflow_signature);
|
|
146
149
|
if (Number(observation?.observation_count || 0) < 3) return null;
|
|
147
150
|
|
|
148
|
-
const existing =
|
|
149
|
-
|
|
151
|
+
const existing = this.skillRunner.findSkillByWorkflowSignature(
|
|
152
|
+
userId,
|
|
153
|
+
draft.metadata.workflow_signature,
|
|
150
154
|
);
|
|
151
155
|
if (existing) {
|
|
152
156
|
if (existing.metadata?.enabled !== false || existing.metadata?.draft !== true) return null;
|
|
153
|
-
return this.skillRunner.updateSkill(existing.name, {
|
|
157
|
+
return this.skillRunner.updateSkill(userId, existing.name, {
|
|
154
158
|
description: draft.description,
|
|
155
159
|
instructions: draft.instructions,
|
|
156
160
|
metadata: {
|
|
@@ -163,6 +167,7 @@ class LearningManager {
|
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
const result = this.skillRunner.createSkill(
|
|
170
|
+
userId,
|
|
166
171
|
draft.name,
|
|
167
172
|
draft.description,
|
|
168
173
|
draft.instructions,
|
|
@@ -247,6 +247,7 @@ class AgentEngine {
|
|
|
247
247
|
const promptSections = await buildSystemPromptSections(userId, context, memoryManager);
|
|
248
248
|
const skillRunner = context.skillRunner || this.skillRunner || null;
|
|
249
249
|
const skillsPrompt = skillRunner?.getSkillsForPrompt?.({
|
|
250
|
+
userId,
|
|
250
251
|
maxTotalChars: 9000,
|
|
251
252
|
maxDescriptionChars: 180,
|
|
252
253
|
maxTriggerChars: 100,
|
|
@@ -1180,6 +1181,24 @@ class AgentEngine {
|
|
|
1180
1181
|
}
|
|
1181
1182
|
|
|
1182
1183
|
async spawnSubagent(userId, parentRunId, task, options = {}) {
|
|
1184
|
+
const parentRunMeta = this.getRunMeta(parentRunId);
|
|
1185
|
+
const parentDepth = Math.max(0, Number(parentRunMeta?.subagentDepth) || 0);
|
|
1186
|
+
if (parentDepth >= 1) {
|
|
1187
|
+
return {
|
|
1188
|
+
error: 'Sub-agents cannot spawn additional sub-agents. Continue the current child run or return results to the parent run.',
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const aiSettings = getAiSettings(userId, options.agentId || null);
|
|
1193
|
+
const maxSubagentsPerRun = Math.max(1, Number(aiSettings.subagent_max_children_per_run) || 10);
|
|
1194
|
+
const existingSubagents = Array.from(this.subagents.values())
|
|
1195
|
+
.filter((record) => record.parentRunId === parentRunId);
|
|
1196
|
+
if (existingSubagents.length >= maxSubagentsPerRun) {
|
|
1197
|
+
return {
|
|
1198
|
+
error: `This run has already spawned ${existingSubagents.length} sub-agents. The limit for one run is ${maxSubagentsPerRun}.`,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1183
1202
|
const handle = uuidv4();
|
|
1184
1203
|
const childRunId = uuidv4();
|
|
1185
1204
|
let relevantMemories = [];
|
|
@@ -1256,6 +1275,8 @@ class AgentEngine {
|
|
|
1256
1275
|
triggerSource: 'agent',
|
|
1257
1276
|
runId: childRunId,
|
|
1258
1277
|
agentId: options.agentId || null,
|
|
1278
|
+
subagentDepth: parentDepth + 1,
|
|
1279
|
+
disallowedToolNames: ['spawn_subagent'],
|
|
1259
1280
|
},
|
|
1260
1281
|
options.model || null
|
|
1261
1282
|
);
|
|
@@ -1521,6 +1542,52 @@ class AgentEngine {
|
|
|
1521
1542
|
return { handle, status: 'cancelled' };
|
|
1522
1543
|
}
|
|
1523
1544
|
|
|
1545
|
+
interruptRun(runId, reason = 'Server shutting down while run was in progress.') {
|
|
1546
|
+
const runMeta = this.activeRuns.get(runId);
|
|
1547
|
+
const delegatedChildren = db.prepare(
|
|
1548
|
+
"SELECT child_run_id FROM agent_delegations WHERE parent_run_id = ? AND status = 'running'"
|
|
1549
|
+
).all(runId);
|
|
1550
|
+
if (runMeta) {
|
|
1551
|
+
runMeta.status = 'interrupted';
|
|
1552
|
+
runMeta.stopReason = reason;
|
|
1553
|
+
runMeta.aborted = true;
|
|
1554
|
+
this.emit(runMeta.userId, 'run:stopping', { runId });
|
|
1555
|
+
for (const pid of runMeta.toolPids) {
|
|
1556
|
+
if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
|
|
1557
|
+
void this.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
runMeta.toolPids.clear();
|
|
1561
|
+
}
|
|
1562
|
+
for (const child of delegatedChildren) {
|
|
1563
|
+
if (child.child_run_id && child.child_run_id !== runId) {
|
|
1564
|
+
this.interruptRun(child.child_run_id, reason);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
db.prepare(
|
|
1568
|
+
`UPDATE agent_delegations
|
|
1569
|
+
SET status = 'interrupted',
|
|
1570
|
+
error = COALESCE(NULLIF(error, ''), ?),
|
|
1571
|
+
updated_at = datetime('now'),
|
|
1572
|
+
completed_at = datetime('now')
|
|
1573
|
+
WHERE parent_run_id = ? AND status = 'running'`
|
|
1574
|
+
).run(reason, runId);
|
|
1575
|
+
db.prepare(
|
|
1576
|
+
`UPDATE agent_runs
|
|
1577
|
+
SET status = 'interrupted',
|
|
1578
|
+
error = COALESCE(NULLIF(error, ''), ?),
|
|
1579
|
+
updated_at = datetime('now'),
|
|
1580
|
+
completed_at = datetime('now')
|
|
1581
|
+
WHERE id = ?`
|
|
1582
|
+
).run(reason, runId);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
interruptAllActiveRuns(reason = 'Server shutting down while run was in progress.') {
|
|
1586
|
+
for (const runId of Array.from(this.activeRuns.keys())) {
|
|
1587
|
+
this.interruptRun(runId, reason);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1524
1591
|
stopRun(runId) {
|
|
1525
1592
|
const runMeta = this.activeRuns.get(runId);
|
|
1526
1593
|
const delegatedChildren = db.prepare(
|
|
@@ -141,6 +141,8 @@ const {
|
|
|
141
141
|
} = require('../messagingFallback');
|
|
142
142
|
const {
|
|
143
143
|
classifyToolExecution,
|
|
144
|
+
isSubstantiveProgressToolName,
|
|
145
|
+
summarizeProgressToolExecutions,
|
|
144
146
|
summarizeToolExecutions,
|
|
145
147
|
summarizeAvailableTools,
|
|
146
148
|
inferToolFailureMessage,
|
|
@@ -663,6 +665,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
663
665
|
steeringQueue: [],
|
|
664
666
|
systemSteeringQueue: [],
|
|
665
667
|
toolPids: new Set(),
|
|
668
|
+
subagentDepth: Math.max(0, Number(options.subagentDepth) || 0),
|
|
666
669
|
repetitionGuard: new ToolRepetitionGuard(),
|
|
667
670
|
seenOutputHashes: new Map(),
|
|
668
671
|
consecutiveReadOnlyIterations: 0,
|
|
@@ -715,7 +718,13 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
715
718
|
const mcpManager = app?.locals?.mcpManager || app?.locals?.mcpClient || engine.mcpManager;
|
|
716
719
|
const integrationManager = app?.locals?.integrationManager || null;
|
|
717
720
|
const mcpTools = mcpManager ? mcpManager.getAllTools(userId, { agentId }) : [];
|
|
718
|
-
const
|
|
721
|
+
const disallowedToolNames = new Set(
|
|
722
|
+
(Array.isArray(options.disallowedToolNames) ? options.disallowedToolNames : [])
|
|
723
|
+
.map((name) => String(name || '').trim())
|
|
724
|
+
.filter(Boolean),
|
|
725
|
+
);
|
|
726
|
+
const allTools = selectToolsForTask(userMessage, builtInTools, mcpTools, options)
|
|
727
|
+
.filter((tool) => !disallowedToolNames.has(tool?.name));
|
|
719
728
|
let tools = allTools;
|
|
720
729
|
const toolNames = allTools.map((tool) => tool.name).filter(Boolean);
|
|
721
730
|
const coreToolStatus = {
|
|
@@ -825,15 +834,21 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
825
834
|
// Real, specific evidence (actual commands + their output) so the update is
|
|
826
835
|
// grounded in what happened, not invented. Bare tool names make a weak model
|
|
827
836
|
// confabulate generic activity ("running the build", "training").
|
|
828
|
-
const recent =
|
|
837
|
+
const recent = summarizeProgressToolExecutions(toolExecutions, 5);
|
|
838
|
+
const currentTool = isSubstantiveProgressToolName(ledger.currentTool)
|
|
839
|
+
? ledger.currentTool
|
|
840
|
+
: '';
|
|
841
|
+
if (!recent && !currentTool) {
|
|
842
|
+
return '';
|
|
843
|
+
}
|
|
829
844
|
const priorUpdate = String(rm?.lastInterimMessage || '').trim();
|
|
830
845
|
const contextBlock = [
|
|
831
846
|
buildProgressUpdatePrompt(),
|
|
832
847
|
stalled ? 'This step has been running a while with no new progress; reassure the user you are still on it.' : '',
|
|
833
848
|
'',
|
|
834
849
|
`Original request: ${summarizeForLog(userMessage, 320)}`,
|
|
835
|
-
`Doing now:
|
|
836
|
-
`Actual recent tool activity (newest last) — describe ONLY this, do not extrapolate:\n${recent}
|
|
850
|
+
currentTool ? `Doing now: using ${currentTool}` : '',
|
|
851
|
+
recent ? `Actual recent tool activity (newest last) — describe ONLY this, do not extrapolate:\n${recent}` : '',
|
|
837
852
|
priorUpdate ? `Your previous update (say something different): ${summarizeForLog(priorUpdate, 160)}` : '',
|
|
838
853
|
].filter(Boolean).join('\n');
|
|
839
854
|
// Reuse the run's real system prompt so the update follows the same voice and
|
|
@@ -1922,20 +1937,41 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1922
1937
|
}
|
|
1923
1938
|
|
|
1924
1939
|
if (engine.isRunStopped(runId)) {
|
|
1925
|
-
|
|
1926
|
-
|
|
1940
|
+
const stoppedRunMeta = engine.getRunMeta(runId);
|
|
1941
|
+
const terminalStatus = stoppedRunMeta?.status === 'interrupted' ? 'interrupted' : 'stopped';
|
|
1942
|
+
const stopReason = stoppedRunMeta?.stopReason || null;
|
|
1943
|
+
db.prepare(
|
|
1944
|
+
`UPDATE agent_runs
|
|
1945
|
+
SET status = ?,
|
|
1946
|
+
error = COALESCE(?, error),
|
|
1947
|
+
updated_at = datetime('now'),
|
|
1948
|
+
completed_at = datetime('now')
|
|
1949
|
+
WHERE id = ?`
|
|
1950
|
+
).run(terminalStatus, stopReason, runId);
|
|
1927
1951
|
console.warn(
|
|
1928
|
-
`[Run ${shortenRunId(runId)}]
|
|
1952
|
+
`[Run ${shortenRunId(runId)}] ${terminalStatus} trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
|
|
1929
1953
|
);
|
|
1954
|
+
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
1930
1955
|
engine.stopMessagingProgressSupervisor(runId);
|
|
1931
1956
|
engine.activeRuns.delete(runId);
|
|
1932
|
-
engine.emit(userId, 'run:stopped', {
|
|
1933
|
-
|
|
1957
|
+
engine.emit(userId, terminalStatus === 'interrupted' ? 'run:interrupted' : 'run:stopped', {
|
|
1958
|
+
runId,
|
|
1934
1959
|
triggerSource,
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1960
|
+
reason: stopReason,
|
|
1961
|
+
});
|
|
1962
|
+
engine.recordRunEvent(
|
|
1963
|
+
userId,
|
|
1964
|
+
runId,
|
|
1965
|
+
terminalStatus === 'interrupted' ? 'run_interrupted' : 'run_stopped',
|
|
1966
|
+
{
|
|
1967
|
+
triggerSource,
|
|
1968
|
+
totalTokens,
|
|
1969
|
+
iterations: iteration,
|
|
1970
|
+
reason: stopReason,
|
|
1971
|
+
},
|
|
1972
|
+
{ agentId },
|
|
1973
|
+
);
|
|
1974
|
+
return { runId, content: '', totalTokens, iterations: iteration, status: terminalStatus };
|
|
1939
1975
|
}
|
|
1940
1976
|
|
|
1941
1977
|
const runMeta = engine.activeRuns.get(runId);
|
|
@@ -149,6 +149,7 @@ function createDefaultAiSettings() {
|
|
|
149
149
|
chat_history_window: 20,
|
|
150
150
|
tool_replay_budget_chars: 6000,
|
|
151
151
|
subagent_max_iterations: 6,
|
|
152
|
+
subagent_max_children_per_run: 10,
|
|
152
153
|
assistant_behavior_notes: '',
|
|
153
154
|
auto_skill_learning: false,
|
|
154
155
|
auto_recording_insights: true,
|
|
@@ -330,6 +331,13 @@ function getAiSettings(userId, agentId = null) {
|
|
|
330
331
|
settings.chat_history_window = Math.max(6, Math.min(Number(settings.chat_history_window) || DEFAULT_AI_SETTINGS.chat_history_window, 40));
|
|
331
332
|
settings.tool_replay_budget_chars = Math.max(1200, Math.min(Number(settings.tool_replay_budget_chars) || DEFAULT_AI_SETTINGS.tool_replay_budget_chars, 12000));
|
|
332
333
|
settings.subagent_max_iterations = Math.max(2, Math.min(Number(settings.subagent_max_iterations) || DEFAULT_AI_SETTINGS.subagent_max_iterations, 12));
|
|
334
|
+
settings.subagent_max_children_per_run = Math.max(
|
|
335
|
+
1,
|
|
336
|
+
Math.min(
|
|
337
|
+
Number(settings.subagent_max_children_per_run) || DEFAULT_AI_SETTINGS.subagent_max_children_per_run,
|
|
338
|
+
20,
|
|
339
|
+
),
|
|
340
|
+
);
|
|
333
341
|
settings.cost_mode = typeof settings.cost_mode === 'string' ? settings.cost_mode : DEFAULT_AI_SETTINGS.cost_mode;
|
|
334
342
|
settings.assistant_behavior_notes = typeof settings.assistant_behavior_notes === 'string'
|
|
335
343
|
? settings.assistant_behavior_notes
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
const os = require('os');
|
|
2
2
|
|
|
3
3
|
const PROMPT_CACHE_TTL = 30_000;
|
|
4
|
+
const PROMPT_CACHE_MAX = 500;
|
|
4
5
|
const promptCache = new Map();
|
|
5
6
|
|
|
7
|
+
function evictExpiredPromptCache() {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
for (const [key, entry] of promptCache.entries()) {
|
|
10
|
+
if (now >= entry.expiresAt) promptCache.delete(key);
|
|
11
|
+
}
|
|
12
|
+
if (promptCache.size > PROMPT_CACHE_MAX) {
|
|
13
|
+
const excess = promptCache.size - PROMPT_CACHE_MAX;
|
|
14
|
+
let deleted = 0;
|
|
15
|
+
for (const key of promptCache.keys()) {
|
|
16
|
+
if (deleted >= excess) break;
|
|
17
|
+
promptCache.delete(key);
|
|
18
|
+
deleted++;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
function invalidateSystemPromptCache(userId, agentId = null) {
|
|
7
24
|
const prefix = `${String(userId || 'global')}:${String(agentId || 'main')}:`;
|
|
8
25
|
for (const key of promptCache.keys()) {
|
|
@@ -358,6 +375,7 @@ async function buildSystemPromptSections(userId, context = {}, memoryManager) {
|
|
|
358
375
|
};
|
|
359
376
|
|
|
360
377
|
if (!hasExtraContext) {
|
|
378
|
+
evictExpiredPromptCache();
|
|
361
379
|
promptCache.set(cacheKey, { sections, expiresAt: now + PROMPT_CACHE_TTL });
|
|
362
380
|
}
|
|
363
381
|
|
|
@@ -153,6 +153,27 @@ function summarizeToolExecutions(toolExecutions = [], maxItems = 10) {
|
|
|
153
153
|
}).join('\n');
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
function isSubstantiveProgressToolName(toolName = '') {
|
|
157
|
+
const name = String(toolName || '').trim();
|
|
158
|
+
if (!name) return false;
|
|
159
|
+
if (name === 'send_message' || name === 'send_interim_update' || name === 'make_call') return false;
|
|
160
|
+
if (name === 'think' || name === 'activate_tools' || name === 'task_complete') return false;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isSubstantiveProgressEvidence(item = {}) {
|
|
165
|
+
if (!isSubstantiveProgressToolName(item.toolName)) return false;
|
|
166
|
+
if (item.evidenceSource === 'messaging') return false;
|
|
167
|
+
return Boolean(item.evidenceRelevant || item.stateChanged || item.error);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function summarizeProgressToolExecutions(toolExecutions = [], maxItems = 10) {
|
|
171
|
+
return summarizeToolExecutions(
|
|
172
|
+
toolExecutions.filter(isSubstantiveProgressEvidence),
|
|
173
|
+
maxItems,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
156
177
|
function summarizeAvailableTools(tools = [], { exclude = [] } = {}) {
|
|
157
178
|
const excluded = new Set((Array.isArray(exclude) ? exclude : [exclude]).filter(Boolean));
|
|
158
179
|
return tools
|
|
@@ -214,6 +235,9 @@ function buildAutonomousRecoveryContext({ err, toolExecutions = [], tools = [],
|
|
|
214
235
|
module.exports = {
|
|
215
236
|
classifyToolExecution,
|
|
216
237
|
deriveEvidenceSource,
|
|
238
|
+
isSubstantiveProgressEvidence,
|
|
239
|
+
isSubstantiveProgressToolName,
|
|
240
|
+
summarizeProgressToolExecutions,
|
|
217
241
|
summarizeToolExecutions,
|
|
218
242
|
summarizeAvailableTools,
|
|
219
243
|
inferToolFailureMessage,
|