@code4bug/jarvis-agent 1.1.5 → 1.1.7
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 +171 -215
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +17 -15
- package/dist/components/MultilineInput.js +2 -2
- package/dist/components/WelcomeHeader.js +1 -1
- package/dist/config/dream.d.ts +10 -0
- package/dist/config/dream.js +60 -0
- package/dist/config/memory.d.ts +7 -0
- package/dist/config/memory.js +55 -0
- package/dist/config/userProfile.d.ts +5 -1
- package/dist/config/userProfile.js +15 -2
- package/dist/core/QueryEngine.d.ts +11 -0
- package/dist/core/QueryEngine.js +104 -8
- package/dist/core/WorkerBridge.d.ts +3 -1
- package/dist/core/WorkerBridge.js +2 -2
- package/dist/core/query.d.ts +5 -1
- package/dist/core/query.js +4 -4
- package/dist/core/queryWorker.d.ts +3 -0
- package/dist/core/queryWorker.js +1 -1
- package/dist/hooks/useSlashMenu.d.ts +3 -1
- package/dist/hooks/useSlashMenu.js +58 -71
- package/dist/screens/repl.js +63 -34
- package/dist/services/api/llm.d.ts +5 -2
- package/dist/services/api/llm.js +28 -7
- package/dist/services/api/mock.d.ts +3 -1
- package/dist/services/api/mock.js +1 -1
- package/dist/services/dream.d.ts +7 -0
- package/dist/services/dream.js +171 -0
- package/dist/services/persistentMemory.d.ts +8 -0
- package/dist/services/persistentMemory.js +178 -0
- package/dist/services/userProfile.d.ts +1 -0
- package/dist/services/userProfile.js +15 -0
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/manageMemory.d.ts +2 -0
- package/dist/tools/manageMemory.js +46 -0
- package/dist/types/index.d.ts +3 -1
- package/package.json +3 -3
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export const MEMORY_FILE_PATH = path.join(os.homedir(), '.jarvis', 'MEMORY.md');
|
|
5
|
+
const MEMORY_FILE_HEADER = [
|
|
6
|
+
'# Jarvis 长期记忆',
|
|
7
|
+
'',
|
|
8
|
+
'> 这里沉淀可复用的经验、技能、偏好、约束与稳定环境事实。',
|
|
9
|
+
'> 避免写入一次性闲聊、临时输出、密钥或其它敏感信息。',
|
|
10
|
+
'',
|
|
11
|
+
].join('\n');
|
|
12
|
+
export function ensureMemoryHomeDir() {
|
|
13
|
+
const dir = path.dirname(MEMORY_FILE_PATH);
|
|
14
|
+
if (!fs.existsSync(dir)) {
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
export function ensureMemoryFile() {
|
|
20
|
+
ensureMemoryHomeDir();
|
|
21
|
+
if (!fs.existsSync(MEMORY_FILE_PATH)) {
|
|
22
|
+
fs.writeFileSync(MEMORY_FILE_PATH, MEMORY_FILE_HEADER, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
return MEMORY_FILE_PATH;
|
|
25
|
+
}
|
|
26
|
+
export function readPersistentMemory() {
|
|
27
|
+
try {
|
|
28
|
+
ensureMemoryFile();
|
|
29
|
+
return fs.readFileSync(MEMORY_FILE_PATH, 'utf-8').trim();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function readPersistentMemoryForPrompt(maxChars = 12000) {
|
|
36
|
+
const content = readPersistentMemory();
|
|
37
|
+
if (!content)
|
|
38
|
+
return '';
|
|
39
|
+
if (content.length <= maxChars)
|
|
40
|
+
return content;
|
|
41
|
+
return `...[已截断,仅保留最近 ${maxChars} 字符]\n${content.slice(-maxChars)}`;
|
|
42
|
+
}
|
|
43
|
+
export function appendPersistentMemory(content) {
|
|
44
|
+
const normalized = content.trim();
|
|
45
|
+
if (!normalized)
|
|
46
|
+
return;
|
|
47
|
+
ensureMemoryFile();
|
|
48
|
+
const current = fs.readFileSync(MEMORY_FILE_PATH, 'utf-8');
|
|
49
|
+
const suffix = current.endsWith('\n\n') ? '' : (current.endsWith('\n') ? '\n' : '\n\n');
|
|
50
|
+
fs.appendFileSync(MEMORY_FILE_PATH, `${suffix}${normalized}\n`, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
export function replacePersistentMemory(content) {
|
|
53
|
+
ensureMemoryFile();
|
|
54
|
+
fs.writeFileSync(MEMORY_FILE_PATH, `${content.trimEnd()}\n`, 'utf-8');
|
|
55
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export declare const USER_PROFILE_PATH: string;
|
|
2
2
|
export declare function ensureJarvisHomeDir(): string;
|
|
3
3
|
export declare function readUserProfile(): string;
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function initializeUserProfileCache(): string;
|
|
5
|
+
export declare function getCachedUserProfile(): string;
|
|
6
|
+
export declare function writeUserProfile(content: string, options?: {
|
|
7
|
+
updateCache?: boolean;
|
|
8
|
+
}): void;
|
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
export const USER_PROFILE_PATH = path.join(os.homedir(), '.jarvis', 'USER.md');
|
|
5
|
+
let cachedUserProfile = '';
|
|
5
6
|
export function ensureJarvisHomeDir() {
|
|
6
7
|
const dir = path.dirname(USER_PROFILE_PATH);
|
|
7
8
|
if (!fs.existsSync(dir)) {
|
|
@@ -19,7 +20,19 @@ export function readUserProfile() {
|
|
|
19
20
|
return '';
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
|
-
export function
|
|
23
|
+
export function initializeUserProfileCache() {
|
|
24
|
+
cachedUserProfile = readUserProfile();
|
|
25
|
+
return cachedUserProfile;
|
|
26
|
+
}
|
|
27
|
+
export function getCachedUserProfile() {
|
|
28
|
+
return cachedUserProfile;
|
|
29
|
+
}
|
|
30
|
+
export function writeUserProfile(content, options) {
|
|
23
31
|
ensureJarvisHomeDir();
|
|
24
|
-
|
|
32
|
+
const normalizedContent = content.trimEnd();
|
|
33
|
+
fs.writeFileSync(USER_PROFILE_PATH, normalizedContent + '\n', 'utf-8');
|
|
34
|
+
if (options?.updateCache) {
|
|
35
|
+
cachedUserProfile = normalizedContent;
|
|
36
|
+
}
|
|
25
37
|
}
|
|
38
|
+
initializeUserProfileCache();
|
|
@@ -20,6 +20,12 @@ export declare class QueryEngine {
|
|
|
20
20
|
private session;
|
|
21
21
|
private transcript;
|
|
22
22
|
private workerBridge;
|
|
23
|
+
private memoryUpdateQueue;
|
|
24
|
+
private userProfileUpdateQueue;
|
|
25
|
+
private dreamUpdateQueue;
|
|
26
|
+
private dreamTimer;
|
|
27
|
+
private lastActivityAt;
|
|
28
|
+
private isQueryRunning;
|
|
23
29
|
constructor();
|
|
24
30
|
/** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
|
|
25
31
|
registerUIBus(onMessage: (msg: Message) => void, onUpdateMessage: (id: string, updates: Partial<Message>) => void): void;
|
|
@@ -38,6 +44,11 @@ export declare class QueryEngine {
|
|
|
38
44
|
switchAgent(agentName: string): void;
|
|
39
45
|
/** 保存会话到文件 */
|
|
40
46
|
private saveSession;
|
|
47
|
+
private schedulePersistentMemoryUpdate;
|
|
48
|
+
private scheduleUserProfileUpdate;
|
|
49
|
+
private touchActivity;
|
|
50
|
+
private scheduleDreamTimer;
|
|
51
|
+
private runDreamCycle;
|
|
41
52
|
/** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
|
|
42
53
|
static listSessions(): {
|
|
43
54
|
id: string;
|
package/dist/core/QueryEngine.js
CHANGED
|
@@ -10,16 +10,27 @@ import { setActiveAgent } from '../config/agentState.js';
|
|
|
10
10
|
import { clearAuthorizations } from './safeguard.js';
|
|
11
11
|
import { agentUIBus } from './AgentRegistry.js';
|
|
12
12
|
import { logError, logInfo, logWarn } from './logger.js';
|
|
13
|
-
import { updateUserProfileFromInput } from '../services/userProfile.js';
|
|
13
|
+
import { shouldIncludeUserProfile, updateUserProfileFromInput } from '../services/userProfile.js';
|
|
14
|
+
import { updatePersistentMemoryFromConversation } from '../services/persistentMemory.js';
|
|
15
|
+
import { updateDreamFromSession } from '../services/dream.js';
|
|
16
|
+
const DREAM_IDLE_MIN_MS = 5 * 60 * 1000;
|
|
17
|
+
const DREAM_IDLE_JITTER_MS = 5 * 60 * 1000;
|
|
14
18
|
export class QueryEngine {
|
|
15
19
|
service;
|
|
16
20
|
session;
|
|
17
21
|
transcript = [];
|
|
18
22
|
workerBridge = new WorkerBridge();
|
|
23
|
+
memoryUpdateQueue = Promise.resolve();
|
|
24
|
+
userProfileUpdateQueue = Promise.resolve();
|
|
25
|
+
dreamUpdateQueue = Promise.resolve();
|
|
26
|
+
dreamTimer = null;
|
|
27
|
+
lastActivityAt = Date.now();
|
|
28
|
+
isQueryRunning = false;
|
|
19
29
|
constructor() {
|
|
20
30
|
this.service = this.createService();
|
|
21
31
|
this.session = this.createSession();
|
|
22
32
|
this.ensureSessionDir();
|
|
33
|
+
this.scheduleDreamTimer();
|
|
23
34
|
logInfo('engine.created', {
|
|
24
35
|
sessionId: this.session.id,
|
|
25
36
|
service: this.service.constructor.name,
|
|
@@ -60,6 +71,9 @@ export class QueryEngine {
|
|
|
60
71
|
}
|
|
61
72
|
/** 处理用户输入(在独立 Worker 线程中执行) */
|
|
62
73
|
async handleQuery(userInput, callbacks) {
|
|
74
|
+
this.touchActivity();
|
|
75
|
+
this.isQueryRunning = true;
|
|
76
|
+
const previousTranscriptLength = this.transcript.length;
|
|
63
77
|
logInfo('query.received', {
|
|
64
78
|
sessionId: this.session.id,
|
|
65
79
|
inputLength: userInput.length,
|
|
@@ -74,12 +88,8 @@ export class QueryEngine {
|
|
|
74
88
|
};
|
|
75
89
|
callbacks.onMessage(userMsg);
|
|
76
90
|
this.session.messages.push(userMsg);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (updated) {
|
|
80
|
-
this.service = this.createService();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
91
|
+
const includeUserProfile = shouldIncludeUserProfile(userInput);
|
|
92
|
+
this.scheduleUserProfileUpdate(userInput);
|
|
83
93
|
// 将回调包装后传给 WorkerBridge,Worker 事件会映射回这里
|
|
84
94
|
const bridgeCallbacks = {
|
|
85
95
|
onMessage: (msg) => {
|
|
@@ -104,7 +114,9 @@ export class QueryEngine {
|
|
|
104
114
|
onSubAgentUpdateMessage: callbacks.onSubAgentUpdateMessage,
|
|
105
115
|
};
|
|
106
116
|
try {
|
|
107
|
-
this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks);
|
|
117
|
+
this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks, { includeUserProfile });
|
|
118
|
+
const recentTranscript = this.transcript.slice(previousTranscriptLength);
|
|
119
|
+
this.schedulePersistentMemoryUpdate(userInput, recentTranscript);
|
|
108
120
|
logInfo('query.completed', {
|
|
109
121
|
sessionId: this.session.id,
|
|
110
122
|
transcriptLength: this.transcript.length,
|
|
@@ -122,6 +134,10 @@ export class QueryEngine {
|
|
|
122
134
|
};
|
|
123
135
|
callbacks.onMessage(errMsg);
|
|
124
136
|
}
|
|
137
|
+
finally {
|
|
138
|
+
this.isQueryRunning = false;
|
|
139
|
+
this.scheduleDreamTimer();
|
|
140
|
+
}
|
|
125
141
|
this.session.updatedAt = Date.now();
|
|
126
142
|
callbacks.onSessionUpdate(this.session);
|
|
127
143
|
this.saveSession();
|
|
@@ -137,6 +153,10 @@ export class QueryEngine {
|
|
|
137
153
|
this.saveSession();
|
|
138
154
|
this.session = this.createSession();
|
|
139
155
|
this.transcript = [];
|
|
156
|
+
this.memoryUpdateQueue = Promise.resolve();
|
|
157
|
+
this.userProfileUpdateQueue = Promise.resolve();
|
|
158
|
+
this.dreamUpdateQueue = Promise.resolve();
|
|
159
|
+
this.touchActivity();
|
|
140
160
|
clearAuthorizations();
|
|
141
161
|
}
|
|
142
162
|
/**
|
|
@@ -154,6 +174,10 @@ export class QueryEngine {
|
|
|
154
174
|
this.saveSession();
|
|
155
175
|
this.session = this.createSession();
|
|
156
176
|
this.transcript = [];
|
|
177
|
+
this.memoryUpdateQueue = Promise.resolve();
|
|
178
|
+
this.userProfileUpdateQueue = Promise.resolve();
|
|
179
|
+
this.dreamUpdateQueue = Promise.resolve();
|
|
180
|
+
this.touchActivity();
|
|
157
181
|
logInfo('agent.switch.completed', {
|
|
158
182
|
sessionId: this.session.id,
|
|
159
183
|
agentName,
|
|
@@ -182,6 +206,77 @@ export class QueryEngine {
|
|
|
182
206
|
logError('session.save_failed', error, { sessionId: this.session.id });
|
|
183
207
|
}
|
|
184
208
|
}
|
|
209
|
+
schedulePersistentMemoryUpdate(userInput, recentTranscript) {
|
|
210
|
+
if (!userInput.trim() || recentTranscript.length === 0)
|
|
211
|
+
return;
|
|
212
|
+
const sessionId = this.session.id;
|
|
213
|
+
this.memoryUpdateQueue = this.memoryUpdateQueue
|
|
214
|
+
.catch(() => { })
|
|
215
|
+
.then(async () => {
|
|
216
|
+
await updatePersistentMemoryFromConversation({
|
|
217
|
+
sessionId,
|
|
218
|
+
userInput,
|
|
219
|
+
recentTranscript,
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
scheduleUserProfileUpdate(userInput) {
|
|
224
|
+
if (!userInput.trim())
|
|
225
|
+
return;
|
|
226
|
+
this.userProfileUpdateQueue = this.userProfileUpdateQueue
|
|
227
|
+
.catch(() => { })
|
|
228
|
+
.then(async () => {
|
|
229
|
+
await updateUserProfileFromInput(userInput);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
touchActivity() {
|
|
233
|
+
this.lastActivityAt = Date.now();
|
|
234
|
+
this.scheduleDreamTimer();
|
|
235
|
+
}
|
|
236
|
+
scheduleDreamTimer() {
|
|
237
|
+
if (this.dreamTimer) {
|
|
238
|
+
clearTimeout(this.dreamTimer);
|
|
239
|
+
this.dreamTimer = null;
|
|
240
|
+
}
|
|
241
|
+
const delay = DREAM_IDLE_MIN_MS + Math.floor(Math.random() * DREAM_IDLE_JITTER_MS);
|
|
242
|
+
this.dreamTimer = setTimeout(() => {
|
|
243
|
+
void this.runDreamCycle();
|
|
244
|
+
}, delay);
|
|
245
|
+
}
|
|
246
|
+
async runDreamCycle() {
|
|
247
|
+
const idleMs = Date.now() - this.lastActivityAt;
|
|
248
|
+
if (this.isQueryRunning || idleMs < DREAM_IDLE_MIN_MS) {
|
|
249
|
+
this.scheduleDreamTimer();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const sessionSnapshot = {
|
|
253
|
+
...this.session,
|
|
254
|
+
messages: [...this.session.messages],
|
|
255
|
+
};
|
|
256
|
+
const transcriptSnapshot = [...this.transcript];
|
|
257
|
+
if (transcriptSnapshot.length === 0) {
|
|
258
|
+
this.scheduleDreamTimer();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.dreamUpdateQueue = this.dreamUpdateQueue
|
|
262
|
+
.catch(() => { })
|
|
263
|
+
.then(async () => {
|
|
264
|
+
logInfo('dream.idle_triggered', {
|
|
265
|
+
sessionId: sessionSnapshot.id,
|
|
266
|
+
idleMs,
|
|
267
|
+
transcriptLength: transcriptSnapshot.length,
|
|
268
|
+
});
|
|
269
|
+
await updateDreamFromSession({
|
|
270
|
+
session: sessionSnapshot,
|
|
271
|
+
transcript: transcriptSnapshot,
|
|
272
|
+
});
|
|
273
|
+
})
|
|
274
|
+
.finally(() => {
|
|
275
|
+
if (!this.isQueryRunning && Date.now() - this.lastActivityAt >= DREAM_IDLE_MIN_MS) {
|
|
276
|
+
this.scheduleDreamTimer();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
185
280
|
/** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
|
|
186
281
|
static listSessions() {
|
|
187
282
|
try {
|
|
@@ -256,6 +351,7 @@ export class QueryEngine {
|
|
|
256
351
|
messageCount: cleanedMessages.length,
|
|
257
352
|
transcriptLength: this.transcript.length,
|
|
258
353
|
});
|
|
354
|
+
this.touchActivity();
|
|
259
355
|
return { session: this.session, messages: cleanedMessages };
|
|
260
356
|
}
|
|
261
357
|
catch (error) {
|
|
@@ -3,7 +3,9 @@ import { EngineCallbacks } from './QueryEngine.js';
|
|
|
3
3
|
export declare class WorkerBridge {
|
|
4
4
|
private worker;
|
|
5
5
|
/** 在独立 Worker 线程中执行查询,返回更新后的 transcript */
|
|
6
|
-
run(userInput: string, transcript: TranscriptMessage[], callbacks: EngineCallbacks
|
|
6
|
+
run(userInput: string, transcript: TranscriptMessage[], callbacks: EngineCallbacks, options?: {
|
|
7
|
+
includeUserProfile?: boolean;
|
|
8
|
+
}): Promise<TranscriptMessage[]>;
|
|
7
9
|
/** 向 Worker 发送中断信号 */
|
|
8
10
|
abort(): void;
|
|
9
11
|
}
|
|
@@ -37,7 +37,7 @@ await tsImport(workerData.__workerFile, pathToFileURL(workerData.__workerFile).h
|
|
|
37
37
|
export class WorkerBridge {
|
|
38
38
|
worker = null;
|
|
39
39
|
/** 在独立 Worker 线程中执行查询,返回更新后的 transcript */
|
|
40
|
-
run(userInput, transcript, callbacks) {
|
|
40
|
+
run(userInput, transcript, callbacks, options) {
|
|
41
41
|
return new Promise((resolve, reject) => {
|
|
42
42
|
const workerTsPath = path.join(__dirname, 'queryWorker.ts');
|
|
43
43
|
const worker = createWorker(workerTsPath);
|
|
@@ -174,7 +174,7 @@ export class WorkerBridge {
|
|
|
174
174
|
}
|
|
175
175
|
});
|
|
176
176
|
// 启动执行
|
|
177
|
-
const runMsg = { type: 'run', userInput, transcript };
|
|
177
|
+
const runMsg = { type: 'run', userInput, transcript, options };
|
|
178
178
|
worker.postMessage(runMsg);
|
|
179
179
|
});
|
|
180
180
|
}
|
package/dist/core/query.d.ts
CHANGED
|
@@ -20,12 +20,15 @@ export interface QueryCallbacks {
|
|
|
20
20
|
/** SubAgent 更新已有消息时透传 */
|
|
21
21
|
onSubAgentUpdateMessage?: (id: string, updates: Partial<Message>) => void;
|
|
22
22
|
}
|
|
23
|
+
interface QueryOptions {
|
|
24
|
+
includeUserProfile?: boolean;
|
|
25
|
+
}
|
|
23
26
|
/**
|
|
24
27
|
* 单轮 Agentic Loop:推理 → 工具调用 → 循环
|
|
25
28
|
*/
|
|
26
29
|
export declare function executeQuery(userInput: string, transcript: TranscriptMessage[], _tools: Tool[], service: LLMService, callbacks: QueryCallbacks, abortSignal: {
|
|
27
30
|
aborted: boolean;
|
|
28
|
-
}): Promise<TranscriptMessage[]>;
|
|
31
|
+
}, options?: QueryOptions): Promise<TranscriptMessage[]>;
|
|
29
32
|
/**
|
|
30
33
|
* 直接执行工具(供 Worker 线程调用)
|
|
31
34
|
* 注意:此函数在 Worker 线程中运行,不能使用 callbacks
|
|
@@ -33,3 +36,4 @@ export declare function executeQuery(userInput: string, transcript: TranscriptMe
|
|
|
33
36
|
export declare function runToolDirect(tc: ToolCallInfo, abortSignal: {
|
|
34
37
|
aborted: boolean;
|
|
35
38
|
}): Promise<string>;
|
|
39
|
+
export {};
|
package/dist/core/query.js
CHANGED
|
@@ -74,7 +74,7 @@ function compressTranscript(transcript) {
|
|
|
74
74
|
/**
|
|
75
75
|
* 单轮 Agentic Loop:推理 → 工具调用 → 循环
|
|
76
76
|
*/
|
|
77
|
-
export async function executeQuery(userInput, transcript, _tools, service, callbacks, abortSignal) {
|
|
77
|
+
export async function executeQuery(userInput, transcript, _tools, service, callbacks, abortSignal, options) {
|
|
78
78
|
logInfo('agent_loop.start', {
|
|
79
79
|
inputLength: userInput.length,
|
|
80
80
|
initialTranscriptLength: transcript.length,
|
|
@@ -95,7 +95,7 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
|
|
|
95
95
|
transcriptLength: localTranscript.length,
|
|
96
96
|
});
|
|
97
97
|
callbacks.onLoopStateChange({ ...loopState });
|
|
98
|
-
const result = await runOneIteration(compressTranscript(localTranscript), _tools, service, callbacks, abortSignal);
|
|
98
|
+
const result = await runOneIteration(compressTranscript(localTranscript), _tools, service, callbacks, abortSignal, options);
|
|
99
99
|
logInfo('agent_loop.iteration.result', {
|
|
100
100
|
iteration: loopState.iteration,
|
|
101
101
|
textLength: result.text.length,
|
|
@@ -182,7 +182,7 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
|
|
|
182
182
|
return localTranscript;
|
|
183
183
|
}
|
|
184
184
|
/** 执行一次 LLM 调用 */
|
|
185
|
-
async function runOneIteration(transcript, tools, service, callbacks, abortSignal) {
|
|
185
|
+
async function runOneIteration(transcript, tools, service, callbacks, abortSignal, options) {
|
|
186
186
|
const startTime = Date.now();
|
|
187
187
|
let accumulatedText = '';
|
|
188
188
|
let accumulatedThinking = '';
|
|
@@ -253,7 +253,7 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
|
|
|
253
253
|
},
|
|
254
254
|
onComplete: () => safeResolve(),
|
|
255
255
|
onError: (err) => reject(err),
|
|
256
|
-
}, abortSignal)
|
|
256
|
+
}, abortSignal, options)
|
|
257
257
|
.catch(reject);
|
|
258
258
|
});
|
|
259
259
|
const duration = Date.now() - startTime;
|
package/dist/core/queryWorker.js
CHANGED
|
@@ -98,7 +98,7 @@ parentPort.on('message', async (msg) => {
|
|
|
98
98
|
onSubAgentUpdateMessage: (id, updates) => send({ type: 'subagent_update_message', id, updates }),
|
|
99
99
|
};
|
|
100
100
|
try {
|
|
101
|
-
const newTranscript = await executeQuery(msg.userInput, msg.transcript, getAllTools(), service, callbacks, abortSignal);
|
|
101
|
+
const newTranscript = await executeQuery(msg.userInput, msg.transcript, getAllTools(), service, callbacks, abortSignal, msg.options);
|
|
102
102
|
logInfo('query_worker.run.done', {
|
|
103
103
|
transcriptLength: newTranscript.length,
|
|
104
104
|
});
|
|
@@ -29,8 +29,10 @@ export declare function useSlashMenu(opts: UseSlashMenuOptions): {
|
|
|
29
29
|
resumeMenuMode: boolean;
|
|
30
30
|
handleSlashMenuUp: () => void;
|
|
31
31
|
handleSlashMenuDown: () => void;
|
|
32
|
-
handleSlashMenuSelect: () => void;
|
|
33
32
|
handleSlashMenuClose: () => void;
|
|
33
|
+
getSelectedCommand: () => SlashCommand | null;
|
|
34
|
+
autocompleteSlashMenuSelection: (currentInput: string) => string;
|
|
35
|
+
openListCommand: (commandName: "agent" | "resume") => "/agent " | "/resume ";
|
|
34
36
|
updateSlashMenu: (val: string) => void;
|
|
35
37
|
setSlashMenuVisible: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
36
38
|
resumeSession: (sessionId: string) => void;
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
2
|
import { QueryEngine } from '../core/QueryEngine.js';
|
|
3
3
|
import { filterCommands, filterAgentCommands } from '../commands/index.js';
|
|
4
|
-
import { setActiveAgent } from '../config/agentState.js';
|
|
5
|
-
import { executeSlashCommand } from '../screens/slashCommands.js';
|
|
6
4
|
/**
|
|
7
5
|
* 斜杠命令菜单状态管理 hook
|
|
8
6
|
*
|
|
9
7
|
* 管理菜单可见性、选中项、二级菜单(agent / resume)等。
|
|
10
8
|
*/
|
|
11
9
|
export function useSlashMenu(opts) {
|
|
12
|
-
const { engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, setLoopState, setIsProcessing, setShowWelcome, setInput, stopAll,
|
|
10
|
+
const { engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, setLoopState, setIsProcessing, setShowWelcome, setInput, stopAll, } = opts;
|
|
13
11
|
const [slashMenuVisible, setSlashMenuVisible] = useState(false);
|
|
14
12
|
const [slashMenuItems, setSlashMenuItems] = useState([]);
|
|
15
13
|
const [slashMenuIndex, setSlashMenuIndex] = useState(0);
|
|
@@ -73,84 +71,70 @@ export function useSlashMenu(opts) {
|
|
|
73
71
|
setMessages((prev) => [...prev, errMsg]);
|
|
74
72
|
}
|
|
75
73
|
}, [engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, stopAll, setLoopState, setIsProcessing, setShowWelcome]);
|
|
76
|
-
|
|
77
|
-
const handleSlashMenuSelect = useCallback(() => {
|
|
74
|
+
const getSelectedCommand = useCallback(() => {
|
|
78
75
|
if (slashMenuItems.length === 0)
|
|
79
|
-
return;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (agentMenuMode) {
|
|
85
|
-
setActiveAgent(cmd.name);
|
|
86
|
-
setInput('');
|
|
87
|
-
setSlashMenuVisible(false);
|
|
88
|
-
setAgentMenuMode(false);
|
|
89
|
-
const switchMsg = {
|
|
90
|
-
id: `switch-${Date.now()}`,
|
|
91
|
-
type: 'system',
|
|
92
|
-
status: 'success',
|
|
93
|
-
content: `已切换智能体为 ${cmd.name},请重启以生效(Ctrl+C 两次退出后重新启动)`,
|
|
94
|
-
timestamp: Date.now(),
|
|
95
|
-
};
|
|
96
|
-
setMessages((prev) => [...prev, switchMsg]);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
// 二级 resume 菜单
|
|
100
|
-
if (resumeMenuMode) {
|
|
101
|
-
setInput('');
|
|
102
|
-
setSlashMenuVisible(false);
|
|
103
|
-
setResumeMenuMode(false);
|
|
104
|
-
resumeSession(cmd.name);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
// 一级菜单选中 /agent -> 进入二级菜单
|
|
108
|
-
if (cmd.name === 'agent') {
|
|
76
|
+
return null;
|
|
77
|
+
return slashMenuItems[slashMenuIndex] ?? null;
|
|
78
|
+
}, [slashMenuItems, slashMenuIndex]);
|
|
79
|
+
const openListCommand = useCallback((commandName) => {
|
|
80
|
+
if (commandName === 'agent') {
|
|
109
81
|
setInput('/agent ');
|
|
110
82
|
const matched = filterAgentCommands('');
|
|
111
83
|
setSlashMenuItems(matched);
|
|
112
84
|
setSlashMenuIndex(0);
|
|
113
85
|
setAgentMenuMode(true);
|
|
114
86
|
setResumeMenuMode(false);
|
|
115
|
-
|
|
87
|
+
setSlashMenuVisible(matched.length > 0);
|
|
88
|
+
return '/agent ';
|
|
116
89
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
90
|
+
const sessions = QueryEngine.listSessions().slice(0, 20);
|
|
91
|
+
const items = sessions.map((s) => {
|
|
92
|
+
const date = new Date(s.updatedAt);
|
|
93
|
+
const dateStr = `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
|
94
|
+
return {
|
|
95
|
+
name: s.id,
|
|
96
|
+
description: `[${dateStr}] ${s.summary}`,
|
|
97
|
+
category: 'builtin',
|
|
98
|
+
submitMode: 'action',
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
setInput('/resume ');
|
|
102
|
+
setSlashMenuItems(items);
|
|
103
|
+
setSlashMenuIndex(0);
|
|
104
|
+
setSlashMenuVisible(items.length > 0);
|
|
105
|
+
setResumeMenuMode(true);
|
|
106
|
+
setAgentMenuMode(false);
|
|
107
|
+
return '/resume ';
|
|
108
|
+
}, [setInput]);
|
|
109
|
+
const autocompleteSlashMenuSelection = useCallback((currentInput) => {
|
|
110
|
+
const cmd = getSelectedCommand();
|
|
111
|
+
if (!cmd)
|
|
112
|
+
return currentInput;
|
|
113
|
+
// 二级 agent 菜单
|
|
114
|
+
if (agentMenuMode) {
|
|
115
|
+
const nextInput = `/agent ${cmd.name}`;
|
|
116
|
+
setInput(nextInput);
|
|
117
|
+
setSlashMenuVisible(false);
|
|
118
|
+
return nextInput;
|
|
136
119
|
}
|
|
137
|
-
//
|
|
138
|
-
if (
|
|
139
|
-
|
|
120
|
+
// 二级 resume 菜单
|
|
121
|
+
if (resumeMenuMode) {
|
|
122
|
+
const nextInput = `/resume ${cmd.name}`;
|
|
123
|
+
setInput(nextInput);
|
|
140
124
|
setSlashMenuVisible(false);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
125
|
+
return nextInput;
|
|
126
|
+
}
|
|
127
|
+
const match = currentInput.match(/^\/\S*/);
|
|
128
|
+
const suffix = match ? currentInput.slice(match[0].length) : '';
|
|
129
|
+
const needsTrailingSpace = !suffix && (cmd.submitMode === 'context' || cmd.submitMode === 'list');
|
|
130
|
+
const nextInput = `/${cmd.name}${suffix}${needsTrailingSpace ? ' ' : ''}`;
|
|
131
|
+
setInput(nextInput);
|
|
132
|
+
if (cmd.submitMode === 'list' && (cmd.name === 'agent' || cmd.name === 'resume') && !suffix.trim()) {
|
|
133
|
+
return openListCommand(cmd.name);
|
|
149
134
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}, [slashMenuItems, slashMenuIndex, agentMenuMode, resumeMenuMode, setInput, setMessages, resumeSession]);
|
|
135
|
+
updateSlashMenu(nextInput);
|
|
136
|
+
return nextInput;
|
|
137
|
+
}, [agentMenuMode, resumeMenuMode, getSelectedCommand, setInput, openListCommand]);
|
|
154
138
|
// 输入变化时更新菜单
|
|
155
139
|
const updateSlashMenu = useCallback((val) => {
|
|
156
140
|
if (val.startsWith('/') && !val.includes('\n')) {
|
|
@@ -177,6 +161,7 @@ export function useSlashMenu(opts) {
|
|
|
177
161
|
name: s.id,
|
|
178
162
|
description: `[${dateStr}] ${s.summary}`,
|
|
179
163
|
category: 'builtin',
|
|
164
|
+
submitMode: 'action',
|
|
180
165
|
};
|
|
181
166
|
});
|
|
182
167
|
const matched = subQuery
|
|
@@ -211,8 +196,10 @@ export function useSlashMenu(opts) {
|
|
|
211
196
|
resumeMenuMode,
|
|
212
197
|
handleSlashMenuUp,
|
|
213
198
|
handleSlashMenuDown,
|
|
214
|
-
handleSlashMenuSelect,
|
|
215
199
|
handleSlashMenuClose,
|
|
200
|
+
getSelectedCommand,
|
|
201
|
+
autocompleteSlashMenuSelection,
|
|
202
|
+
openListCommand,
|
|
216
203
|
updateSlashMenu,
|
|
217
204
|
setSlashMenuVisible,
|
|
218
205
|
resumeSession,
|