@code4bug/jarvis-agent 1.0.3 → 1.1.4
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 +63 -0
- package/dist/agents/jarvis.md +11 -0
- package/dist/cli.js +2 -2
- package/dist/commands/index.js +2 -2
- package/dist/commands/init.js +1 -1
- package/dist/components/MessageItem.d.ts +1 -1
- package/dist/components/MessageItem.js +10 -6
- package/dist/components/SlashCommandMenu.d.ts +1 -1
- package/dist/components/StatusBar.d.ts +2 -1
- package/dist/components/StatusBar.js +6 -5
- package/dist/components/StreamingText.js +1 -1
- package/dist/components/WelcomeHeader.js +1 -1
- package/dist/config/constants.js +3 -3
- package/dist/core/AgentMessageBus.d.ts +63 -0
- package/dist/core/AgentMessageBus.js +107 -0
- package/dist/core/AgentRegistry.d.ts +22 -0
- package/dist/core/AgentRegistry.js +16 -0
- package/dist/core/QueryEngine.d.ts +7 -1
- package/dist/core/QueryEngine.js +18 -7
- package/dist/core/SubAgentBridge.d.ts +20 -0
- package/dist/core/SubAgentBridge.js +191 -0
- package/dist/core/WorkerBridge.d.ts +2 -2
- package/dist/core/WorkerBridge.js +68 -0
- package/dist/core/busAccess.d.ts +9 -0
- package/dist/core/busAccess.js +32 -0
- package/dist/core/hint.js +4 -4
- package/dist/core/query.d.ts +4 -0
- package/dist/core/query.js +91 -5
- package/dist/core/queryWorker.d.ts +62 -0
- package/dist/core/queryWorker.js +35 -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 +107 -0
- package/dist/core/workerBusProxy.d.ts +10 -0
- package/dist/core/workerBusProxy.js +57 -0
- package/dist/hooks/useSlashMenu.d.ts +7 -5
- package/dist/hooks/useSlashMenu.js +9 -5
- package/dist/hooks/useStreamThrottle.d.ts +1 -1
- package/dist/hooks/useTokenDisplay.d.ts +1 -0
- package/dist/hooks/useTokenDisplay.js +5 -0
- package/dist/index.js +1 -1
- package/dist/screens/repl.js +52 -34
- package/dist/screens/slashCommands.d.ts +1 -1
- package/dist/screens/slashCommands.js +7 -6
- package/dist/services/api/llm.d.ts +4 -2
- package/dist/services/api/llm.js +20 -6
- package/dist/services/api/mock.d.ts +1 -1
- package/dist/skills/index.d.ts +2 -2
- package/dist/skills/index.js +2 -2
- package/dist/tools/createSkill.d.ts +1 -1
- package/dist/tools/createSkill.js +3 -3
- package/dist/tools/index.d.ts +15 -9
- package/dist/tools/index.js +21 -10
- package/dist/tools/listDirectory.d.ts +1 -1
- 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/readFile.d.ts +1 -1
- package/dist/tools/runAgent.d.ts +11 -0
- package/dist/tools/runAgent.js +111 -0
- package/dist/tools/runCommand.d.ts +1 -1
- package/dist/tools/runCommand.js +1 -1
- package/dist/tools/searchFiles.d.ts +1 -1
- package/dist/tools/semanticSearch.d.ts +1 -1
- package/dist/tools/semanticSearch.js +1 -1
- 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 +163 -0
- package/dist/tools/subscribeMessage.d.ts +8 -0
- package/dist/tools/subscribeMessage.js +59 -0
- package/dist/tools/writeFile.d.ts +1 -1
- package/dist/tools/writeFile.js +1 -1
- package/dist/types/index.d.ts +49 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,9 +32,48 @@
|
|
|
32
32
|
- 内置工具系统:文件读写、命令执行、目录浏览、内容搜索
|
|
33
33
|
- 外部 Skill 扩展机制,支持 Python 脚本作为工具
|
|
34
34
|
- 多智能体切换,通过 Markdown 定义智能体人格与能力
|
|
35
|
+
- 多智能体通信,支持智能体之间分工、协同与结果汇总
|
|
35
36
|
- 会话持久化,自动保存历史与 Token 消耗统计
|
|
36
37
|
- 安全围栏,危险命令自动拦截并交互式确认
|
|
37
38
|
- 终端 UI 优化,清晰的状态指示与结构化消息展示
|
|
39
|
+
- **并行任务执行**,多工具调用自动并行,显著提升执行效率
|
|
40
|
+
|
|
41
|
+
## 并行任务
|
|
42
|
+
|
|
43
|
+
当 LLM 在单轮推理中返回多个工具调用时,Jarvis 会自动判断是否可以并行执行,而不是逐个串行等待。
|
|
44
|
+
|
|
45
|
+
### 工作原理
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
LLM 返回多个工具调用
|
|
49
|
+
↓
|
|
50
|
+
canRunInParallel() 判断
|
|
51
|
+
↓
|
|
52
|
+
┌─────────────────────────────┐
|
|
53
|
+
│ 可并行(全部为只读操作) │ → 每个工具在独立 Worker 线程中同时执行
|
|
54
|
+
│ 不可并行(含写操作/Bash) │ → 串行执行,保证顺序与安全
|
|
55
|
+
└─────────────────────────────┘
|
|
56
|
+
↓
|
|
57
|
+
所有结果汇总后统一写入 transcript
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
每个并行工具运行在独立的 Node.js Worker 线程中(`worker_threads`),主线程不阻塞,TUI 实时更新每个工具的执行状态。
|
|
61
|
+
|
|
62
|
+
### 并行条件
|
|
63
|
+
|
|
64
|
+
| 场景 | 是否并行 |
|
|
65
|
+
|------|----------|
|
|
66
|
+
| 多个只读工具(ReadFile / ListDirectory / SearchFiles / SemanticSearch) | 是 |
|
|
67
|
+
| 含任意写操作(WriteFile) | 否 |
|
|
68
|
+
| 含 Bash 命令 | 否(副作用无法静态分析) |
|
|
69
|
+
| 多个只读 Skill | 是 |
|
|
70
|
+
|
|
71
|
+
### 安全保障
|
|
72
|
+
|
|
73
|
+
- 写-写冲突、读-写竞态:通过工具类型静态分析提前规避
|
|
74
|
+
- 并行模式下的危险 Bash 命令:直接跳过并提示(无法弹出交互式确认)
|
|
75
|
+
- 任意工具出错:终止当前轮次,不影响已完成的并行结果
|
|
76
|
+
- 用户按 ESC 中断:所有正在运行的 Worker 均收到中断信号
|
|
38
77
|
|
|
39
78
|
## 快速开始
|
|
40
79
|
|
|
@@ -144,6 +183,30 @@ src/
|
|
|
144
183
|
|
|
145
184
|
运行时通过 `/agent <name>` 切换,选择会持久化到 `~/.jarvis/agent.json`。
|
|
146
185
|
|
|
186
|
+
## 多智能体通信
|
|
187
|
+
|
|
188
|
+
Jarvis 在多智能体能力基础上进一步支持智能体之间的通信与协作,不再局限于单个智能体独立完成任务。
|
|
189
|
+
|
|
190
|
+
这不只是能力的叠加,而是一种协作方式的跃迁。一个智能体擅长理解需求,另一个智能体擅长架构设计,还有智能体专注编码、测试、评审与复盘。当它们开始彼此沟通、相互补位,整个系统就不再只是“一个 AI 在工作”,而像是一支真正持续运转的数字化团队。
|
|
191
|
+
|
|
192
|
+
你可以让不同角色的智能体围绕同一目标协同工作,例如:
|
|
193
|
+
|
|
194
|
+
- 产品经理智能体负责拆解需求、输出任务边界
|
|
195
|
+
- 架构师智能体负责设计技术方案与模块划分
|
|
196
|
+
- 开发智能体负责编码实现与联调
|
|
197
|
+
- 测试智能体负责验证、回归与问题反馈
|
|
198
|
+
|
|
199
|
+
通过多智能体通信机制,多个智能体可以围绕同一上下文进行信息传递、任务接力和结果汇总,从而搭配实现各种多人协作场景构建。
|
|
200
|
+
|
|
201
|
+
从单点问答,到多角色联动;从工具调用,到团队协作流程编排。多智能体带来的不是简单的“多开几个助手”,而是面向复杂任务的组织能力升级,也为产品构建、研发协同、流程自动化打开了近乎无限的可能。
|
|
202
|
+
|
|
203
|
+
适用场景包括:
|
|
204
|
+
|
|
205
|
+
- 需求分析 + 方案设计 + 开发落地的一体化协作流程
|
|
206
|
+
- 前端、后端、测试等不同角色的分工配合
|
|
207
|
+
- 代码评审、缺陷修复、回归验证的闭环协同
|
|
208
|
+
- 模拟真实团队的多人协作交付流程
|
|
209
|
+
|
|
147
210
|
详见 [AGENT_INSTRUCTIONS.md](./AGENT_INSTRUCTIONS.md)。
|
|
148
211
|
|
|
149
212
|
## Skill 扩展
|
package/dist/agents/jarvis.md
CHANGED
|
@@ -33,6 +33,17 @@ vibe: 你的全能助手,有问必答,随时待命。
|
|
|
33
33
|
- 技术选型建议与对比分析
|
|
34
34
|
- API 设计与数据模型规划
|
|
35
35
|
|
|
36
|
+
### 多智能体协作与对话模拟
|
|
37
|
+
- 使用 `start_agent` 异步启动后台子 Agent,立即返回 `task_id`,不阻塞主流程
|
|
38
|
+
- 使用 `send_to_agent` 向指定子 Agent 发送消息(主 Agent → 子 Agent)
|
|
39
|
+
- 使用 `subscribe_message` 订阅 `agent-reply:{task_id}` 频道等待子 Agent 回复
|
|
40
|
+
- 使用 `create_subagent` 启动一次性子任务(阻塞等待结果)
|
|
41
|
+
- 多轮对话模式(如老师-学生):
|
|
42
|
+
1. `start_agent` 启动子 Agent,instruction 中告知其订阅 inbox 并循环回复
|
|
43
|
+
2. `send_to_agent` 发送第一条消息
|
|
44
|
+
3. `subscribe_message` 等待回复,读取后继续发下一条
|
|
45
|
+
4. 发送约定的结束信号(如 `[END]`)终止子 Agent 循环
|
|
46
|
+
|
|
36
47
|
### 脚本执行与自动化
|
|
37
48
|
- 通过 Bash 工具直接执行 Shell 命令和脚本(`Bash(command)`)
|
|
38
49
|
- 支持执行 Python / Node.js / Shell 等脚本文件
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { APP_VERSION } from './config/constants';
|
|
3
|
-
import { startJarvis } from './index';
|
|
2
|
+
import { APP_VERSION } from './config/constants.js';
|
|
3
|
+
import { startJarvis } from './index.js';
|
|
4
4
|
const arg = process.argv[2];
|
|
5
5
|
if (arg === '--version' || arg === '-v' || arg === 'version') {
|
|
6
6
|
console.log(APP_VERSION);
|
package/dist/commands/index.js
CHANGED
|
@@ -26,8 +26,8 @@ const toolCommands = [
|
|
|
26
26
|
{ name: 'create_skill', description: '创建新的 Skill 到 ~/.jarvis/skills/', category: 'builtin' },
|
|
27
27
|
];
|
|
28
28
|
/** 智能体子命令:从 agents 目录动态加载(二级菜单) */
|
|
29
|
-
import { loadAllAgents } from '../agents/index';
|
|
30
|
-
import { listSkills } from '../skills/index';
|
|
29
|
+
import { loadAllAgents } from '../agents/index.js';
|
|
30
|
+
import { listSkills } from '../skills/index.js';
|
|
31
31
|
let _agentSubCommands = null;
|
|
32
32
|
export function getAgentSubCommands() {
|
|
33
33
|
if (_agentSubCommands)
|
package/dist/commands/init.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { execSync } from 'child_process';
|
|
10
|
-
import { APP_NAME, APP_VERSION } from '../config/constants';
|
|
10
|
+
import { APP_NAME, APP_VERSION } from '../config/constants.js';
|
|
11
11
|
// ===== 辅助函数 =====
|
|
12
12
|
/** 安全执行命令 */
|
|
13
13
|
function safeExec(cmd) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import Spinner from 'ink-spinner';
|
|
5
|
-
import MarkdownText from './MarkdownText';
|
|
5
|
+
import MarkdownText from './MarkdownText.js';
|
|
6
6
|
// 状态圆点 icon,根据消息类型 + 状态决定颜色
|
|
7
7
|
// reasoning 完成 → 白色 / tool_exec 成功 → 绿色 / error → 红色 / aborted → 黄色 / pending → 黄色
|
|
8
8
|
function statusDot(status, type) {
|
|
@@ -52,7 +52,7 @@ function MessageItem({ msg, showDetails = false }) {
|
|
|
52
52
|
}
|
|
53
53
|
// thinking 完成后持久化渲染,Ctrl+O 展开/折叠
|
|
54
54
|
if (msg.type === 'thinking' && msg.status === 'success' && msg.think) {
|
|
55
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", children: ['○', " "] }), _jsx(Text, { color: "gray", children: "Thinking" }), _jsxs(Text, { color: "gray", dimColor: true, children: [" (", msg.think.length, " chars)"] }), !showDetails && _jsx(Text, { color: "gray", dimColor: true, children: " [Ctrl+O \u5C55\u5F00]" })] }), showDetails && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(Text, { color: "gray", dimColor: true, wrap: "wrap", children: msg.think }) }))] }));
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", children: ['○', " "] }), msg.subAgentId && (_jsxs(Text, { color: "blue", dimColor: true, children: [msg.subAgentId, " \u203A "] })), _jsx(Text, { color: "gray", children: "Thinking" }), _jsxs(Text, { color: "gray", dimColor: true, children: [" (", msg.think.length, " chars)"] }), !showDetails && _jsx(Text, { color: "gray", dimColor: true, children: " [Ctrl+O \u5C55\u5F00]" })] }), showDetails && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(Text, { color: "gray", dimColor: true, wrap: "wrap", children: msg.think }) }))] }));
|
|
56
56
|
}
|
|
57
57
|
if (msg.type === 'tool_exec') {
|
|
58
58
|
// Bash 工具直接显示命令内容
|
|
@@ -67,16 +67,20 @@ function MessageItem({ msg, showDetails = false }) {
|
|
|
67
67
|
const toolLabel = isBash && bashCmd ? `Bash(${bashCmd})` : (msg.toolName || 'tool');
|
|
68
68
|
// 并行组标识
|
|
69
69
|
const isParallel = !!msg.parallelGroupId;
|
|
70
|
-
|
|
70
|
+
// SubAgent 前缀,例如 "代码审查Agent"
|
|
71
|
+
const subAgentPrefix = msg.subAgentId ? `${msg.subAgentId} - ` : '';
|
|
72
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [isParallel && _jsx(Text, { color: "cyan", dimColor: true, children: "\u21C9 " }), _jsxs(Text, { color: dotColor, children: [dot, " "] }), subAgentPrefix ? (_jsx(Text, { color: "blue", dimColor: true, children: subAgentPrefix })) : null, isBash && bashCmd ? (_jsxs(Text, { children: [_jsx(Text, { color: "white", bold: true, children: "Bash" }), _jsxs(Text, { color: "gray", children: ["(", bashCmd, ")"] })] })) : isSkill ? (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: skillName }), _jsxs(Text, { color: "gray", children: ["(", skillArgsSummary, ")"] })] })) : (_jsx(Text, { color: "magenta", bold: true, children: toolLabel }))] }), showDetails && msg.toolArgs && !isBash && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "gray", dimColor: true, wrap: "wrap", children: JSON.stringify(msg.toolArgs) }) })), showDetails && msg.toolResult && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "gray", wrap: "wrap", children: msg.toolResult.length > 300 ? msg.toolResult.slice(0, 300) + '…' : msg.toolResult }) })), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
|
|
71
73
|
}
|
|
72
74
|
if (msg.type === 'error') {
|
|
73
75
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: "red", children: [_jsx(Text, { color: dotColor, children: dot }), " ", msg.content] }), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
|
|
74
76
|
}
|
|
75
77
|
if (msg.type === 'reasoning') {
|
|
76
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
78
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [msg.subAgentId ? (
|
|
79
|
+
// 有 subAgentId 时:标题行 + 内容分两行,避免内容粘连
|
|
80
|
+
_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsxs(Text, { color: "blue", dimColor: true, children: [msg.subAgentId, " \u203A "] })] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(MarkdownText, { text: msg.content }) })] })) : (_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsx(Box, { flexDirection: "column", flexShrink: 1, children: _jsx(MarkdownText, { text: msg.content }) })] })), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
|
|
77
81
|
}
|
|
78
82
|
if (msg.status !== 'pending' && msg.content) {
|
|
79
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
83
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: dotColor, children: dot }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(MarkdownText, { text: msg.content, color: "gray" }) }), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
|
|
80
84
|
}
|
|
81
85
|
return null;
|
|
82
86
|
}
|
|
@@ -2,7 +2,8 @@ import React from 'react';
|
|
|
2
2
|
interface StatusBarProps {
|
|
3
3
|
width: number;
|
|
4
4
|
totalTokens: number;
|
|
5
|
+
activeAgents?: number;
|
|
5
6
|
}
|
|
6
|
-
declare function StatusBar({ width, totalTokens }: StatusBarProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
declare function StatusBar({ width, totalTokens, activeAgents }: StatusBarProps): import("react/jsx-runtime").JSX.Element;
|
|
7
8
|
declare const _default: React.MemoExoticComponent<typeof StatusBar>;
|
|
8
9
|
export default _default;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import { MODEL_NAME, PROJECT_NAME, ENABLE_THINKING_MODE_TOGGLE, CONTEXT_TOKEN_LIMIT } from '../config/constants';
|
|
4
|
+
import { MODEL_NAME, PROJECT_NAME, ENABLE_THINKING_MODE_TOGGLE, CONTEXT_TOKEN_LIMIT } from '../config/constants.js';
|
|
5
5
|
/** 生成 token 用量进度条 */
|
|
6
6
|
function tokenProgressBar(used, limit, barWidth) {
|
|
7
7
|
const ratio = Math.min(used / limit, 1);
|
|
@@ -11,16 +11,17 @@ function tokenProgressBar(used, limit, barWidth) {
|
|
|
11
11
|
const color = ratio >= 0.9 ? 'red' : ratio >= 0.7 ? 'yellow' : 'green';
|
|
12
12
|
return { bar, color };
|
|
13
13
|
}
|
|
14
|
-
function StatusBar({ width, totalTokens }) {
|
|
14
|
+
function StatusBar({ width, totalTokens, activeAgents = 0 }) {
|
|
15
15
|
const left = ` ${MODEL_NAME} │ ${PROJECT_NAME}`;
|
|
16
|
-
//
|
|
16
|
+
// 右侧:智能体数量(有后台 Agent 时显示)+ token 进度条 + 思考模式切换(可选)
|
|
17
|
+
const agentPart = activeAgents > 0 ? `⬡ ${activeAgents} agent${activeAgents > 1 ? 's' : ''} │ ` : '';
|
|
17
18
|
const tokenLabel = `${totalTokens}/${CONTEXT_TOKEN_LIMIT}`;
|
|
18
19
|
const barWidth = 10;
|
|
19
20
|
const { bar, color } = tokenProgressBar(totalTokens, CONTEXT_TOKEN_LIMIT, barWidth);
|
|
20
21
|
const effortPart = ENABLE_THINKING_MODE_TOGGLE ? ' │ ● medium · /effort' : '';
|
|
21
22
|
// 右侧完整文本长度(用于计算间距)
|
|
22
|
-
const rightLen = tokenLabel.length + 1 + barWidth + effortPart.length + 1;
|
|
23
|
+
const rightLen = agentPart.length + tokenLabel.length + 1 + barWidth + effortPart.length + 1;
|
|
23
24
|
const gap = Math.max(width - left.length - rightLen, 1);
|
|
24
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: left }), _jsx(Text, { children: ' '.repeat(gap) }), _jsxs(Text, { color: "gray", children: [tokenLabel, " "] }), _jsx(Text, { color: color, children: bar }), ENABLE_THINKING_MODE_TOGGLE && _jsx(Text, { color: "gray", children: effortPart }), _jsx(Text, { children: " " })] }));
|
|
25
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: left }), _jsx(Text, { children: ' '.repeat(gap) }), activeAgents > 0 && _jsx(Text, { color: "cyan", children: agentPart }), _jsxs(Text, { color: "gray", children: [tokenLabel, " "] }), _jsx(Text, { color: color, children: bar }), ENABLE_THINKING_MODE_TOGGLE && _jsx(Text, { color: "gray", children: effortPart }), _jsx(Text, { children: " " })] }));
|
|
25
26
|
}
|
|
26
27
|
export default React.memo(StatusBar);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import MarkdownText from './MarkdownText';
|
|
4
|
+
import MarkdownText from './MarkdownText.js';
|
|
5
5
|
function StreamingText({ text }) {
|
|
6
6
|
if (!text)
|
|
7
7
|
return null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import { APP_NAME, APP_VERSION, MODEL_NAME } from '../config/constants';
|
|
4
|
+
import { APP_NAME, APP_VERSION, MODEL_NAME } from '../config/constants.js';
|
|
5
5
|
function truncatePath(p, max) {
|
|
6
6
|
if (p.length <= max)
|
|
7
7
|
return p;
|
package/dist/config/constants.js
CHANGED
|
@@ -24,7 +24,7 @@ export const SESSIONS_DIR = path.join(os.homedir(), '.jarvis', 'sessions');
|
|
|
24
24
|
/** 输入后是否隐藏 WelcomeHeader,默认 false(不隐藏) */
|
|
25
25
|
export const HIDE_WELCOME_AFTER_INPUT = false;
|
|
26
26
|
/** 从配置文件获取当前模型名称 */
|
|
27
|
-
import { loadConfig, getActiveModel } from './loader';
|
|
27
|
+
import { loadConfig, getActiveModel } from './loader.js';
|
|
28
28
|
const _cfg = loadConfig();
|
|
29
29
|
function resolveModelName() {
|
|
30
30
|
try {
|
|
@@ -50,8 +50,8 @@ export const DEFAULT_AGENT_COLOR = 'green';
|
|
|
50
50
|
/** 智能体默认标识符 */
|
|
51
51
|
export const DEFAULT_AGENT_EMOJI = '>';
|
|
52
52
|
// ===== 动态应用名称(跟随激活智能体) =====
|
|
53
|
-
import { getAgent } from '../agents/index';
|
|
54
|
-
import { getActiveAgent } from './agentState';
|
|
53
|
+
import { getAgent } from '../agents/index.js';
|
|
54
|
+
import { getActiveAgent } from './agentState.js';
|
|
55
55
|
/** 当前激活的智能体名称 — 启动时从 ~/.jarvis/agent.json 读取,运行时可切换 */
|
|
56
56
|
export const DEFAULT_AGENT = getActiveAgent(DEFAULT_AGENT_FALLBACK);
|
|
57
57
|
function resolveAppName() {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentMessageBus — SubAgent 间通讯总线(进程内单例)
|
|
3
|
+
*
|
|
4
|
+
* 提供发布/订阅机制,允许:
|
|
5
|
+
* - SubAgent 向命名频道发布消息
|
|
6
|
+
* - SubAgent 订阅频道,等待其他 Agent 发布的消息
|
|
7
|
+
* - 主 Agent 观察所有频道历史
|
|
8
|
+
*
|
|
9
|
+
* 线程安全说明:
|
|
10
|
+
* Node.js Worker 线程之间不共享内存,因此 MessageBus 运行在
|
|
11
|
+
* 主线程(queryWorker)中,SubAgent Worker 通过 IPC 消息与其交互。
|
|
12
|
+
* SubAgentBridge 负责在主线程侧代理 publish/subscribe 请求。
|
|
13
|
+
*/
|
|
14
|
+
export interface BusMessage {
|
|
15
|
+
/** 发布者 Agent 标识 */
|
|
16
|
+
from: string;
|
|
17
|
+
/** 频道名 */
|
|
18
|
+
channel: string;
|
|
19
|
+
/** 消息内容 */
|
|
20
|
+
payload: string;
|
|
21
|
+
/** 发布时间戳 */
|
|
22
|
+
timestamp: number;
|
|
23
|
+
}
|
|
24
|
+
declare class AgentMessageBus {
|
|
25
|
+
/** 频道历史消息,key = channel */
|
|
26
|
+
private history;
|
|
27
|
+
/** 订阅者,key = channel,value = 等待中的 resolve 列表 */
|
|
28
|
+
private waiters;
|
|
29
|
+
/**
|
|
30
|
+
* 向频道发布消息
|
|
31
|
+
* @param from 发布者标识(agentId / taskId)
|
|
32
|
+
* @param channel 频道名
|
|
33
|
+
* @param payload 消息内容
|
|
34
|
+
*/
|
|
35
|
+
publish(from: string, channel: string, payload: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* 订阅频道,返回下一条消息(Promise)
|
|
38
|
+
* - 若 fromOffset 指定,且历史中该 offset 之后已有消息,立即返回(不阻塞)
|
|
39
|
+
* - 否则阻塞等待新消息到达
|
|
40
|
+
* @param channel 频道名
|
|
41
|
+
* @param timeoutMs 超时毫秒,默认 30s,超时返回 null
|
|
42
|
+
* @param fromOffset 从第几条开始消费(0-based),不传则只等新消息
|
|
43
|
+
*/
|
|
44
|
+
subscribe(channel: string, timeoutMs?: number, fromOffset?: number): Promise<BusMessage | null>;
|
|
45
|
+
/**
|
|
46
|
+
* 获取频道当前消息总数(用于记录 offset)
|
|
47
|
+
*/
|
|
48
|
+
getOffset(channel: string): number;
|
|
49
|
+
/**
|
|
50
|
+
* 读取频道历史消息(不阻塞)
|
|
51
|
+
* @param channel 频道名
|
|
52
|
+
* @param limit 最多返回条数,默认全部
|
|
53
|
+
*/
|
|
54
|
+
getHistory(channel: string, limit?: number): BusMessage[];
|
|
55
|
+
/** 列出所有活跃频道 */
|
|
56
|
+
listChannels(): string[];
|
|
57
|
+
/** 清空指定频道(测试用) */
|
|
58
|
+
clearChannel(channel: string): void;
|
|
59
|
+
/** 清空所有频道 */
|
|
60
|
+
clearAll(): void;
|
|
61
|
+
}
|
|
62
|
+
export declare const agentMessageBus: AgentMessageBus;
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentMessageBus — SubAgent 间通讯总线(进程内单例)
|
|
3
|
+
*
|
|
4
|
+
* 提供发布/订阅机制,允许:
|
|
5
|
+
* - SubAgent 向命名频道发布消息
|
|
6
|
+
* - SubAgent 订阅频道,等待其他 Agent 发布的消息
|
|
7
|
+
* - 主 Agent 观察所有频道历史
|
|
8
|
+
*
|
|
9
|
+
* 线程安全说明:
|
|
10
|
+
* Node.js Worker 线程之间不共享内存,因此 MessageBus 运行在
|
|
11
|
+
* 主线程(queryWorker)中,SubAgent Worker 通过 IPC 消息与其交互。
|
|
12
|
+
* SubAgentBridge 负责在主线程侧代理 publish/subscribe 请求。
|
|
13
|
+
*/
|
|
14
|
+
class AgentMessageBus {
|
|
15
|
+
/** 频道历史消息,key = channel */
|
|
16
|
+
history = new Map();
|
|
17
|
+
/** 订阅者,key = channel,value = 等待中的 resolve 列表 */
|
|
18
|
+
waiters = new Map();
|
|
19
|
+
/**
|
|
20
|
+
* 向频道发布消息
|
|
21
|
+
* @param from 发布者标识(agentId / taskId)
|
|
22
|
+
* @param channel 频道名
|
|
23
|
+
* @param payload 消息内容
|
|
24
|
+
*/
|
|
25
|
+
publish(from, channel, payload) {
|
|
26
|
+
const msg = { from, channel, payload, timestamp: Date.now() };
|
|
27
|
+
// 存入历史
|
|
28
|
+
if (!this.history.has(channel))
|
|
29
|
+
this.history.set(channel, []);
|
|
30
|
+
this.history.get(channel).push(msg);
|
|
31
|
+
// 通知等待中的订阅者
|
|
32
|
+
const waiters = this.waiters.get(channel);
|
|
33
|
+
if (waiters && waiters.length > 0) {
|
|
34
|
+
const toNotify = [...waiters];
|
|
35
|
+
// 移除 once 订阅者
|
|
36
|
+
this.waiters.set(channel, waiters.filter((w) => !w.once));
|
|
37
|
+
for (const w of toNotify) {
|
|
38
|
+
w.resolve(msg);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 订阅频道,返回下一条消息(Promise)
|
|
44
|
+
* - 若 fromOffset 指定,且历史中该 offset 之后已有消息,立即返回(不阻塞)
|
|
45
|
+
* - 否则阻塞等待新消息到达
|
|
46
|
+
* @param channel 频道名
|
|
47
|
+
* @param timeoutMs 超时毫秒,默认 30s,超时返回 null
|
|
48
|
+
* @param fromOffset 从第几条开始消费(0-based),不传则只等新消息
|
|
49
|
+
*/
|
|
50
|
+
subscribe(channel, timeoutMs = 30_000, fromOffset) {
|
|
51
|
+
// 如果指定了 offset 且历史中已有该位置之后的消息,立即返回
|
|
52
|
+
if (fromOffset !== undefined) {
|
|
53
|
+
const history = this.history.get(channel) ?? [];
|
|
54
|
+
if (fromOffset < history.length) {
|
|
55
|
+
return Promise.resolve(history[fromOffset]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const timer = setTimeout(() => {
|
|
60
|
+
// 超时:从等待列表移除
|
|
61
|
+
const list = this.waiters.get(channel);
|
|
62
|
+
if (list) {
|
|
63
|
+
this.waiters.set(channel, list.filter((w) => w.resolve !== resolve));
|
|
64
|
+
}
|
|
65
|
+
resolve(null);
|
|
66
|
+
}, timeoutMs);
|
|
67
|
+
const wrappedResolve = (msg) => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
resolve(msg);
|
|
70
|
+
};
|
|
71
|
+
if (!this.waiters.has(channel))
|
|
72
|
+
this.waiters.set(channel, []);
|
|
73
|
+
this.waiters.get(channel).push({ resolve: wrappedResolve, once: true });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 获取频道当前消息总数(用于记录 offset)
|
|
78
|
+
*/
|
|
79
|
+
getOffset(channel) {
|
|
80
|
+
return (this.history.get(channel) ?? []).length;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 读取频道历史消息(不阻塞)
|
|
84
|
+
* @param channel 频道名
|
|
85
|
+
* @param limit 最多返回条数,默认全部
|
|
86
|
+
*/
|
|
87
|
+
getHistory(channel, limit) {
|
|
88
|
+
const msgs = this.history.get(channel) ?? [];
|
|
89
|
+
return limit ? msgs.slice(-limit) : [...msgs];
|
|
90
|
+
}
|
|
91
|
+
/** 列出所有活跃频道 */
|
|
92
|
+
listChannels() {
|
|
93
|
+
return [...this.history.keys()];
|
|
94
|
+
}
|
|
95
|
+
/** 清空指定频道(测试用) */
|
|
96
|
+
clearChannel(channel) {
|
|
97
|
+
this.history.delete(channel);
|
|
98
|
+
this.waiters.delete(channel);
|
|
99
|
+
}
|
|
100
|
+
/** 清空所有频道 */
|
|
101
|
+
clearAll() {
|
|
102
|
+
this.history.clear();
|
|
103
|
+
this.waiters.clear();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// 进程内单例
|
|
107
|
+
export const agentMessageBus = new AgentMessageBus();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentUIBus — 后台子 Agent 向 UI 推送消息的持久通道
|
|
3
|
+
*
|
|
4
|
+
* 解决问题:spawn_agent 启动的后台子 Agent 生命周期跨越多个 queryWorker,
|
|
5
|
+
* 而 toolCallbacks 只在单次 LLM 调用期间有效。
|
|
6
|
+
*
|
|
7
|
+
* 方案:后台子 Agent 的消息通过此模块的全局回调推送,
|
|
8
|
+
* QueryEngine 在初始化时注册回调,生命周期与应用一致。
|
|
9
|
+
*/
|
|
10
|
+
import { Message } from '../types/index.js';
|
|
11
|
+
type UIMessageCallback = (msg: Message) => void;
|
|
12
|
+
type UIUpdateCallback = (id: string, updates: Partial<Message>) => void;
|
|
13
|
+
declare class AgentUIBus {
|
|
14
|
+
private onMessage;
|
|
15
|
+
private onUpdateMessage;
|
|
16
|
+
/** QueryEngine 初始化时注册,生命周期与应用一致 */
|
|
17
|
+
register(onMessage: UIMessageCallback, onUpdateMessage: UIUpdateCallback): void;
|
|
18
|
+
push(msg: Message): void;
|
|
19
|
+
update(id: string, updates: Partial<Message>): void;
|
|
20
|
+
}
|
|
21
|
+
export declare const agentUIBus: AgentUIBus;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class AgentUIBus {
|
|
2
|
+
onMessage = null;
|
|
3
|
+
onUpdateMessage = null;
|
|
4
|
+
/** QueryEngine 初始化时注册,生命周期与应用一致 */
|
|
5
|
+
register(onMessage, onUpdateMessage) {
|
|
6
|
+
this.onMessage = onMessage;
|
|
7
|
+
this.onUpdateMessage = onUpdateMessage;
|
|
8
|
+
}
|
|
9
|
+
push(msg) {
|
|
10
|
+
this.onMessage?.(msg);
|
|
11
|
+
}
|
|
12
|
+
update(id, updates) {
|
|
13
|
+
this.onUpdateMessage?.(id, updates);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export const agentUIBus = new AgentUIBus();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Message, Session, LoopState } from '../types/index.js';
|
|
2
|
-
import { DangerConfirmResult } from './query';
|
|
2
|
+
import { DangerConfirmResult } from './query.js';
|
|
3
3
|
export interface EngineCallbacks {
|
|
4
4
|
onMessage: (msg: Message) => void;
|
|
5
5
|
onUpdateMessage: (id: string, updates: Partial<Message>) => void;
|
|
@@ -10,6 +10,10 @@ export interface EngineCallbacks {
|
|
|
10
10
|
onSessionUpdate: (session: Session) => void;
|
|
11
11
|
/** 危险命令交互式确认 */
|
|
12
12
|
onConfirmDangerousCommand?: (command: string, reason: string, ruleName: string) => Promise<DangerConfirmResult>;
|
|
13
|
+
/** SubAgent 产生消息时推送到 UI(带 subAgentId 前缀) */
|
|
14
|
+
onSubAgentMessage?: (msg: Message) => void;
|
|
15
|
+
/** SubAgent 更新已有消息 */
|
|
16
|
+
onSubAgentUpdateMessage?: (id: string, updates: Partial<Message>) => void;
|
|
13
17
|
}
|
|
14
18
|
export declare class QueryEngine {
|
|
15
19
|
private service;
|
|
@@ -17,6 +21,8 @@ export declare class QueryEngine {
|
|
|
17
21
|
private transcript;
|
|
18
22
|
private workerBridge;
|
|
19
23
|
constructor();
|
|
24
|
+
/** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
|
|
25
|
+
registerUIBus(onMessage: (msg: Message) => void, onUpdateMessage: (id: string, updates: Partial<Message>) => void): void;
|
|
20
26
|
private createSession;
|
|
21
27
|
private ensureSessionDir;
|
|
22
28
|
/** 处理用户输入(在独立 Worker 线程中执行) */
|
package/dist/core/QueryEngine.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { v4 as uuid } from 'uuid';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { WorkerBridge } from './WorkerBridge';
|
|
5
|
-
import { MockService } from '../services/api/mock';
|
|
6
|
-
import { LLMServiceImpl } from '../services/api/llm';
|
|
7
|
-
import { loadConfig, getActiveModel } from '../config/loader';
|
|
8
|
-
import { SESSIONS_DIR } from '../config/constants';
|
|
9
|
-
import { setActiveAgent } from '../config/agentState';
|
|
10
|
-
import { clearAuthorizations } from './safeguard';
|
|
4
|
+
import { WorkerBridge } from './WorkerBridge.js';
|
|
5
|
+
import { MockService } from '../services/api/mock.js';
|
|
6
|
+
import { LLMServiceImpl } from '../services/api/llm.js';
|
|
7
|
+
import { loadConfig, getActiveModel } from '../config/loader.js';
|
|
8
|
+
import { SESSIONS_DIR } from '../config/constants.js';
|
|
9
|
+
import { setActiveAgent } from '../config/agentState.js';
|
|
10
|
+
import { clearAuthorizations } from './safeguard.js';
|
|
11
|
+
import { agentUIBus } from './AgentRegistry.js';
|
|
11
12
|
export class QueryEngine {
|
|
12
13
|
service;
|
|
13
14
|
session;
|
|
@@ -31,6 +32,10 @@ export class QueryEngine {
|
|
|
31
32
|
this.session = this.createSession();
|
|
32
33
|
this.ensureSessionDir();
|
|
33
34
|
}
|
|
35
|
+
/** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
|
|
36
|
+
registerUIBus(onMessage, onUpdateMessage) {
|
|
37
|
+
agentUIBus.register(onMessage, onUpdateMessage);
|
|
38
|
+
}
|
|
34
39
|
createSession() {
|
|
35
40
|
return {
|
|
36
41
|
id: uuid(),
|
|
@@ -73,6 +78,12 @@ export class QueryEngine {
|
|
|
73
78
|
onLoopStateChange: callbacks.onLoopStateChange,
|
|
74
79
|
onSessionUpdate: callbacks.onSessionUpdate,
|
|
75
80
|
onConfirmDangerousCommand: callbacks.onConfirmDangerousCommand,
|
|
81
|
+
onSubAgentMessage: (msg) => {
|
|
82
|
+
// SubAgent 消息也存入会话,方便持久化
|
|
83
|
+
this.session.messages.push(msg);
|
|
84
|
+
callbacks.onSubAgentMessage?.(msg);
|
|
85
|
+
},
|
|
86
|
+
onSubAgentUpdateMessage: callbacks.onSubAgentUpdateMessage,
|
|
76
87
|
};
|
|
77
88
|
try {
|
|
78
89
|
this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Message, LoopState, SubAgentTask, SubAgentResult } from '../types/index.js';
|
|
2
|
+
import { DangerConfirmResult } from './query.js';
|
|
3
|
+
export interface SubAgentBridgeCallbacks {
|
|
4
|
+
onMessage: (taskId: string, msg: Message) => void;
|
|
5
|
+
onUpdateMessage: (taskId: string, id: string, updates: Partial<Message>) => void;
|
|
6
|
+
onStreamText: (taskId: string, text: string) => void;
|
|
7
|
+
onLoopStateChange: (taskId: string, state: LoopState) => void;
|
|
8
|
+
/** 危险命令确认,委托给主线程 UI */
|
|
9
|
+
onConfirmDangerousCommand?: (taskId: string, command: string, reason: string, ruleName: string) => Promise<DangerConfirmResult>;
|
|
10
|
+
}
|
|
11
|
+
export declare class SubAgentBridge {
|
|
12
|
+
private worker;
|
|
13
|
+
/**
|
|
14
|
+
* 在独立 Worker 线程中执行 SubAgent 任务
|
|
15
|
+
* @returns SubAgentResult(包含最终输出、消息列表、transcript)
|
|
16
|
+
*/
|
|
17
|
+
run(task: SubAgentTask, callbacks: SubAgentBridgeCallbacks): Promise<SubAgentResult>;
|
|
18
|
+
/** 中断 SubAgent 执行 */
|
|
19
|
+
abort(): void;
|
|
20
|
+
}
|