@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.
Files changed (61) hide show
  1. package/README.md +67 -0
  2. package/dist/agents/jarvis.md +11 -0
  3. package/dist/cli.js +13 -0
  4. package/dist/components/MessageItem.js +9 -5
  5. package/dist/components/StatusBar.d.ts +2 -1
  6. package/dist/components/StatusBar.js +5 -4
  7. package/dist/config/constants.d.ts +2 -0
  8. package/dist/config/constants.js +3 -1
  9. package/dist/config/userProfile.d.ts +4 -0
  10. package/dist/config/userProfile.js +25 -0
  11. package/dist/core/AgentMessageBus.d.ts +50 -0
  12. package/dist/core/AgentMessageBus.js +128 -0
  13. package/dist/core/AgentRegistry.d.ts +22 -0
  14. package/dist/core/AgentRegistry.js +16 -0
  15. package/dist/core/QueryEngine.d.ts +7 -0
  16. package/dist/core/QueryEngine.js +82 -30
  17. package/dist/core/SubAgentBridge.d.ts +20 -0
  18. package/dist/core/SubAgentBridge.js +208 -0
  19. package/dist/core/WorkerBridge.js +80 -0
  20. package/dist/core/busAccess.d.ts +9 -0
  21. package/dist/core/busAccess.js +32 -0
  22. package/dist/core/logger.d.ts +8 -0
  23. package/dist/core/logger.js +63 -0
  24. package/dist/core/query.d.ts +4 -0
  25. package/dist/core/query.js +169 -4
  26. package/dist/core/queryWorker.d.ts +62 -0
  27. package/dist/core/queryWorker.js +46 -0
  28. package/dist/core/spawnRegistry.d.ts +16 -0
  29. package/dist/core/spawnRegistry.js +32 -0
  30. package/dist/core/subAgentWorker.d.ts +89 -0
  31. package/dist/core/subAgentWorker.js +121 -0
  32. package/dist/core/workerBusProxy.d.ts +10 -0
  33. package/dist/core/workerBusProxy.js +57 -0
  34. package/dist/hooks/useSlashMenu.d.ts +2 -0
  35. package/dist/hooks/useSlashMenu.js +5 -1
  36. package/dist/hooks/useTokenDisplay.d.ts +1 -0
  37. package/dist/hooks/useTokenDisplay.js +5 -0
  38. package/dist/index.js +2 -0
  39. package/dist/screens/repl.js +48 -16
  40. package/dist/screens/slashCommands.js +2 -1
  41. package/dist/services/api/llm.d.ts +2 -0
  42. package/dist/services/api/llm.js +23 -1
  43. package/dist/services/userProfile.d.ts +1 -0
  44. package/dist/services/userProfile.js +127 -0
  45. package/dist/tools/index.d.ts +7 -1
  46. package/dist/tools/index.js +13 -2
  47. package/dist/tools/publishMessage.d.ts +8 -0
  48. package/dist/tools/publishMessage.js +41 -0
  49. package/dist/tools/readChannel.d.ts +8 -0
  50. package/dist/tools/readChannel.js +44 -0
  51. package/dist/tools/runAgent.d.ts +11 -0
  52. package/dist/tools/runAgent.js +111 -0
  53. package/dist/tools/runCommand.js +16 -0
  54. package/dist/tools/sendToAgent.d.ts +11 -0
  55. package/dist/tools/sendToAgent.js +35 -0
  56. package/dist/tools/spawnAgent.d.ts +6 -0
  57. package/dist/tools/spawnAgent.js +180 -0
  58. package/dist/tools/subscribeMessage.d.ts +8 -0
  59. package/dist/tools/subscribeMessage.js +59 -0
  60. package/dist/types/index.d.ts +49 -1
  61. 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
+ };
@@ -13,6 +13,8 @@ interface UseSlashMenuOptions {
13
13
  setInput: React.Dispatch<React.SetStateAction<string>>;
14
14
  /** resume 时重置所有流式状态(含 thinkingIdRef) */
15
15
  stopAll: () => void;
16
+ /** /new 命令:开启新会话 */
17
+ onNewSession: () => void;
16
18
  }
17
19
  /**
18
20
  * 斜杠命令菜单状态管理 hook
@@ -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]);
@@ -9,5 +9,6 @@ export declare function useTokenDisplay(): {
9
9
  startTokenTimer: () => void;
10
10
  stopTokenTimer: () => void;
11
11
  updateTokenCount: (count: number) => void;
12
+ syncTokenDisplay: (count: number) => void;
12
13
  resetTokens: () => void;
13
14
  };
@@ -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
  }
@@ -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) => updateTokenCount(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
- parts.push(`${i + 1}. \`${t.name}\` - ${t.description.slice(0, 60)}`);
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;
@@ -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
- this.systemPrompt = agentPrompt + roleBoundary + systemInfo;
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
+ }
@@ -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
- export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill };
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
  /** 按名称查找内置工具 */
@@ -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
- export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill };
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 = [readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill];
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);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * publish_message 工具
3
+ *
4
+ * 允许 SubAgent(或主 Agent)向命名频道发布消息,
5
+ * 其他正在订阅该频道的 Agent 会立即收到通知。
6
+ */
7
+ import { Tool } from '../types/index.js';
8
+ export declare const publishMessage: Tool;