@code4bug/jarvis-agent 1.0.4 → 1.1.5
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 +67 -0
- package/dist/agents/jarvis.md +11 -0
- package/dist/cli.js +13 -0
- package/dist/components/MessageItem.js +9 -5
- package/dist/components/StatusBar.d.ts +2 -1
- package/dist/components/StatusBar.js +5 -4
- package/dist/config/constants.d.ts +2 -0
- package/dist/config/constants.js +3 -1
- package/dist/config/userProfile.d.ts +4 -0
- package/dist/config/userProfile.js +25 -0
- package/dist/core/AgentMessageBus.d.ts +50 -0
- package/dist/core/AgentMessageBus.js +128 -0
- package/dist/core/AgentRegistry.d.ts +22 -0
- package/dist/core/AgentRegistry.js +16 -0
- package/dist/core/QueryEngine.d.ts +7 -0
- package/dist/core/QueryEngine.js +82 -30
- package/dist/core/SubAgentBridge.d.ts +20 -0
- package/dist/core/SubAgentBridge.js +208 -0
- package/dist/core/WorkerBridge.js +80 -0
- package/dist/core/busAccess.d.ts +9 -0
- package/dist/core/busAccess.js +32 -0
- package/dist/core/logger.d.ts +8 -0
- package/dist/core/logger.js +63 -0
- package/dist/core/query.d.ts +4 -0
- package/dist/core/query.js +169 -4
- package/dist/core/queryWorker.d.ts +62 -0
- package/dist/core/queryWorker.js +46 -0
- package/dist/core/spawnRegistry.d.ts +16 -0
- package/dist/core/spawnRegistry.js +32 -0
- package/dist/core/subAgentWorker.d.ts +89 -0
- package/dist/core/subAgentWorker.js +121 -0
- package/dist/core/workerBusProxy.d.ts +10 -0
- package/dist/core/workerBusProxy.js +57 -0
- package/dist/hooks/useSlashMenu.d.ts +2 -0
- package/dist/hooks/useSlashMenu.js +5 -1
- package/dist/hooks/useTokenDisplay.d.ts +1 -0
- package/dist/hooks/useTokenDisplay.js +5 -0
- package/dist/index.js +2 -0
- package/dist/screens/repl.js +48 -16
- package/dist/screens/slashCommands.js +2 -1
- package/dist/services/api/llm.d.ts +2 -0
- package/dist/services/api/llm.js +23 -1
- package/dist/services/userProfile.d.ts +1 -0
- package/dist/services/userProfile.js +127 -0
- package/dist/tools/index.d.ts +7 -1
- package/dist/tools/index.js +13 -2
- package/dist/tools/publishMessage.d.ts +8 -0
- package/dist/tools/publishMessage.js +41 -0
- package/dist/tools/readChannel.d.ts +8 -0
- package/dist/tools/readChannel.js +44 -0
- package/dist/tools/runAgent.d.ts +11 -0
- package/dist/tools/runAgent.js +111 -0
- package/dist/tools/runCommand.js +16 -0
- package/dist/tools/sendToAgent.d.ts +11 -0
- package/dist/tools/sendToAgent.js +35 -0
- package/dist/tools/spawnAgent.d.ts +6 -0
- package/dist/tools/spawnAgent.js +180 -0
- package/dist/tools/subscribeMessage.d.ts +8 -0
- package/dist/tools/subscribeMessage.js +59 -0
- package/dist/types/index.d.ts +49 -1
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubAgent Worker 线程入口
|
|
3
|
+
*
|
|
4
|
+
* 每个 SubAgent 运行在独立线程中,拥有完整的 react_loop 能力。
|
|
5
|
+
* 通过 parentPort 与主线程(SubAgentBridge)双向通信。
|
|
6
|
+
*
|
|
7
|
+
* 消息协议:
|
|
8
|
+
* 主线程 → Worker:SubAgentInbound
|
|
9
|
+
* Worker → 主线程:SubAgentOutbound
|
|
10
|
+
*/
|
|
11
|
+
import { parentPort } from 'worker_threads';
|
|
12
|
+
import { executeQuery } from './query.js';
|
|
13
|
+
import { getAllTools } from '../tools/index.js';
|
|
14
|
+
import { LLMServiceImpl, fromModelConfig } from '../services/api/llm.js';
|
|
15
|
+
import { MockService } from '../services/api/mock.js';
|
|
16
|
+
import { loadConfig, getActiveModel } from '../config/loader.js';
|
|
17
|
+
import { pendingBusRequests } from './workerBusProxy.js';
|
|
18
|
+
import { logError, logInfo, logWarn } from './logger.js';
|
|
19
|
+
if (!parentPort)
|
|
20
|
+
throw new Error('subAgentWorker must run inside worker_threads');
|
|
21
|
+
logInfo('subagent_worker.ready');
|
|
22
|
+
// ===== 初始化 LLM 服务 =====
|
|
23
|
+
const config = loadConfig();
|
|
24
|
+
const activeModel = getActiveModel(config);
|
|
25
|
+
function buildSubAgentService(role, customSystemPrompt) {
|
|
26
|
+
const systemPrompt = customSystemPrompt
|
|
27
|
+
?? (role
|
|
28
|
+
? `${role}\n\n你是一个专注的子任务执行助手。请严格按照任务指令完成工作,输出结构化结果。`
|
|
29
|
+
: '你是一个专注的子任务执行助手。请严格按照任务指令完成工作,输出结构化结果。');
|
|
30
|
+
if (activeModel) {
|
|
31
|
+
try {
|
|
32
|
+
return new LLMServiceImpl({ ...fromModelConfig(activeModel), systemPrompt });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return new MockService();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return new MockService();
|
|
39
|
+
}
|
|
40
|
+
// ===== 中断信号 =====
|
|
41
|
+
const abortSignal = { aborted: false };
|
|
42
|
+
// ===== 危险命令确认:挂起 Promise,等待主线程回复 =====
|
|
43
|
+
const pendingConfirms = new Map();
|
|
44
|
+
// ===== 监听主线程消息 =====
|
|
45
|
+
parentPort.on('message', async (msg) => {
|
|
46
|
+
if (msg.type === 'abort') {
|
|
47
|
+
abortSignal.aborted = true;
|
|
48
|
+
logWarn('subagent_worker.abort_received');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (msg.type === 'danger_confirm_result') {
|
|
52
|
+
const resolve = pendingConfirms.get(msg.requestId);
|
|
53
|
+
if (resolve) {
|
|
54
|
+
pendingConfirms.delete(msg.requestId);
|
|
55
|
+
resolve(msg.choice);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// MessageBus IPC 回复分发 → 转发给 workerBusProxy 的 pending map
|
|
60
|
+
if (msg.type === 'bus_publish_ack' ||
|
|
61
|
+
msg.type === 'bus_subscribe_result' ||
|
|
62
|
+
msg.type === 'bus_read_history_result' ||
|
|
63
|
+
msg.type === 'bus_get_offset_result' ||
|
|
64
|
+
msg.type === 'bus_list_channels_result') {
|
|
65
|
+
const resolve = pendingBusRequests.get(msg.requestId);
|
|
66
|
+
if (resolve) {
|
|
67
|
+
pendingBusRequests.delete(msg.requestId);
|
|
68
|
+
if (msg.type === 'bus_publish_ack')
|
|
69
|
+
resolve(undefined);
|
|
70
|
+
else if (msg.type === 'bus_subscribe_result')
|
|
71
|
+
resolve(msg.message);
|
|
72
|
+
else if (msg.type === 'bus_read_history_result')
|
|
73
|
+
resolve(msg.messages);
|
|
74
|
+
else if (msg.type === 'bus_get_offset_result')
|
|
75
|
+
resolve(msg.offset);
|
|
76
|
+
else if (msg.type === 'bus_list_channels_result')
|
|
77
|
+
resolve(msg.channels);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (msg.type === 'run') {
|
|
82
|
+
abortSignal.aborted = false;
|
|
83
|
+
const { task } = msg;
|
|
84
|
+
const { taskId, instruction, allowedTools, contextTranscript, role, systemPrompt } = task;
|
|
85
|
+
logInfo('subagent_worker.run.start', {
|
|
86
|
+
taskId,
|
|
87
|
+
inputLength: instruction.length,
|
|
88
|
+
allowedTools,
|
|
89
|
+
transcriptLength: contextTranscript?.length ?? 0,
|
|
90
|
+
});
|
|
91
|
+
const send = (out) => parentPort.postMessage(out);
|
|
92
|
+
const service = buildSubAgentService(role, systemPrompt);
|
|
93
|
+
const tools = getAllTools().filter((t) => !allowedTools || allowedTools.length === 0 || allowedTools.includes(t.name));
|
|
94
|
+
const callbacks = {
|
|
95
|
+
onMessage: (m) => send({ type: 'message', taskId, msg: m }),
|
|
96
|
+
onUpdateMessage: (id, updates) => send({ type: 'update_message', taskId, id, updates }),
|
|
97
|
+
onStreamText: (text) => send({ type: 'stream_text', taskId, text }),
|
|
98
|
+
onClearStreamText: () => { },
|
|
99
|
+
onLoopStateChange: (state) => send({ type: 'loop_state', taskId, state }),
|
|
100
|
+
onConfirmDangerousCommand: (command, reason, ruleName) => {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const requestId = `${Date.now()}-${Math.random()}`;
|
|
103
|
+
pendingConfirms.set(requestId, resolve);
|
|
104
|
+
send({ type: 'danger_confirm_request', taskId, requestId, command, reason, ruleName });
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
const newTranscript = await executeQuery(instruction, contextTranscript ?? [], tools, service, callbacks, abortSignal);
|
|
110
|
+
logInfo('subagent_worker.run.done', {
|
|
111
|
+
taskId,
|
|
112
|
+
transcriptLength: newTranscript.length,
|
|
113
|
+
});
|
|
114
|
+
send({ type: 'done', taskId, transcript: newTranscript });
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
logError('subagent_worker.run.failed', err, { taskId });
|
|
118
|
+
send({ type: 'error', taskId, message: err.message ?? '未知错误' });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BusMessage } from './AgentMessageBus.js';
|
|
2
|
+
/** 等待中的 bus 请求,key = requestId */
|
|
3
|
+
export declare const pendingBusRequests: Map<string, (result: any) => void>;
|
|
4
|
+
export declare const workerBusProxy: {
|
|
5
|
+
publish(from: string, channel: string, payload: string): Promise<void>;
|
|
6
|
+
subscribe(channel: string, timeoutMs?: number, fromOffset?: number): Promise<BusMessage | null>;
|
|
7
|
+
getHistory(channel: string, limit?: number): Promise<BusMessage[]>;
|
|
8
|
+
getOffset(channel: string): Promise<number>;
|
|
9
|
+
listChannels(): Promise<string[]>;
|
|
10
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workerBusProxy — Worker 线程侧 MessageBus IPC 代理
|
|
3
|
+
*
|
|
4
|
+
* 在 SubAgent Worker 线程中,通过 parentPort 消息将 bus 操作
|
|
5
|
+
* 转发给主线程(SubAgentBridge)代理执行,绕过线程内存隔离。
|
|
6
|
+
*
|
|
7
|
+
* 此文件独立于 subAgentWorker.ts,避免循环依赖。
|
|
8
|
+
* subAgentWorker.ts 负责注册 pendingBusRequests 的回复分发。
|
|
9
|
+
*/
|
|
10
|
+
import { parentPort } from 'worker_threads';
|
|
11
|
+
/** 等待中的 bus 请求,key = requestId */
|
|
12
|
+
export const pendingBusRequests = new Map();
|
|
13
|
+
function requestId() {
|
|
14
|
+
return `bus-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
15
|
+
}
|
|
16
|
+
function send(msg) {
|
|
17
|
+
if (!parentPort)
|
|
18
|
+
throw new Error('workerBusProxy: 不在 Worker 线程中');
|
|
19
|
+
parentPort.postMessage(msg);
|
|
20
|
+
}
|
|
21
|
+
export const workerBusProxy = {
|
|
22
|
+
publish(from, channel, payload) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const reqId = requestId();
|
|
25
|
+
pendingBusRequests.set(reqId, () => resolve());
|
|
26
|
+
send({ type: 'bus_publish', requestId: reqId, from, channel, payload });
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
subscribe(channel, timeoutMs = 30_000, fromOffset) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const reqId = requestId();
|
|
32
|
+
pendingBusRequests.set(reqId, (msg) => resolve(msg));
|
|
33
|
+
send({ type: 'bus_subscribe', requestId: reqId, channel, timeoutMs, fromOffset });
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
getHistory(channel, limit) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const reqId = requestId();
|
|
39
|
+
pendingBusRequests.set(reqId, (msgs) => resolve(msgs));
|
|
40
|
+
send({ type: 'bus_read_history', requestId: reqId, channel, limit });
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
getOffset(channel) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const reqId = requestId();
|
|
46
|
+
pendingBusRequests.set(reqId, (offset) => resolve(offset));
|
|
47
|
+
send({ type: 'bus_get_offset', requestId: reqId, channel });
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
listChannels() {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const reqId = requestId();
|
|
53
|
+
pendingBusRequests.set(reqId, (channels) => resolve(channels));
|
|
54
|
+
send({ type: 'bus_list_channels', requestId: reqId });
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -9,7 +9,7 @@ import { executeSlashCommand } from '../screens/slashCommands.js';
|
|
|
9
9
|
* 管理菜单可见性、选中项、二级菜单(agent / resume)等。
|
|
10
10
|
*/
|
|
11
11
|
export function useSlashMenu(opts) {
|
|
12
|
-
const { engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, setLoopState, setIsProcessing, setShowWelcome, setInput, stopAll, } = opts;
|
|
12
|
+
const { engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, setLoopState, setIsProcessing, setShowWelcome, setInput, stopAll, onNewSession, } = opts;
|
|
13
13
|
const [slashMenuVisible, setSlashMenuVisible] = useState(false);
|
|
14
14
|
const [slashMenuItems, setSlashMenuItems] = useState([]);
|
|
15
15
|
const [slashMenuIndex, setSlashMenuIndex] = useState(0);
|
|
@@ -138,6 +138,10 @@ export function useSlashMenu(opts) {
|
|
|
138
138
|
if (cmd.category === 'builtin') {
|
|
139
139
|
setInput('');
|
|
140
140
|
setSlashMenuVisible(false);
|
|
141
|
+
if (cmd.name === 'new') {
|
|
142
|
+
onNewSession();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
141
145
|
const msg = executeSlashCommand(cmd.name);
|
|
142
146
|
if (msg)
|
|
143
147
|
setMessages((prev) => [...prev, msg]);
|
|
@@ -25,6 +25,10 @@ export function useTokenDisplay() {
|
|
|
25
25
|
const updateTokenCount = useCallback((count) => {
|
|
26
26
|
tokenCountRef.current = count;
|
|
27
27
|
}, []);
|
|
28
|
+
const syncTokenDisplay = useCallback((count) => {
|
|
29
|
+
tokenCountRef.current = count;
|
|
30
|
+
setDisplayTokens(count);
|
|
31
|
+
}, []);
|
|
28
32
|
const resetTokens = useCallback(() => {
|
|
29
33
|
tokenCountRef.current = 0;
|
|
30
34
|
setDisplayTokens(0);
|
|
@@ -40,6 +44,7 @@ export function useTokenDisplay() {
|
|
|
40
44
|
startTokenTimer,
|
|
41
45
|
stopTokenTimer,
|
|
42
46
|
updateTokenCount,
|
|
47
|
+
syncTokenDisplay,
|
|
43
48
|
resetTokens,
|
|
44
49
|
};
|
|
45
50
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from 'ink';
|
|
3
|
+
import { logInfo } from './core/logger.js';
|
|
3
4
|
import REPL from './screens/repl.js';
|
|
4
5
|
export function startJarvis() {
|
|
6
|
+
logInfo('app.render.start');
|
|
5
7
|
render(_jsx(REPL, {}), { exitOnCtrlC: false });
|
|
6
8
|
}
|
package/dist/screens/repl.js
CHANGED
|
@@ -20,6 +20,8 @@ import { executeSlashCommand } from './slashCommands.js';
|
|
|
20
20
|
import { QueryEngine } from '../core/QueryEngine.js';
|
|
21
21
|
import { HIDE_WELCOME_AFTER_INPUT } from '../config/constants.js';
|
|
22
22
|
import { generateAgentHint } from '../core/hint.js';
|
|
23
|
+
import { subscribeAgentCount, getActiveAgentCount } from '../core/spawnRegistry.js';
|
|
24
|
+
import { logError, logInfo, logWarn } from '../core/logger.js';
|
|
23
25
|
export default function REPL() {
|
|
24
26
|
const { exit } = useApp();
|
|
25
27
|
const width = useTerminalWidth();
|
|
@@ -33,6 +35,7 @@ export default function REPL() {
|
|
|
33
35
|
const [showWelcome, setShowWelcome] = useState(true);
|
|
34
36
|
const [showDetails, setShowDetails] = useState(false);
|
|
35
37
|
const [placeholder, setPlaceholder] = useState('');
|
|
38
|
+
const [activeAgents, setActiveAgents] = useState(getActiveAgentCount());
|
|
36
39
|
const lastEscRef = useRef(0);
|
|
37
40
|
const sessionRef = useRef({
|
|
38
41
|
id: '', messages: [], createdAt: 0, updatedAt: 0, totalTokens: 0, totalCost: 0,
|
|
@@ -40,23 +43,50 @@ export default function REPL() {
|
|
|
40
43
|
const engineRef = useRef(null);
|
|
41
44
|
// 节流 hooks
|
|
42
45
|
const { streamText, streamBufferRef, startStreamTimer, stopStreamTimer, appendStreamChunk, clearStream, handleThinkingUpdate, finishThinking, thinkingIdRef, stopAll, } = useStreamThrottle(setMessages);
|
|
43
|
-
const { displayTokens, tokenCountRef, startTokenTimer, stopTokenTimer, updateTokenCount, resetTokens, } = useTokenDisplay();
|
|
46
|
+
const { displayTokens, tokenCountRef, startTokenTimer, stopTokenTimer, updateTokenCount, syncTokenDisplay, resetTokens, } = useTokenDisplay();
|
|
47
|
+
// ===== 新会话逻辑 =====
|
|
48
|
+
const handleNewSession = useCallback(() => {
|
|
49
|
+
logInfo('ui.new_session');
|
|
50
|
+
if (engineRef.current) {
|
|
51
|
+
engineRef.current.reset();
|
|
52
|
+
sessionRef.current = engineRef.current.getSession();
|
|
53
|
+
}
|
|
54
|
+
setMessages([]);
|
|
55
|
+
clearStream();
|
|
56
|
+
setLoopState(null);
|
|
57
|
+
setIsProcessing(false);
|
|
58
|
+
setShowWelcome(true);
|
|
59
|
+
resetTokens();
|
|
60
|
+
generateAgentHint().then((hint) => setPlaceholder(hint)).catch(() => { });
|
|
61
|
+
}, [clearStream, resetTokens]);
|
|
44
62
|
// 斜杠菜单
|
|
45
63
|
const slashMenu = useSlashMenu({
|
|
46
64
|
engineRef, sessionRef, tokenCountRef,
|
|
47
65
|
setMessages,
|
|
48
|
-
setDisplayTokens: (n) =>
|
|
66
|
+
setDisplayTokens: (n) => syncTokenDisplay(n),
|
|
49
67
|
setLoopState, setIsProcessing, setShowWelcome, setInput,
|
|
50
68
|
stopAll,
|
|
69
|
+
onNewSession: handleNewSession,
|
|
51
70
|
});
|
|
52
71
|
// 危险命令确认
|
|
53
72
|
const [dangerConfirm, setDangerConfirm] = useState(null);
|
|
54
73
|
useEffect(() => {
|
|
55
74
|
engineRef.current = new QueryEngine();
|
|
56
75
|
sessionRef.current = engineRef.current.getSession();
|
|
76
|
+
// 注册持久 UI 回调,供 spawn_agent 后台子 Agent 跨轮次推送消息
|
|
77
|
+
engineRef.current.registerUIBus((msg) => setMessages((prev) => [...prev, msg]), (id, updates) => setMessages((prev) => prev.map((m) => (m.id === id ? { ...m, ...updates } : m))));
|
|
57
78
|
generateAgentHint().then((hint) => setPlaceholder(hint)).catch((err) => {
|
|
79
|
+
logError('ui.hint.init_failed', err);
|
|
58
80
|
console.error('[hint] 初始化提示失败:', err);
|
|
59
81
|
});
|
|
82
|
+
logInfo('ui.repl.mounted');
|
|
83
|
+
return () => {
|
|
84
|
+
logInfo('ui.repl.unmounted');
|
|
85
|
+
};
|
|
86
|
+
}, []);
|
|
87
|
+
// 订阅后台 SubAgent 计数变化
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
return subscribeAgentCount((count) => setActiveAgents(count));
|
|
60
90
|
}, []);
|
|
61
91
|
// ===== Engine Callbacks =====
|
|
62
92
|
const callbacks = {
|
|
@@ -106,12 +136,22 @@ export default function REPL() {
|
|
|
106
136
|
setDangerConfirm({ command, reason, ruleName, resolve });
|
|
107
137
|
});
|
|
108
138
|
},
|
|
139
|
+
onSubAgentMessage: (msg) => {
|
|
140
|
+
setMessages((prev) => [...prev, msg]);
|
|
141
|
+
},
|
|
142
|
+
onSubAgentUpdateMessage: (id, updates) => {
|
|
143
|
+
setMessages((prev) => prev.map((msg) => (msg.id === id ? { ...msg, ...updates } : msg)));
|
|
144
|
+
},
|
|
109
145
|
};
|
|
110
146
|
// ===== 提交处理 =====
|
|
111
147
|
const handleSubmit = useCallback(async (value) => {
|
|
112
148
|
const trimmed = value.trim();
|
|
113
149
|
if (!trimmed || isProcessing || !engineRef.current)
|
|
114
150
|
return;
|
|
151
|
+
logInfo('ui.submit', {
|
|
152
|
+
inputLength: trimmed.length,
|
|
153
|
+
isSlashCommand: trimmed.startsWith('/'),
|
|
154
|
+
});
|
|
115
155
|
if (trimmed.startsWith('/')) {
|
|
116
156
|
const parts = trimmed.slice(1).split(/\s+/);
|
|
117
157
|
const cmdName = parts[0].toLowerCase();
|
|
@@ -121,18 +161,7 @@ export default function REPL() {
|
|
|
121
161
|
setInput('');
|
|
122
162
|
slashMenu.setSlashMenuVisible(false);
|
|
123
163
|
if (cmdName === 'new') {
|
|
124
|
-
|
|
125
|
-
if (engineRef.current) {
|
|
126
|
-
engineRef.current.reset();
|
|
127
|
-
sessionRef.current = engineRef.current.getSession();
|
|
128
|
-
}
|
|
129
|
-
setMessages([]);
|
|
130
|
-
clearStream();
|
|
131
|
-
setLoopState(null);
|
|
132
|
-
setIsProcessing(false);
|
|
133
|
-
setShowWelcome(true);
|
|
134
|
-
resetTokens();
|
|
135
|
-
generateAgentHint().then((hint) => setPlaceholder(hint)).catch(() => { });
|
|
164
|
+
handleNewSession();
|
|
136
165
|
}
|
|
137
166
|
else if (cmdName === 'session_clear') {
|
|
138
167
|
if (engineRef.current) {
|
|
@@ -228,7 +257,7 @@ export default function REPL() {
|
|
|
228
257
|
setInput('');
|
|
229
258
|
clearStream();
|
|
230
259
|
await engineRef.current.handleQuery(trimmed, callbacks);
|
|
231
|
-
}, [isProcessing, pushHistory, clearStream, resetTokens, slashMenu]);
|
|
260
|
+
}, [isProcessing, pushHistory, clearStream, resetTokens, slashMenu, handleNewSession]);
|
|
232
261
|
// ===== 输入处理 =====
|
|
233
262
|
const handleUpArrow = useCallback(() => {
|
|
234
263
|
const result = navigateUp(input);
|
|
@@ -341,12 +370,15 @@ export default function REPL() {
|
|
|
341
370
|
setShowWelcome(true);
|
|
342
371
|
resetTokens();
|
|
343
372
|
generateAgentHint().then((hint) => setPlaceholder(hint)).catch((err) => {
|
|
373
|
+
logError('ui.hint.reset_failed', err);
|
|
344
374
|
console.error('[hint] 重新生成提示失败:', err);
|
|
345
375
|
});
|
|
376
|
+
logInfo('ui.clear_screen_reset');
|
|
346
377
|
return;
|
|
347
378
|
}
|
|
348
379
|
if (key.escape) {
|
|
349
380
|
if (isProcessing && engineRef.current) {
|
|
381
|
+
logWarn('ui.abort_by_escape');
|
|
350
382
|
engineRef.current.abort();
|
|
351
383
|
}
|
|
352
384
|
else if (input.length > 0) {
|
|
@@ -366,5 +398,5 @@ export default function REPL() {
|
|
|
366
398
|
return (_jsxs(Box, { flexDirection: "column", width: width, children: [showWelcome && _jsx(WelcomeHeader, { width: width }), _jsxs(Box, { flexDirection: "column", paddingX: 1, marginTop: showWelcome ? 0 : 1, children: [messages.map((msg) => (_jsx(MessageItem, { msg: msg, showDetails: showDetails }, msg.id))), streamText && _jsx(StreamingText, { text: streamText }), dangerConfirm && (_jsx(DangerConfirm, { command: dangerConfirm.command, reason: dangerConfirm.reason, ruleName: dangerConfirm.ruleName, onSelect: (choice) => {
|
|
367
399
|
dangerConfirm.resolve(choice);
|
|
368
400
|
setDangerConfirm(null);
|
|
369
|
-
} })), loopState?.isRunning && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "gray", children: [" iteration ", loopState.iteration, "/", loopState.maxIterations] })] }))] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), slashMenu.slashMenuVisible && !isProcessing && (_jsx(SlashCommandMenu, { commands: slashMenu.slashMenuItems, selectedIndex: slashMenu.slashMenuIndex })), _jsx(Box, { children: countdown !== null ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: "Press " }), _jsx(Text, { color: "yellow", bold: true, children: "Ctrl+C" }), _jsx(Text, { color: "yellow", children: " again to exit " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["(", countdown, "s)"] })] })) : isProcessing ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "gray", italic: true, children: " processing..." })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(MultilineInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, placeholder: placeholder, isActive: !isProcessing, showCursor: windowFocused && !isProcessing, slashMenuActive: slashMenu.slashMenuVisible, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: slashMenu.handleSlashMenuSelect, onSlashMenuClose: slashMenu.handleSlashMenuClose, onTabFillPlaceholder: handleTabFillPlaceholder })] })) }), _jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), _jsx(StatusBar, { width: width - 2, totalTokens: displayTokens })] })] }));
|
|
401
|
+
} })), loopState?.isRunning && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "gray", children: [" iteration ", loopState.iteration, "/", loopState.maxIterations] })] }))] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), slashMenu.slashMenuVisible && !isProcessing && (_jsx(SlashCommandMenu, { commands: slashMenu.slashMenuItems, selectedIndex: slashMenu.slashMenuIndex })), _jsx(Box, { children: countdown !== null ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: "Press " }), _jsx(Text, { color: "yellow", bold: true, children: "Ctrl+C" }), _jsx(Text, { color: "yellow", children: " again to exit " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["(", countdown, "s)"] })] })) : isProcessing ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "gray", italic: true, children: " processing..." })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(MultilineInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, placeholder: placeholder, isActive: !isProcessing, showCursor: windowFocused && !isProcessing, slashMenuActive: slashMenu.slashMenuVisible, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: slashMenu.handleSlashMenuSelect, onSlashMenuClose: slashMenu.handleSlashMenuClose, onTabFillPlaceholder: handleTabFillPlaceholder })] })) }), _jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), _jsx(StatusBar, { width: width - 2, totalTokens: displayTokens, activeAgents: activeAgents })] })] }));
|
|
370
402
|
}
|
|
@@ -89,7 +89,8 @@ export function executeSlashCommand(cmdName) {
|
|
|
89
89
|
const parts = [];
|
|
90
90
|
parts.push('### Built-in Tools\n');
|
|
91
91
|
allTools.forEach((t, i) => {
|
|
92
|
-
|
|
92
|
+
const summary = t.description.split('\n')[0].slice(0, 80);
|
|
93
|
+
parts.push(`${i + 1}. \`${t.name}\` - ${summary}`);
|
|
93
94
|
});
|
|
94
95
|
parts.push('');
|
|
95
96
|
parts.push(`### External Skills\n`);
|
|
@@ -16,6 +16,8 @@ export interface LLMConfig {
|
|
|
16
16
|
temperature?: number;
|
|
17
17
|
/** 额外请求体参数,直接合并到 API 请求 body */
|
|
18
18
|
extraBody?: Record<string, unknown>;
|
|
19
|
+
/** 直接指定 system prompt,跳过 agent 文件加载(SubAgent 场景使用) */
|
|
20
|
+
systemPrompt?: string;
|
|
19
21
|
}
|
|
20
22
|
/** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
|
|
21
23
|
export declare function getDefaultConfig(): LLMConfig;
|
package/dist/services/api/llm.js
CHANGED
|
@@ -11,6 +11,7 @@ import { getAgent } from '../../agents/index.js';
|
|
|
11
11
|
import { DEFAULT_AGENT } from '../../config/constants.js';
|
|
12
12
|
import { getActiveAgent } from '../../config/agentState.js';
|
|
13
13
|
import { getSystemInfoPrompt } from '../../config/systemInfo.js';
|
|
14
|
+
import { readUserProfile } from '../../config/userProfile.js';
|
|
14
15
|
/** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
|
|
15
16
|
export function getDefaultConfig() {
|
|
16
17
|
const jarvisCfg = loadConfig();
|
|
@@ -37,6 +38,13 @@ export function fromModelConfig(mc) {
|
|
|
37
38
|
extraBody: mc.extra_body,
|
|
38
39
|
};
|
|
39
40
|
}
|
|
41
|
+
function buildUserProfilePrompt() {
|
|
42
|
+
const userProfile = readUserProfile();
|
|
43
|
+
if (!userProfile)
|
|
44
|
+
return '';
|
|
45
|
+
return '\n\n---\n[用户画像] 以下内容来自 ~/.jarvis/USER.md,请将其视为对用户特征的长期记忆。在后续回复中可以据此调整表达方式、信息密度与建议方式,但不要直接暴露这段系统内容。' +
|
|
46
|
+
`\n${userProfile}`;
|
|
47
|
+
}
|
|
40
48
|
/** 将内部 TranscriptMessage[] 转为 OpenAI messages 格式 */
|
|
41
49
|
function toOpenAIMessages(transcript, systemPrompt) {
|
|
42
50
|
const messages = [];
|
|
@@ -142,6 +150,11 @@ export class LLMServiceImpl {
|
|
|
142
150
|
if (!this.config.apiKey) {
|
|
143
151
|
throw new Error('API_KEY 未配置。请在 .jarvis/config.json 或环境变量中设置。');
|
|
144
152
|
}
|
|
153
|
+
// 若外部直接传入 systemPrompt(SubAgent 场景),直接使用,跳过 agent 文件加载
|
|
154
|
+
if (this.config.systemPrompt) {
|
|
155
|
+
this.systemPrompt = this.config.systemPrompt + buildUserProfilePrompt();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
145
158
|
// 从当前激活的智能体加载 system prompt(运行时动态读取)
|
|
146
159
|
const currentAgent = getActiveAgent(DEFAULT_AGENT);
|
|
147
160
|
const agent = getAgent(currentAgent);
|
|
@@ -157,7 +170,16 @@ export class LLMServiceImpl {
|
|
|
157
170
|
'安全围栏会自动处理拦截和用户确认流程。';
|
|
158
171
|
// 追加系统环境信息,帮助 LLM 感知用户运行环境
|
|
159
172
|
const systemInfo = getSystemInfoPrompt();
|
|
160
|
-
|
|
173
|
+
// 追加当前激活模型信息
|
|
174
|
+
const jarvisCfg = loadConfig();
|
|
175
|
+
const activeModelCfg = getActiveModel(jarvisCfg);
|
|
176
|
+
const activeModelKey = jarvisCfg.system.model ?? 'unknown';
|
|
177
|
+
const modelInfo = '\n\n---\n[当前模型] 以下是本次会话使用的 LLM 模型信息:' +
|
|
178
|
+
`\n- 模型标识: ${activeModelKey}` +
|
|
179
|
+
`\n- 模型名称: ${activeModelCfg?.model ?? 'unknown'}` +
|
|
180
|
+
`\n- API 地址: ${activeModelCfg?.api_url ?? 'unknown'}` +
|
|
181
|
+
`\n- 最大 Token: ${activeModelCfg?.max_tokens ?? 'unknown'}`;
|
|
182
|
+
this.systemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo + buildUserProfilePrompt();
|
|
161
183
|
}
|
|
162
184
|
async streamMessage(transcript, tools, callbacks, abortSignal) {
|
|
163
185
|
const messages = toOpenAIMessages(transcript, this.systemPrompt);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function updateUserProfileFromInput(userInput: string): Promise<boolean>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { loadConfig, getActiveModel } from '../config/loader.js';
|
|
2
|
+
import { readUserProfile, writeUserProfile } from '../config/userProfile.js';
|
|
3
|
+
import { logError, logInfo, logWarn } from '../core/logger.js';
|
|
4
|
+
function extractMessageText(content) {
|
|
5
|
+
if (typeof content === 'string')
|
|
6
|
+
return content;
|
|
7
|
+
if (!Array.isArray(content))
|
|
8
|
+
return '';
|
|
9
|
+
return content
|
|
10
|
+
.map((item) => (item?.type === 'text' ? item.text ?? '' : ''))
|
|
11
|
+
.join('');
|
|
12
|
+
}
|
|
13
|
+
function buildProfilePrompt(userInput, existingProfile) {
|
|
14
|
+
return [
|
|
15
|
+
'请基于“已有用户画像”和“最新用户输入”,整理一份新的 USER.md。',
|
|
16
|
+
'',
|
|
17
|
+
'目标:让后续智能体在系统提示词中读取后,能够更了解用户。',
|
|
18
|
+
'',
|
|
19
|
+
'必须遵守:',
|
|
20
|
+
'1. 只保留对后续交互有帮助的信息。',
|
|
21
|
+
'2. 有明确依据的内容写成确定描述;只有弱信号时写成“可能/倾向于”。',
|
|
22
|
+
'3. 没有依据时明确写“未知”,不要编造职业、年龄、经历。',
|
|
23
|
+
'4. 输出必须是中文 Markdown,不要输出代码块围栏,不要解释你的推理过程。',
|
|
24
|
+
'5. 画像应简洁稳定,避免复述用户原话。',
|
|
25
|
+
'',
|
|
26
|
+
'请严格按下面结构输出:',
|
|
27
|
+
'# 用户画像',
|
|
28
|
+
'## 基本信息',
|
|
29
|
+
'- 职业:',
|
|
30
|
+
'- 年龄阶段:',
|
|
31
|
+
'- 所在地区:',
|
|
32
|
+
'- 语言偏好:',
|
|
33
|
+
'',
|
|
34
|
+
'## 思维与沟通',
|
|
35
|
+
'- 思维习惯:',
|
|
36
|
+
'- 沟通风格:',
|
|
37
|
+
'- 决策偏好:',
|
|
38
|
+
'',
|
|
39
|
+
'## 能力与背景',
|
|
40
|
+
'- 技术背景:',
|
|
41
|
+
'- 专业领域:',
|
|
42
|
+
'- 熟悉工具:',
|
|
43
|
+
'',
|
|
44
|
+
'## 当前关注点',
|
|
45
|
+
'- 长期目标:',
|
|
46
|
+
'- 近期任务倾向:',
|
|
47
|
+
'- 约束与偏好:',
|
|
48
|
+
'',
|
|
49
|
+
'## 交互建议',
|
|
50
|
+
'- 回答策略:',
|
|
51
|
+
'- 需要避免:',
|
|
52
|
+
'',
|
|
53
|
+
'## 置信说明',
|
|
54
|
+
'- 高置信信息:',
|
|
55
|
+
'- 低置信推断:',
|
|
56
|
+
'- 明显未知:',
|
|
57
|
+
'',
|
|
58
|
+
'---',
|
|
59
|
+
'已有用户画像:',
|
|
60
|
+
existingProfile || '(暂无)',
|
|
61
|
+
'',
|
|
62
|
+
'最新用户输入:',
|
|
63
|
+
userInput,
|
|
64
|
+
].join('\n');
|
|
65
|
+
}
|
|
66
|
+
export async function updateUserProfileFromInput(userInput) {
|
|
67
|
+
const normalizedInput = userInput.trim();
|
|
68
|
+
if (!normalizedInput)
|
|
69
|
+
return false;
|
|
70
|
+
const config = loadConfig();
|
|
71
|
+
const activeModel = getActiveModel(config);
|
|
72
|
+
if (!activeModel) {
|
|
73
|
+
logWarn('user_profile.skip.no_active_model');
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const prompt = buildProfilePrompt(normalizedInput, readUserProfile());
|
|
77
|
+
const body = {
|
|
78
|
+
model: activeModel.model,
|
|
79
|
+
messages: [
|
|
80
|
+
{
|
|
81
|
+
role: 'system',
|
|
82
|
+
content: '你是一个严谨的用户画像整理助手,负责维护 ~/.jarvis/USER.md。输出必须是可直接写入文件的 Markdown 正文。',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
role: 'user',
|
|
86
|
+
content: prompt,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
max_tokens: Math.min(activeModel.max_tokens ?? 4096, 1200),
|
|
90
|
+
temperature: 0.2,
|
|
91
|
+
stream: false,
|
|
92
|
+
};
|
|
93
|
+
if (activeModel.extra_body) {
|
|
94
|
+
Object.assign(body, activeModel.extra_body);
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(activeModel.api_url, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
Authorization: `Bearer ${activeModel.api_key}`,
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const errorText = await response.text().catch(() => '');
|
|
107
|
+
throw new Error(`API 错误 ${response.status}: ${errorText.slice(0, 300)}`);
|
|
108
|
+
}
|
|
109
|
+
const data = await response.json();
|
|
110
|
+
const content = extractMessageText(data.choices?.[0]?.message?.content).trim();
|
|
111
|
+
if (!content) {
|
|
112
|
+
throw new Error('用户画像生成结果为空');
|
|
113
|
+
}
|
|
114
|
+
writeUserProfile(content);
|
|
115
|
+
logInfo('user_profile.updated', {
|
|
116
|
+
inputLength: normalizedInput.length,
|
|
117
|
+
outputLength: content.length,
|
|
118
|
+
});
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
logError('user_profile.update_failed', error, {
|
|
123
|
+
inputLength: normalizedInput.length,
|
|
124
|
+
});
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -6,7 +6,13 @@ import { listDirectory } from './listDirectory.js';
|
|
|
6
6
|
import { searchFiles } from './searchFiles.js';
|
|
7
7
|
import { semanticSearch } from './semanticSearch.js';
|
|
8
8
|
import { createSkill } from './createSkill.js';
|
|
9
|
-
|
|
9
|
+
import { runAgent } from './runAgent.js';
|
|
10
|
+
import { spawnAgent } from './spawnAgent.js';
|
|
11
|
+
import { sendToAgent } from './sendToAgent.js';
|
|
12
|
+
import { publishMessage } from './publishMessage.js';
|
|
13
|
+
import { subscribeMessage } from './subscribeMessage.js';
|
|
14
|
+
import { readChannel } from './readChannel.js';
|
|
15
|
+
export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill, runAgent, spawnAgent, sendToAgent, publishMessage, subscribeMessage, readChannel, };
|
|
10
16
|
/** 所有内置工具 */
|
|
11
17
|
export declare const allTools: Tool[];
|
|
12
18
|
/** 按名称查找内置工具 */
|
package/dist/tools/index.js
CHANGED
|
@@ -5,9 +5,20 @@ import { listDirectory } from './listDirectory.js';
|
|
|
5
5
|
import { searchFiles } from './searchFiles.js';
|
|
6
6
|
import { semanticSearch } from './semanticSearch.js';
|
|
7
7
|
import { createSkill } from './createSkill.js';
|
|
8
|
-
|
|
8
|
+
import { runAgent } from './runAgent.js';
|
|
9
|
+
import { spawnAgent } from './spawnAgent.js';
|
|
10
|
+
import { sendToAgent } from './sendToAgent.js';
|
|
11
|
+
import { publishMessage } from './publishMessage.js';
|
|
12
|
+
import { subscribeMessage } from './subscribeMessage.js';
|
|
13
|
+
import { readChannel } from './readChannel.js';
|
|
14
|
+
export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill, runAgent, spawnAgent, sendToAgent, publishMessage, subscribeMessage, readChannel, };
|
|
9
15
|
/** 所有内置工具 */
|
|
10
|
-
export const allTools = [
|
|
16
|
+
export const allTools = [
|
|
17
|
+
readFile, writeFile, runCommand, listDirectory, searchFiles,
|
|
18
|
+
semanticSearch, createSkill,
|
|
19
|
+
runAgent, spawnAgent, sendToAgent,
|
|
20
|
+
publishMessage, subscribeMessage, readChannel,
|
|
21
|
+
];
|
|
11
22
|
/** 按名称查找内置工具 */
|
|
12
23
|
export function findTool(name) {
|
|
13
24
|
return allTools.find((t) => t.name === name);
|