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.
- package/README.md +5 -1
- package/docs/integrations.md +11 -7
- package/package.json +2 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +4 -4
- package/server/services/ai/deliverables/artifact_helpers.js +108 -16
- package/server/services/ai/deliverables/selector.js +17 -0
- package/server/services/ai/engine.js +2 -4724
- package/server/services/ai/loop/agent_engine_core.js +1590 -0
- package/server/services/ai/loop/blank_recovery.js +37 -0
- package/server/services/ai/loop/callbacks.js +151 -0
- package/server/services/ai/loop/completion_judge.js +252 -0
- package/server/services/ai/loop/conversation_loop.js +2447 -0
- package/server/services/ai/loop/delivery_state.js +27 -0
- package/server/services/ai/loop/error_recovery.js +38 -0
- package/server/services/ai/loop/iteration_budget.js +24 -0
- package/server/services/ai/loop/messaging_delivery.js +296 -0
- package/server/services/ai/loop/model_io.js +258 -0
- package/server/services/ai/loop/progress_classification.js +178 -0
- package/server/services/ai/loop/progress_monitor.js +81 -0
- package/server/services/ai/loop/run_state.js +356 -0
- package/server/services/ai/loop/tool_dispatch.js +231 -0
- package/server/services/ai/loopPolicy.js +15 -6
- package/server/services/ai/messagingFallback.js +23 -1
- package/server/services/ai/preModelCompaction.js +31 -0
- package/server/services/ai/repetitionGuard.js +47 -2
- package/server/services/ai/systemPrompt.js +6 -0
- package/server/services/ai/taskAnalysis.js +10 -0
- package/server/services/ai/toolEvidence.js +15 -3
- package/server/services/ai/toolResult.js +29 -0
- package/server/services/ai/toolSelector.js +20 -3
- package/server/services/ai/tools.js +196 -26
- package/server/services/integrations/github/common.js +2 -2
- package/server/services/integrations/github/repos.js +123 -55
- package/server/services/runtime/docker-vm-manager.js +21 -1
- package/server/services/workspace/manager.js +56 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const READ_ONLY_COMMANDS = new Set([
|
|
4
|
+
'awk',
|
|
5
|
+
'base64',
|
|
6
|
+
'cat',
|
|
7
|
+
'curl',
|
|
8
|
+
'diff',
|
|
9
|
+
'du',
|
|
10
|
+
'egrep',
|
|
11
|
+
'env',
|
|
12
|
+
'fgrep',
|
|
13
|
+
'find',
|
|
14
|
+
'git',
|
|
15
|
+
'grep',
|
|
16
|
+
'head',
|
|
17
|
+
'jq',
|
|
18
|
+
'less',
|
|
19
|
+
'ls',
|
|
20
|
+
'pwd',
|
|
21
|
+
'rg',
|
|
22
|
+
'sed',
|
|
23
|
+
'sort',
|
|
24
|
+
'tail',
|
|
25
|
+
'tee',
|
|
26
|
+
'test',
|
|
27
|
+
'tr',
|
|
28
|
+
'tree',
|
|
29
|
+
'wc',
|
|
30
|
+
'which',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const GIT_READ_ONLY_SUBCOMMANDS = new Set([
|
|
34
|
+
'branch',
|
|
35
|
+
'diff',
|
|
36
|
+
'grep',
|
|
37
|
+
'log',
|
|
38
|
+
'ls-files',
|
|
39
|
+
'ls-remote',
|
|
40
|
+
'rev-parse',
|
|
41
|
+
'show',
|
|
42
|
+
'status',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const STATE_CHANGING_COMMANDS = new Set([
|
|
46
|
+
'apply_patch',
|
|
47
|
+
'chmod',
|
|
48
|
+
'chown',
|
|
49
|
+
'cp',
|
|
50
|
+
'git-clone',
|
|
51
|
+
'git-commit',
|
|
52
|
+
'git-push',
|
|
53
|
+
'git-switch',
|
|
54
|
+
'git-checkout',
|
|
55
|
+
'git-merge',
|
|
56
|
+
'git-rebase',
|
|
57
|
+
'install',
|
|
58
|
+
'mkdir',
|
|
59
|
+
'mv',
|
|
60
|
+
'npm',
|
|
61
|
+
'pnpm',
|
|
62
|
+
'rm',
|
|
63
|
+
'rmdir',
|
|
64
|
+
'touch',
|
|
65
|
+
'yarn',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
function stripShellNoise(command = '') {
|
|
69
|
+
return String(command || '')
|
|
70
|
+
.replace(/(^|\n)\s*#.*(?=\n|$)/g, '\n')
|
|
71
|
+
.replace(/\s+/g, ' ')
|
|
72
|
+
.trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function firstToken(segment = '') {
|
|
76
|
+
const match = String(segment || '').trim().match(/^([A-Za-z0-9_./-]+)/);
|
|
77
|
+
return match ? match[1] : '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeCommandName(token = '') {
|
|
81
|
+
return String(token || '').trim().split('/').pop().toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function splitCommandSegments(command = '') {
|
|
85
|
+
return stripShellNoise(command)
|
|
86
|
+
.split(/\s*(?:&&|\|\||;|\||\n)\s*/g)
|
|
87
|
+
.map((segment) => segment.trim())
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function stripEnvAssignments(segment = '') {
|
|
92
|
+
let text = String(segment || '').trim();
|
|
93
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(text)) {
|
|
94
|
+
text = text.replace(/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s*/, '').trim();
|
|
95
|
+
}
|
|
96
|
+
return text;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function gitSubcommand(segment = '') {
|
|
100
|
+
const parts = stripEnvAssignments(segment).split(/\s+/).filter(Boolean);
|
|
101
|
+
if (normalizeCommandName(parts[0]) !== 'git') return '';
|
|
102
|
+
return String(parts[1] || '').toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isReadOnlyGitCommand(segment = '') {
|
|
106
|
+
const subcommand = gitSubcommand(segment);
|
|
107
|
+
if (!subcommand) return false;
|
|
108
|
+
return GIT_READ_ONLY_SUBCOMMANDS.has(subcommand);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isReadOnlyInterpreterCommand(segment = '') {
|
|
112
|
+
const normalized = stripEnvAssignments(segment);
|
|
113
|
+
const commandName = normalizeCommandName(firstToken(normalized));
|
|
114
|
+
if (!['node', 'perl', 'python', 'python3'].includes(commandName)) return false;
|
|
115
|
+
if (/\b(open|write|writefile|appendfile|unlink|rename|mkdir|rmdir|remove|rm|spawn|exec)\b/i.test(normalized)) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return /\b(print|json\.|json_tool|json\.load|json\.loads|sys\.stdin|process\.exit|console\.log)\b|-m\s+json\.tool/i.test(normalized);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hasStateChangingRedirect(segment = '') {
|
|
122
|
+
const matches = String(segment || '').matchAll(/(?:^|[^&|;])(?:>>?|1>)\s*(?:"([^"]+)"|'([^']+)'|(\S+))/g);
|
|
123
|
+
for (const match of matches) {
|
|
124
|
+
const target = String(match[1] || match[2] || match[3] || '').trim();
|
|
125
|
+
if (!target || target === '/dev/null') continue;
|
|
126
|
+
if (target.startsWith('/tmp/') || target.startsWith('/var/tmp/')) continue;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isStateChangingShellSegment(segment = '') {
|
|
133
|
+
const normalized = stripEnvAssignments(segment);
|
|
134
|
+
if (hasStateChangingRedirect(normalized)) return true;
|
|
135
|
+
const command = normalizeCommandName(firstToken(normalized));
|
|
136
|
+
if (!command) return false;
|
|
137
|
+
if (command === 'git') {
|
|
138
|
+
const subcommand = gitSubcommand(normalized);
|
|
139
|
+
return subcommand && !GIT_READ_ONLY_SUBCOMMANDS.has(subcommand);
|
|
140
|
+
}
|
|
141
|
+
return STATE_CHANGING_COMMANDS.has(command);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isClearlyReadOnlyShellCommand(command = '') {
|
|
145
|
+
const segments = splitCommandSegments(command);
|
|
146
|
+
if (segments.length === 0) return false;
|
|
147
|
+
return segments.every((segment) => {
|
|
148
|
+
const normalized = stripEnvAssignments(segment);
|
|
149
|
+
if (isStateChangingShellSegment(normalized)) return false;
|
|
150
|
+
if (isReadOnlyGitCommand(normalized)) return true;
|
|
151
|
+
if (isReadOnlyInterpreterCommand(normalized)) return true;
|
|
152
|
+
const commandName = normalizeCommandName(firstToken(normalized));
|
|
153
|
+
if (!commandName) return false;
|
|
154
|
+
return READ_ONLY_COMMANDS.has(commandName);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isProgressToolCall(toolName, toolArgs = {}) {
|
|
159
|
+
const name = String(toolName || '');
|
|
160
|
+
if (!name) return false;
|
|
161
|
+
if (name === 'activate_tools' || name === 'save_widget_snapshot') return false;
|
|
162
|
+
if (name === 'send_interim_update') return false;
|
|
163
|
+
if (/^(list_|search_|read_file|get_file|find_files?|github_list|github_get|github_search|browser_get|browser_read)/.test(name)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
if (name === 'http_request' || name === 'github_api_request') {
|
|
167
|
+
return String(toolArgs?.method || 'GET').toUpperCase() !== 'GET';
|
|
168
|
+
}
|
|
169
|
+
if (name === 'execute_command') {
|
|
170
|
+
return !isClearlyReadOnlyShellCommand(toolArgs?.command || '');
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
isClearlyReadOnlyShellCommand,
|
|
177
|
+
isProgressToolCall,
|
|
178
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const FIRST_UPDATE_MS = 30 * 1000;
|
|
4
|
+
const REPEAT_UPDATE_MS = 75 * 1000;
|
|
5
|
+
const STALL_MS = 150 * 1000;
|
|
6
|
+
const TICK_MS = 15 * 1000;
|
|
7
|
+
|
|
8
|
+
function isoNow() {
|
|
9
|
+
return new Date().toISOString();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function timestampMs(value, fallback = 0) {
|
|
13
|
+
const resolved = value ? Date.parse(value) : NaN;
|
|
14
|
+
return Number.isFinite(resolved) ? resolved : fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildInitialProgressLedger({ startedAt, retryState = {} } = {}) {
|
|
18
|
+
const startedAtIso = startedAt || isoNow();
|
|
19
|
+
const interimHistory = Array.isArray(retryState.interimHistory)
|
|
20
|
+
? retryState.interimHistory
|
|
21
|
+
.map((item) => String(item?.content || '').trim())
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
: [];
|
|
24
|
+
const lastInterimMessage = interimHistory[interimHistory.length - 1] || '';
|
|
25
|
+
const lastVisibleAt = retryState.lastUserVisibleUpdateAt || (lastInterimMessage ? startedAtIso : null);
|
|
26
|
+
return {
|
|
27
|
+
currentStep: retryState.currentStep || null,
|
|
28
|
+
currentTool: retryState.currentTool || null,
|
|
29
|
+
currentStepStartedAt: retryState.currentStepStartedAt || null,
|
|
30
|
+
lastVerifiedProgressAt: retryState.lastVerifiedProgressAt || startedAtIso,
|
|
31
|
+
lastUserVisibleUpdateAt: lastVisibleAt,
|
|
32
|
+
lastFinalDeliveryAt: retryState.lastFinalDeliveryAt || null,
|
|
33
|
+
heartbeatCount: Number(retryState.heartbeatCount || 0),
|
|
34
|
+
stallNotifiedAt: retryState.stallNotifiedAt || null,
|
|
35
|
+
progressState: retryState.progressState || 'active',
|
|
36
|
+
currentPhase: retryState.currentPhase || 'idle',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function evaluateProgressLiveness(runMeta, now = Date.now()) {
|
|
41
|
+
const startedAtMs = Number.isFinite(runMeta?.startedAt) ? runMeta.startedAt : now;
|
|
42
|
+
const ledger = runMeta?.progressLedger || {};
|
|
43
|
+
const lastVerifiedAtMs = timestampMs(ledger.lastVerifiedProgressAt, startedAtMs);
|
|
44
|
+
const lastVisibleAtMs = timestampMs(ledger.lastUserVisibleUpdateAt, 0);
|
|
45
|
+
const thresholdMs = lastVisibleAtMs > 0 ? REPEAT_UPDATE_MS : FIRST_UPDATE_MS;
|
|
46
|
+
const comparisonVisibleAtMs = lastVisibleAtMs > 0 ? lastVisibleAtMs : startedAtMs;
|
|
47
|
+
const shouldNudge = (now - comparisonVisibleAtMs) >= thresholdMs;
|
|
48
|
+
const stalled = (now - lastVerifiedAtMs) >= STALL_MS;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
startedAtMs,
|
|
52
|
+
thresholdMs,
|
|
53
|
+
shouldNudge,
|
|
54
|
+
stalled,
|
|
55
|
+
phase: ledger.currentPhase || 'idle',
|
|
56
|
+
currentStep: ledger.currentStep || null,
|
|
57
|
+
currentTool: ledger.currentTool || null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildProgressNudge({ stalled = false } = {}) {
|
|
62
|
+
return [
|
|
63
|
+
'Mandatory internal progress check for the active messaging run.',
|
|
64
|
+
stalled
|
|
65
|
+
? 'No verified progress has been recorded for the stall threshold.'
|
|
66
|
+
: 'The originating chat has not received a user-visible update for the progress threshold.',
|
|
67
|
+
'Before starting more tool work, either send a concise model-authored interim update with send_interim_update, report a real blocker, or finish with the final answer.',
|
|
68
|
+
'Do not continue silently once this check is present unless the immediate next action itself delivers a final answer or explicit no-response decision.',
|
|
69
|
+
'Do not repeat previous status text and do not treat an interim update as final delivery.',
|
|
70
|
+
].join(' ');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
FIRST_UPDATE_MS,
|
|
75
|
+
REPEAT_UPDATE_MS,
|
|
76
|
+
STALL_MS,
|
|
77
|
+
TICK_MS,
|
|
78
|
+
buildInitialProgressLedger,
|
|
79
|
+
evaluateProgressLiveness,
|
|
80
|
+
buildProgressNudge,
|
|
81
|
+
};
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { v4: uuidv4 } = require('uuid');
|
|
4
|
+
const db = require('../../../db/database');
|
|
5
|
+
const { activateTools } = require('../toolSelector');
|
|
6
|
+
const { recordRunEvent } = require('../runEvents');
|
|
7
|
+
const { parseMaybeJson } = require('../logFormat');
|
|
8
|
+
const { mergeGoalContracts } = require('./completion_judge');
|
|
9
|
+
const { buildInitialProgressLedger } = require('./progress_monitor');
|
|
10
|
+
const {
|
|
11
|
+
createDeliveryState,
|
|
12
|
+
markInterimDelivered,
|
|
13
|
+
markFinalDelivered,
|
|
14
|
+
} = require('./delivery_state');
|
|
15
|
+
|
|
16
|
+
function isoNow() {
|
|
17
|
+
return new Date().toISOString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function persistRunMetadata(_engine, runId, patch = {}) {
|
|
21
|
+
if (!runId || !patch || typeof patch !== 'object') return;
|
|
22
|
+
const existing = db.prepare('SELECT metadata_json FROM agent_runs WHERE id = ?').get(runId);
|
|
23
|
+
const current = parseMaybeJson(existing?.metadata_json, {}) || {};
|
|
24
|
+
const next = { ...current, ...patch };
|
|
25
|
+
db.prepare('UPDATE agent_runs SET metadata_json = ? WHERE id = ?')
|
|
26
|
+
.run(JSON.stringify(next), runId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function updateRunGoalContract(engine, runId, patch = {}, options = {}) {
|
|
30
|
+
const runMeta = engine.getRunMeta(runId);
|
|
31
|
+
if (!runMeta) return null;
|
|
32
|
+
runMeta.goalContract = mergeGoalContracts(runMeta.goalContract, patch);
|
|
33
|
+
if (options.persist !== false) {
|
|
34
|
+
persistRunMetadata(engine, runId, {
|
|
35
|
+
goalContract: runMeta.goalContract,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return runMeta.goalContract;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildProgressLedgerSnapshot(_engine, runMeta) {
|
|
42
|
+
if (!runMeta?.progressLedger) return null;
|
|
43
|
+
return {
|
|
44
|
+
currentStep: runMeta.progressLedger.currentStep || null,
|
|
45
|
+
currentTool: runMeta.progressLedger.currentTool || null,
|
|
46
|
+
currentStepStartedAt: runMeta.progressLedger.currentStepStartedAt || null,
|
|
47
|
+
lastVerifiedProgressAt: runMeta.progressLedger.lastVerifiedProgressAt || null,
|
|
48
|
+
lastUserVisibleUpdateAt: runMeta.progressLedger.lastUserVisibleUpdateAt || null,
|
|
49
|
+
lastFinalDeliveryAt: runMeta.progressLedger.lastFinalDeliveryAt || null,
|
|
50
|
+
heartbeatCount: Number(runMeta.progressLedger.heartbeatCount || 0),
|
|
51
|
+
stallNotifiedAt: runMeta.progressLedger.stallNotifiedAt || null,
|
|
52
|
+
progressState: runMeta.progressLedger.progressState || 'active',
|
|
53
|
+
currentPhase: runMeta.progressLedger.currentPhase || 'idle',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function persistProgressLedger(engine, runId) {
|
|
58
|
+
const runMeta = engine.getRunMeta(runId);
|
|
59
|
+
if (!runMeta?.progressLedger) return;
|
|
60
|
+
persistRunMetadata(engine, runId, {
|
|
61
|
+
progressLedger: buildProgressLedgerSnapshot(engine, runMeta),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function updateRunProgress(engine, runId, patch = {}, options = {}) {
|
|
66
|
+
const runMeta = engine.getRunMeta(runId);
|
|
67
|
+
if (!runMeta) return null;
|
|
68
|
+
if (!runMeta.progressLedger) {
|
|
69
|
+
runMeta.progressLedger = buildInitialProgressLedger({
|
|
70
|
+
startedAt: runMeta.startedAtIso || isoNow(),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const previousState = runMeta.progressLedger.progressState || 'active';
|
|
75
|
+
runMeta.progressLedger = {
|
|
76
|
+
...runMeta.progressLedger,
|
|
77
|
+
...patch,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (options.verified === true) {
|
|
81
|
+
runMeta.progressLedger.lastVerifiedProgressAt = options.timestamp || isoNow();
|
|
82
|
+
runMeta.progressLedger.progressState = 'active';
|
|
83
|
+
runMeta.progressLedger.stallNotifiedAt = null;
|
|
84
|
+
recordRunEventSafe(engine, runMeta.userId, runId, 'progress_verified', {
|
|
85
|
+
phase: runMeta.progressLedger.currentPhase || 'idle',
|
|
86
|
+
currentStep: runMeta.progressLedger.currentStep || null,
|
|
87
|
+
currentTool: runMeta.progressLedger.currentTool || null,
|
|
88
|
+
}, { agentId: runMeta.agentId, stepId: options.stepId || null });
|
|
89
|
+
if (previousState === 'stalled') {
|
|
90
|
+
recordRunEventSafe(engine, runMeta.userId, runId, 'progress_resumed', {
|
|
91
|
+
phase: runMeta.progressLedger.currentPhase || 'idle',
|
|
92
|
+
currentStep: runMeta.progressLedger.currentStep || null,
|
|
93
|
+
currentTool: runMeta.progressLedger.currentTool || null,
|
|
94
|
+
}, { agentId: runMeta.agentId, stepId: options.stepId || null });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (options.persist !== false) {
|
|
99
|
+
persistProgressLedger(engine, runId);
|
|
100
|
+
}
|
|
101
|
+
return runMeta.progressLedger;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function markRunVisibleProgress(engine, runId, timestamp = isoNow()) {
|
|
105
|
+
const runMeta = engine.getRunMeta(runId);
|
|
106
|
+
if (!runMeta) return null;
|
|
107
|
+
if (!runMeta.deliveryState) runMeta.deliveryState = createDeliveryState();
|
|
108
|
+
markInterimDelivered(runMeta.deliveryState);
|
|
109
|
+
const ledger = updateRunProgress(engine, runId, {
|
|
110
|
+
lastUserVisibleUpdateAt: timestamp,
|
|
111
|
+
}, {
|
|
112
|
+
persist: false,
|
|
113
|
+
});
|
|
114
|
+
persistProgressLedger(engine, runId);
|
|
115
|
+
return ledger;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function markRunFinalDelivery(engine, runId, content = '', timestamp = isoNow()) {
|
|
119
|
+
const runMeta = engine.getRunMeta(runId);
|
|
120
|
+
if (!runMeta) return null;
|
|
121
|
+
if (!runMeta.deliveryState) runMeta.deliveryState = createDeliveryState();
|
|
122
|
+
markFinalDelivered(runMeta.deliveryState);
|
|
123
|
+
runMeta.messagingSent = true;
|
|
124
|
+
runMeta.finalDeliverySent = true;
|
|
125
|
+
runMeta.lastSentMessage = String(content || '').trim() || runMeta.lastSentMessage || '';
|
|
126
|
+
const ledger = updateRunProgress(engine, runId, {
|
|
127
|
+
lastUserVisibleUpdateAt: timestamp,
|
|
128
|
+
lastFinalDeliveryAt: timestamp,
|
|
129
|
+
progressState: 'complete',
|
|
130
|
+
}, {
|
|
131
|
+
persist: false,
|
|
132
|
+
});
|
|
133
|
+
persistProgressLedger(engine, runId);
|
|
134
|
+
return ledger;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function recordRunEventSafe(_engine, userId, runId, eventType, payload = {}, options = {}) {
|
|
138
|
+
try {
|
|
139
|
+
return recordRunEvent({
|
|
140
|
+
runId,
|
|
141
|
+
userId,
|
|
142
|
+
agentId: options.agentId || null,
|
|
143
|
+
eventType,
|
|
144
|
+
requestId: options.requestId || null,
|
|
145
|
+
stepId: options.stepId || null,
|
|
146
|
+
payload,
|
|
147
|
+
});
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function initializeToolRuntime(engine, runId, allTools, initialTools, options = {}) {
|
|
154
|
+
const runMeta = engine.getRunMeta(runId);
|
|
155
|
+
if (!runMeta) return;
|
|
156
|
+
runMeta.toolCatalog = Array.isArray(allTools) ? allTools : [];
|
|
157
|
+
runMeta.activeTools = Array.isArray(initialTools) ? initialTools : [];
|
|
158
|
+
runMeta.toolSelectionOptions = {
|
|
159
|
+
widgetId: options.widgetId || null,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getActiveTools(engine, runId) {
|
|
164
|
+
return engine.getRunMeta(runId)?.activeTools || [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function activateToolsForRun(engine, runId, names = []) {
|
|
168
|
+
const runMeta = engine.getRunMeta(runId);
|
|
169
|
+
if (!runMeta) throw new Error('Run is not active.');
|
|
170
|
+
const result = activateTools(
|
|
171
|
+
runMeta.activeTools,
|
|
172
|
+
runMeta.toolCatalog,
|
|
173
|
+
names,
|
|
174
|
+
runMeta.toolSelectionOptions,
|
|
175
|
+
);
|
|
176
|
+
runMeta.activeTools = result.tools;
|
|
177
|
+
recordRunEventSafe(engine, runMeta.userId, runId, 'tools_activated', {
|
|
178
|
+
activated: result.activated,
|
|
179
|
+
evicted: result.evicted,
|
|
180
|
+
unknown: result.unknown,
|
|
181
|
+
notActivated: result.notActivated,
|
|
182
|
+
activeToolNames: result.tools.map((tool) => tool.name),
|
|
183
|
+
}, { agentId: runMeta.agentId });
|
|
184
|
+
return {
|
|
185
|
+
success: result.unknown.length === 0 && result.notActivated.length === 0,
|
|
186
|
+
activated: result.activated,
|
|
187
|
+
evicted: result.evicted,
|
|
188
|
+
unknown: result.unknown,
|
|
189
|
+
not_activated: result.notActivated,
|
|
190
|
+
active_tools: result.tools.map((tool) => tool.name),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function findActiveRunForUser(engine, userId, predicate = null) {
|
|
195
|
+
let candidate = null;
|
|
196
|
+
for (const [runId, runMeta] of engine.activeRuns.entries()) {
|
|
197
|
+
if (runMeta.userId !== userId || runMeta.aborted) continue;
|
|
198
|
+
if (typeof predicate === 'function' && !predicate(runMeta, runId)) continue;
|
|
199
|
+
if (!candidate || (runMeta.startedAt || 0) >= (candidate.startedAt || 0)) {
|
|
200
|
+
candidate = { runId, ...runMeta };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return candidate;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function findSteerableRunForUser(engine, userId, triggerSource = 'web') {
|
|
207
|
+
return findActiveRunForUser(
|
|
208
|
+
engine,
|
|
209
|
+
userId,
|
|
210
|
+
(runMeta) => runMeta.triggerSource === triggerSource && runMeta.triggerType === 'user'
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function enqueueSteering(engine, runId, content, metadata = {}) {
|
|
215
|
+
const runMeta = engine.getRunMeta(runId);
|
|
216
|
+
const trimmed = typeof content === 'string' ? content.trim() : '';
|
|
217
|
+
if (!runMeta || runMeta.aborted || !trimmed) return null;
|
|
218
|
+
|
|
219
|
+
const item = {
|
|
220
|
+
id: uuidv4(),
|
|
221
|
+
content: trimmed,
|
|
222
|
+
metadata,
|
|
223
|
+
createdAt: isoNow(),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
runMeta.steeringQueue.push(item);
|
|
227
|
+
engine.emit(runMeta.userId, 'run:steer_queued', {
|
|
228
|
+
runId,
|
|
229
|
+
content: item.content,
|
|
230
|
+
pendingCount: runMeta.steeringQueue.length,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
runId,
|
|
235
|
+
pendingCount: runMeta.steeringQueue.length,
|
|
236
|
+
item,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function enqueueSystemSteering(engine, runId, content, metadata = {}) {
|
|
241
|
+
const runMeta = engine.getRunMeta(runId);
|
|
242
|
+
const trimmed = typeof content === 'string' ? content.trim() : '';
|
|
243
|
+
if (!runMeta || runMeta.aborted || !trimmed) return null;
|
|
244
|
+
if (!Array.isArray(runMeta.systemSteeringQueue)) {
|
|
245
|
+
runMeta.systemSteeringQueue = [];
|
|
246
|
+
}
|
|
247
|
+
const signature = JSON.stringify({
|
|
248
|
+
content: trimmed,
|
|
249
|
+
reason: metadata.reason || '',
|
|
250
|
+
});
|
|
251
|
+
if (runMeta.systemSteeringQueue.some((item) => item.signature === signature)) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const item = {
|
|
255
|
+
id: uuidv4(),
|
|
256
|
+
content: trimmed,
|
|
257
|
+
metadata,
|
|
258
|
+
signature,
|
|
259
|
+
createdAt: isoNow(),
|
|
260
|
+
};
|
|
261
|
+
runMeta.systemSteeringQueue.push(item);
|
|
262
|
+
return item;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function applyQueuedSystemSteering(engine, runId, messages) {
|
|
266
|
+
const runMeta = engine.getRunMeta(runId);
|
|
267
|
+
if (!runMeta?.systemSteeringQueue?.length) {
|
|
268
|
+
return { messages, appliedCount: 0 };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const queued = runMeta.systemSteeringQueue.splice(0, runMeta.systemSteeringQueue.length);
|
|
272
|
+
for (const entry of queued) {
|
|
273
|
+
messages.push({ role: 'system', content: entry.content });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { messages, appliedCount: queued.length };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function applyQueuedSteering(engine, runId, messages, { userId, conversationId }) {
|
|
280
|
+
const runMeta = engine.getRunMeta(runId);
|
|
281
|
+
if (!runMeta?.steeringQueue?.length) {
|
|
282
|
+
return { messages, appliedCount: 0 };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const queued = runMeta.steeringQueue.splice(0, runMeta.steeringQueue.length);
|
|
286
|
+
messages.push({
|
|
287
|
+
role: 'system',
|
|
288
|
+
content: [
|
|
289
|
+
'The user sent follow-up messages while you were already working.',
|
|
290
|
+
'Treat them as steering or next-up context for the same conversation.',
|
|
291
|
+
'If a message materially changes the active task, incorporate it now.',
|
|
292
|
+
'If it is unrelated or better handled after the current task, finish the current work first and then address it.',
|
|
293
|
+
].join(' '),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
for (const entry of queued) {
|
|
297
|
+
messages.push({ role: 'user', content: entry.content });
|
|
298
|
+
if (conversationId) {
|
|
299
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
|
|
300
|
+
.run(conversationId, 'user', entry.content);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
engine.emit(userId, 'run:steer_applied', {
|
|
305
|
+
runId,
|
|
306
|
+
count: queued.length,
|
|
307
|
+
pendingCount: runMeta.steeringQueue.length,
|
|
308
|
+
latestContent: queued[queued.length - 1]?.content || '',
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return { messages, appliedCount: queued.length };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function isRunStopped(engine, runId) {
|
|
315
|
+
return engine.getRunMeta(runId)?.aborted === true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function attachProcessToRun(engine, runId, pid) {
|
|
319
|
+
const runMeta = engine.getRunMeta(runId);
|
|
320
|
+
if (!runMeta || !pid) return;
|
|
321
|
+
runMeta.toolPids.add(pid);
|
|
322
|
+
if (runMeta.aborted) {
|
|
323
|
+
if (engine.runtimeManager && typeof engine.runtimeManager.killCommand === 'function') {
|
|
324
|
+
void engine.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function detachProcessFromRun(engine, runId, pid) {
|
|
330
|
+
const runMeta = engine.getRunMeta(runId);
|
|
331
|
+
if (!runMeta || !pid) return;
|
|
332
|
+
runMeta.toolPids.delete(pid);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = {
|
|
336
|
+
activateToolsForRun,
|
|
337
|
+
applyQueuedSteering,
|
|
338
|
+
applyQueuedSystemSteering,
|
|
339
|
+
attachProcessToRun,
|
|
340
|
+
buildProgressLedgerSnapshot,
|
|
341
|
+
detachProcessFromRun,
|
|
342
|
+
enqueueSteering,
|
|
343
|
+
enqueueSystemSteering,
|
|
344
|
+
findActiveRunForUser,
|
|
345
|
+
findSteerableRunForUser,
|
|
346
|
+
getActiveTools,
|
|
347
|
+
initializeToolRuntime,
|
|
348
|
+
isRunStopped,
|
|
349
|
+
markRunFinalDelivery,
|
|
350
|
+
markRunVisibleProgress,
|
|
351
|
+
persistProgressLedger,
|
|
352
|
+
persistRunMetadata,
|
|
353
|
+
recordRunEventSafe,
|
|
354
|
+
updateRunGoalContract,
|
|
355
|
+
updateRunProgress,
|
|
356
|
+
};
|