merge: resolve conflict with main (customTools + maxToolOutputChars)
This commit is contained in:
commit
10337f88ab
48
README.md
48
README.md
|
|
@ -17,7 +17,7 @@ CrewAI is Python. LangGraph makes you draw the graph by hand. `open-multi-agent`
|
||||||
|
|
||||||
- **Goal to result in one call.** `runTeam(team, "Build a REST API")` kicks off a coordinator agent that decomposes the goal into a task DAG, resolves dependencies, runs independent tasks in parallel, and synthesizes the final output. No graph to draw, no tasks to wire up.
|
- **Goal to result in one call.** `runTeam(team, "Build a REST API")` kicks off a coordinator agent that decomposes the goal into a task DAG, resolves dependencies, runs independent tasks in parallel, and synthesizes the final output. No graph to draw, no tasks to wire up.
|
||||||
- **TypeScript-native, three runtime dependencies.** `@anthropic-ai/sdk`, `openai`, `zod`. That is the whole runtime. Embed in Express, Next.js, serverless functions, or CI/CD pipelines. No Python runtime, no subprocess bridge, no cloud sidecar.
|
- **TypeScript-native, three runtime dependencies.** `@anthropic-ai/sdk`, `openai`, `zod`. That is the whole runtime. Embed in Express, Next.js, serverless functions, or CI/CD pipelines. No Python runtime, no subprocess bridge, no cloud sidecar.
|
||||||
- **Multi-model teams.** Claude, GPT, Gemini, Grok, Copilot, or any OpenAI-compatible local model (Ollama, vLLM, LM Studio, llama.cpp) in the same team. Run the architect on Opus 4.6, the developer on GPT-5.4, the reviewer on local Gemma 4, all in one `runTeam()` call. Gemini ships as an optional peer dependency: `npm install @google/genai` to enable.
|
- **Multi-model teams.** Claude, GPT, Gemini, Grok, MiniMax, DeepSeek, Copilot, or any OpenAI-compatible local model (Ollama, vLLM, LM Studio, llama.cpp) in the same team. Run the architect on Opus 4.6, the developer on GPT-5.4, the reviewer on local Gemma 4, all in one `runTeam()` call. Gemini ships as an optional peer dependency: `npm install @google/genai` to enable.
|
||||||
|
|
||||||
Other features (MCP integration, context strategies, structured output, task retry, human-in-the-loop, lifecycle hooks, loop detection, observability) live below the fold and in [`examples/`](./examples/).
|
Other features (MCP integration, context strategies, structured output, task retry, human-in-the-loop, lifecycle hooks, loop detection, observability) live below the fold and in [`examples/`](./examples/).
|
||||||
|
|
||||||
|
|
@ -72,6 +72,9 @@ Set the API key for your provider. Local models via Ollama require no API key
|
||||||
- `OPENAI_API_KEY`
|
- `OPENAI_API_KEY`
|
||||||
- `GEMINI_API_KEY`
|
- `GEMINI_API_KEY`
|
||||||
- `XAI_API_KEY` (for Grok)
|
- `XAI_API_KEY` (for Grok)
|
||||||
|
- `MINIMAX_API_KEY` (for MiniMax)
|
||||||
|
- `MINIMAX_BASE_URL` (for MiniMax — optional, selects endpoint)
|
||||||
|
- `DEEPSEEK_API_KEY` (for DeepSeek)
|
||||||
- `GITHUB_TOKEN` (for Copilot)
|
- `GITHUB_TOKEN` (for Copilot)
|
||||||
|
|
||||||
**CLI (`oma`).** For shell and CI, the package exposes a JSON-first binary. See [docs/cli.md](./docs/cli.md) for `oma run`, `oma task`, `oma provider`, exit codes, and file formats.
|
**CLI (`oma`).** For shell and CI, the package exposes a JSON-first binary. See [docs/cli.md](./docs/cli.md) for `oma run`, `oma task`, `oma provider`, exit codes, and file formats.
|
||||||
|
|
@ -139,14 +142,17 @@ For MapReduce-style fan-out without task dependencies, use `AgentPool.runParalle
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
16 runnable scripts in [`examples/`](./examples/). Start with these four:
|
18 runnable scripts and 1 full-stack demo in [`examples/`](./examples/). Start with these:
|
||||||
|
|
||||||
- [02 — Team Collaboration](examples/02-team-collaboration.ts): `runTeam()` coordinator pattern.
|
- [02 — Team Collaboration](examples/02-team-collaboration.ts): `runTeam()` coordinator pattern.
|
||||||
- [06 — Local Model](examples/06-local-model.ts): Ollama and Claude in one pipeline via `baseURL`.
|
- [06 — Local Model](examples/06-local-model.ts): Ollama and Claude in one pipeline via `baseURL`.
|
||||||
- [09 — Structured Output](examples/09-structured-output.ts): any agent returns Zod-validated JSON.
|
- [09 — Structured Output](examples/09-structured-output.ts): any agent returns Zod-validated JSON.
|
||||||
- [11 — Trace Observability](examples/11-trace-observability.ts): `onTrace` spans for LLM calls, tools, and tasks.
|
- [11 — Trace Observability](examples/11-trace-observability.ts): `onTrace` spans for LLM calls, tools, and tasks.
|
||||||
|
- [17 — MiniMax](examples/17-minimax.ts): three-agent team using MiniMax M2.7.
|
||||||
|
- [18 — DeepSeek](examples/18-deepseek.ts): three-agent team using DeepSeek Chat.
|
||||||
|
- [with-vercel-ai-sdk](examples/with-vercel-ai-sdk/): Next.js app — OMA `runTeam()` + AI SDK `useChat` streaming.
|
||||||
|
|
||||||
Run any with `npx tsx examples/02-team-collaboration.ts`.
|
Run scripts with `npx tsx examples/02-team-collaboration.ts`.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|
@ -182,6 +188,8 @@ Run any with `npx tsx examples/02-team-collaboration.ts`.
|
||||||
│ │ - CopilotAdapter │
|
│ │ - CopilotAdapter │
|
||||||
│ │ - GeminiAdapter │
|
│ │ - GeminiAdapter │
|
||||||
│ │ - GrokAdapter │
|
│ │ - GrokAdapter │
|
||||||
|
│ │ - MiniMaxAdapter │
|
||||||
|
│ │ - DeepSeekAdapter │
|
||||||
│ └──────────────────────┘
|
│ └──────────────────────┘
|
||||||
┌────────▼──────────┐
|
┌────────▼──────────┐
|
||||||
│ AgentRunner │ ┌──────────────────────┐
|
│ AgentRunner │ ┌──────────────────────┐
|
||||||
|
|
@ -281,6 +289,9 @@ Notes:
|
||||||
| Anthropic (Claude) | `provider: 'anthropic'` | `ANTHROPIC_API_KEY` | Verified |
|
| Anthropic (Claude) | `provider: 'anthropic'` | `ANTHROPIC_API_KEY` | Verified |
|
||||||
| OpenAI (GPT) | `provider: 'openai'` | `OPENAI_API_KEY` | Verified |
|
| OpenAI (GPT) | `provider: 'openai'` | `OPENAI_API_KEY` | Verified |
|
||||||
| Grok (xAI) | `provider: 'grok'` | `XAI_API_KEY` | Verified |
|
| Grok (xAI) | `provider: 'grok'` | `XAI_API_KEY` | Verified |
|
||||||
|
| MiniMax (global) | `provider: 'minimax'` | `MINIMAX_API_KEY` | Verified |
|
||||||
|
| MiniMax (China) | `provider: 'minimax'` + `MINIMAX_BASE_URL` | `MINIMAX_API_KEY` | Verified |
|
||||||
|
| DeepSeek | `provider: 'deepseek'` | `DEEPSEEK_API_KEY` | Verified |
|
||||||
| GitHub Copilot | `provider: 'copilot'` | `GITHUB_TOKEN` | Verified |
|
| GitHub Copilot | `provider: 'copilot'` | `GITHUB_TOKEN` | Verified |
|
||||||
| Gemini | `provider: 'gemini'` | `GEMINI_API_KEY` | Verified |
|
| Gemini | `provider: 'gemini'` | `GEMINI_API_KEY` | Verified |
|
||||||
| Ollama / vLLM / LM Studio | `provider: 'openai'` + `baseURL` | — | Verified |
|
| Ollama / vLLM / LM Studio | `provider: 'openai'` + `baseURL` | — | Verified |
|
||||||
|
|
@ -290,7 +301,7 @@ Gemini requires `npm install @google/genai` (optional peer dependency).
|
||||||
|
|
||||||
Verified local models with tool-calling: **Gemma 4** (see [example 08](examples/08-gemma4-local.ts)).
|
Verified local models with tool-calling: **Gemma 4** (see [example 08](examples/08-gemma4-local.ts)).
|
||||||
|
|
||||||
Any OpenAI-compatible API should work via `provider: 'openai'` + `baseURL` (DeepSeek, Groq, Mistral, Qwen, MiniMax, etc.). **Grok now has first-class support** via `provider: 'grok'`.
|
Any OpenAI-compatible API should work via `provider: 'openai'` + `baseURL` (Groq, Mistral, Qwen, etc.). **Grok, MiniMax, and DeepSeek now have first-class support** via `provider: 'grok'`, `provider: 'minimax'`, and `provider: 'deepseek'`.
|
||||||
|
|
||||||
### Local Model Tool-Calling
|
### Local Model Tool-Calling
|
||||||
|
|
||||||
|
|
@ -330,7 +341,34 @@ const grokAgent: AgentConfig = {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
(Set your `XAI_API_KEY` environment variable — no `baseURL` needed anymore.)
|
(Set your `XAI_API_KEY` environment variable — no `baseURL` needed.)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const minimaxAgent: AgentConfig = {
|
||||||
|
name: 'minimax-agent',
|
||||||
|
provider: 'minimax',
|
||||||
|
model: 'MiniMax-M2.7',
|
||||||
|
systemPrompt: 'You are a helpful assistant.',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `MINIMAX_API_KEY`. The adapter selects the endpoint via `MINIMAX_BASE_URL`:
|
||||||
|
|
||||||
|
- `https://api.minimax.io/v1` Global, default
|
||||||
|
- `https://api.minimaxi.com/v1` China mainland endpoint
|
||||||
|
|
||||||
|
You can also pass `baseURL` directly in `AgentConfig` to override the env var.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const deepseekAgent: AgentConfig = {
|
||||||
|
name: 'deepseek-agent',
|
||||||
|
provider: 'deepseek',
|
||||||
|
model: 'deepseek-chat',
|
||||||
|
systemPrompt: 'You are a helpful assistant.',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `DEEPSEEK_API_KEY`. Available models: `deepseek-chat` (DeepSeek-V3, recommended for coding) and `deepseek-reasoner` (thinking mode).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
||||||
46
README_zh.md
46
README_zh.md
|
|
@ -17,7 +17,7 @@ CrewAI 是 Python。LangGraph 需要你自己画图。`open-multi-agent` 是你
|
||||||
|
|
||||||
- **一次调用从目标到结果。** `runTeam(team, "构建一个 REST API")` 启动一个协调者 agent,把目标拆成任务 DAG,解析依赖,独立任务并行执行,最终合成输出。不需要画图,不需要手动连任务。
|
- **一次调用从目标到结果。** `runTeam(team, "构建一个 REST API")` 启动一个协调者 agent,把目标拆成任务 DAG,解析依赖,独立任务并行执行,最终合成输出。不需要画图,不需要手动连任务。
|
||||||
- **TypeScript 原生,3 个运行时依赖。** `@anthropic-ai/sdk`、`openai`、`zod`。这就是全部运行时。可嵌入 Express、Next.js、Serverless 函数或 CI/CD 流水线。没有 Python 运行时,没有子进程桥接,没有云端 sidecar。
|
- **TypeScript 原生,3 个运行时依赖。** `@anthropic-ai/sdk`、`openai`、`zod`。这就是全部运行时。可嵌入 Express、Next.js、Serverless 函数或 CI/CD 流水线。没有 Python 运行时,没有子进程桥接,没有云端 sidecar。
|
||||||
- **多模型团队。** Claude、GPT、Gemini、Grok、Copilot,或任何 OpenAI 兼容的本地模型(Ollama、vLLM、LM Studio、llama.cpp)可以在同一个团队中使用。让架构师用 Opus 4.6,开发者用 GPT-5.4,评审用本地的 Gemma 4,一次 `runTeam()` 调用全部搞定。Gemini 作为 optional peer dependency 提供:使用前需 `npm install @google/genai`。
|
- **多模型团队。** Claude、GPT、Gemini、Grok、MiniMax、DeepSeek、Copilot,或任何 OpenAI 兼容的本地模型(Ollama、vLLM、LM Studio、llama.cpp)可以在同一个团队中使用。让架构师用 Opus 4.6,开发者用 GPT-5.4,评审用本地的 Gemma 4,一次 `runTeam()` 调用全部搞定。Gemini 作为 optional peer dependency 提供:使用前需 `npm install @google/genai`。
|
||||||
|
|
||||||
其他能力(MCP 集成、上下文策略、结构化输出、任务重试、人机协同、生命周期钩子、循环检测、可观测性)在下方章节和 [`examples/`](./examples/) 里。
|
其他能力(MCP 集成、上下文策略、结构化输出、任务重试、人机协同、生命周期钩子、循环检测、可观测性)在下方章节和 [`examples/`](./examples/) 里。
|
||||||
|
|
||||||
|
|
@ -72,6 +72,9 @@ npm install @jackchen_me/open-multi-agent
|
||||||
- `OPENAI_API_KEY`
|
- `OPENAI_API_KEY`
|
||||||
- `GEMINI_API_KEY`
|
- `GEMINI_API_KEY`
|
||||||
- `XAI_API_KEY`(Grok)
|
- `XAI_API_KEY`(Grok)
|
||||||
|
- `MINIMAX_API_KEY`(MiniMax)
|
||||||
|
- `MINIMAX_BASE_URL`(MiniMax — 可选,用于选择接入端点)
|
||||||
|
- `DEEPSEEK_API_KEY`(DeepSeek)
|
||||||
- `GITHUB_TOKEN`(Copilot)
|
- `GITHUB_TOKEN`(Copilot)
|
||||||
|
|
||||||
三个智能体,一个目标——框架处理剩下的一切:
|
三个智能体,一个目标——框架处理剩下的一切:
|
||||||
|
|
@ -137,14 +140,17 @@ Tokens: 12847 output tokens
|
||||||
|
|
||||||
## 示例
|
## 示例
|
||||||
|
|
||||||
[`examples/`](./examples/) 里有 15 个可运行脚本。推荐从这 4 个开始:
|
[`examples/`](./examples/) 里有 18 个可运行脚本和 1 个完整项目。推荐从这几个开始:
|
||||||
|
|
||||||
- [02 — 团队协作](examples/02-team-collaboration.ts):`runTeam()` 协调者模式。
|
- [02 — 团队协作](examples/02-team-collaboration.ts):`runTeam()` 协调者模式。
|
||||||
- [06 — 本地模型](examples/06-local-model.ts):通过 `baseURL` 把 Ollama 和 Claude 放在同一条管线。
|
- [06 — 本地模型](examples/06-local-model.ts):通过 `baseURL` 把 Ollama 和 Claude 放在同一条管线。
|
||||||
- [09 — 结构化输出](examples/09-structured-output.ts):任意 agent 产出 Zod 校验过的 JSON。
|
- [09 — 结构化输出](examples/09-structured-output.ts):任意 agent 产出 Zod 校验过的 JSON。
|
||||||
- [11 — 可观测性](examples/11-trace-observability.ts):`onTrace` 回调,为 LLM 调用、工具、任务发出结构化 span。
|
- [11 — 可观测性](examples/11-trace-observability.ts):`onTrace` 回调,为 LLM 调用、工具、任务发出结构化 span。
|
||||||
|
- [17 — MiniMax](examples/17-minimax.ts):使用 MiniMax M2.7 的三智能体团队。
|
||||||
|
- [18 — DeepSeek](examples/18-deepseek.ts):使用 DeepSeek Chat 的三智能体团队。
|
||||||
|
- [with-vercel-ai-sdk](examples/with-vercel-ai-sdk/):Next.js 应用 — OMA `runTeam()` + AI SDK `useChat` 流式输出。
|
||||||
|
|
||||||
用 `npx tsx examples/02-team-collaboration.ts` 运行任意一个。
|
用 `npx tsx examples/02-team-collaboration.ts` 运行脚本示例。
|
||||||
|
|
||||||
## 架构
|
## 架构
|
||||||
|
|
||||||
|
|
@ -180,6 +186,8 @@ Tokens: 12847 output tokens
|
||||||
│ │ - CopilotAdapter │
|
│ │ - CopilotAdapter │
|
||||||
│ │ - GeminiAdapter │
|
│ │ - GeminiAdapter │
|
||||||
│ │ - GrokAdapter │
|
│ │ - GrokAdapter │
|
||||||
|
│ │ - MiniMaxAdapter │
|
||||||
|
│ │ - DeepSeekAdapter │
|
||||||
│ └──────────────────────┘
|
│ └──────────────────────┘
|
||||||
┌────────▼──────────┐
|
┌────────▼──────────┐
|
||||||
│ AgentRunner │ ┌──────────────────────┐
|
│ AgentRunner │ ┌──────────────────────┐
|
||||||
|
|
@ -255,6 +263,9 @@ const customAgent: AgentConfig = {
|
||||||
| Anthropic (Claude) | `provider: 'anthropic'` | `ANTHROPIC_API_KEY` | 已验证 |
|
| Anthropic (Claude) | `provider: 'anthropic'` | `ANTHROPIC_API_KEY` | 已验证 |
|
||||||
| OpenAI (GPT) | `provider: 'openai'` | `OPENAI_API_KEY` | 已验证 |
|
| OpenAI (GPT) | `provider: 'openai'` | `OPENAI_API_KEY` | 已验证 |
|
||||||
| Grok (xAI) | `provider: 'grok'` | `XAI_API_KEY` | 已验证 |
|
| Grok (xAI) | `provider: 'grok'` | `XAI_API_KEY` | 已验证 |
|
||||||
|
| MiniMax(全球) | `provider: 'minimax'` | `MINIMAX_API_KEY` | 已验证 |
|
||||||
|
| MiniMax(国内) | `provider: 'minimax'` + `MINIMAX_BASE_URL` | `MINIMAX_API_KEY` | 已验证 |
|
||||||
|
| DeepSeek | `provider: 'deepseek'` | `DEEPSEEK_API_KEY` | 已验证 |
|
||||||
| GitHub Copilot | `provider: 'copilot'` | `GITHUB_TOKEN` | 已验证 |
|
| GitHub Copilot | `provider: 'copilot'` | `GITHUB_TOKEN` | 已验证 |
|
||||||
| Gemini | `provider: 'gemini'` | `GEMINI_API_KEY` | 已验证 |
|
| Gemini | `provider: 'gemini'` | `GEMINI_API_KEY` | 已验证 |
|
||||||
| Ollama / vLLM / LM Studio | `provider: 'openai'` + `baseURL` | — | 已验证 |
|
| Ollama / vLLM / LM Studio | `provider: 'openai'` + `baseURL` | — | 已验证 |
|
||||||
|
|
@ -264,7 +275,7 @@ Gemini 需要 `npm install @google/genai`(optional peer dependency)。
|
||||||
|
|
||||||
已验证支持 tool-calling 的本地模型:**Gemma 4**(见[示例 08](examples/08-gemma4-local.ts))。
|
已验证支持 tool-calling 的本地模型:**Gemma 4**(见[示例 08](examples/08-gemma4-local.ts))。
|
||||||
|
|
||||||
任何 OpenAI 兼容 API 均可通过 `provider: 'openai'` + `baseURL` 接入(DeepSeek、Groq、Mistral、Qwen、MiniMax 等)。**Grok 现已原生支持**,使用 `provider: 'grok'`。
|
任何 OpenAI 兼容 API 均可通过 `provider: 'openai'` + `baseURL` 接入(Groq、Mistral、Qwen 等)。**Grok、MiniMax 和 DeepSeek 现已原生支持**,分别使用 `provider: 'grok'`、`provider: 'minimax'` 和 `provider: 'deepseek'`。
|
||||||
|
|
||||||
### 本地模型 Tool-Calling
|
### 本地模型 Tool-Calling
|
||||||
|
|
||||||
|
|
@ -306,6 +317,33 @@ const grokAgent: AgentConfig = {
|
||||||
|
|
||||||
(设置 `XAI_API_KEY` 环境变量即可,无需 `baseURL`。)
|
(设置 `XAI_API_KEY` 环境变量即可,无需 `baseURL`。)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const minimaxAgent: AgentConfig = {
|
||||||
|
name: 'minimax-agent',
|
||||||
|
provider: 'minimax',
|
||||||
|
model: 'MiniMax-M2.7',
|
||||||
|
systemPrompt: 'You are a helpful assistant.',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
设置 `MINIMAX_API_KEY`。适配器通过 `MINIMAX_BASE_URL` 选择接入端点:
|
||||||
|
|
||||||
|
- `https://api.minimax.io/v1` 全球端点,默认
|
||||||
|
- `https://api.minimaxi.com/v1` 中国大陆端点
|
||||||
|
|
||||||
|
也可在 `AgentConfig` 中直接传入 `baseURL` 覆盖环境变量。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const deepseekAgent: AgentConfig = {
|
||||||
|
name: 'deepseek-agent',
|
||||||
|
provider: 'deepseek',
|
||||||
|
model: 'deepseek-chat',
|
||||||
|
systemPrompt: '你是一个有用的助手。',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
设置 `DEEPSEEK_API_KEY`。可用模型:`deepseek-chat`(DeepSeek-V3,推荐用于编码任务)和 `deepseek-reasoner`(思考模式)。
|
||||||
|
|
||||||
## 参与贡献
|
## 参与贡献
|
||||||
|
|
||||||
欢迎提 Issue、功能需求和 PR。以下方向的贡献尤其有价值:
|
欢迎提 Issue、功能需求和 PR。以下方向的贡献尤其有价值:
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ npm run build
|
||||||
node dist/cli/oma.js help
|
node dist/cli/oma.js help
|
||||||
```
|
```
|
||||||
|
|
||||||
Set the usual provider API keys in the environment (see [README](../README.md#quick-start)); the CLI does not read secrets from flags.
|
Set the usual provider API keys in the environment (see [README](../README.md#quick-start)); the CLI does not read secrets from flags. MiniMax additionally reads `MINIMAX_BASE_URL` to select the global (`https://api.minimax.io/v1`) or China (`https://api.minimaxi.com/v1`) endpoint.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ Global flags: [`--pretty`](#output-flags), [`--include-messages`](#output-flags)
|
||||||
Read-only helper for wiring JSON configs and env vars.
|
Read-only helper for wiring JSON configs and env vars.
|
||||||
|
|
||||||
- **`oma provider`** or **`oma provider list`** — Prints JSON: built-in provider ids, API key environment variable names, whether `baseURL` is supported, and short notes (e.g. OpenAI-compatible servers, Copilot in CI).
|
- **`oma provider`** or **`oma provider list`** — Prints JSON: built-in provider ids, API key environment variable names, whether `baseURL` is supported, and short notes (e.g. OpenAI-compatible servers, Copilot in CI).
|
||||||
- **`oma provider template <provider>`** — Prints a JSON object with example `orchestrator` and `agent` fields plus placeholder `env` entries. `<provider>` is one of: `anthropic`, `openai`, `gemini`, `grok`, `copilot`.
|
- **`oma provider template <provider>`** — Prints a JSON object with example `orchestrator` and `agent` fields plus placeholder `env` entries. `<provider>` is one of: `anthropic`, `openai`, `gemini`, `grok`, `minimax`, `deepseek`, `copilot`.
|
||||||
|
|
||||||
Supports `--pretty`.
|
Supports `--pretty`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
/**
|
||||||
|
* Example 17 — Multi-Agent Team Collaboration with MiniMax
|
||||||
|
*
|
||||||
|
* Three specialized agents (architect, developer, reviewer) collaborate via `runTeam()`
|
||||||
|
* to build a minimal Express.js REST API. Every agent uses MiniMax's flagship model.
|
||||||
|
*
|
||||||
|
* Run:
|
||||||
|
* npx tsx examples/17-minimax.ts
|
||||||
|
*
|
||||||
|
* Prerequisites:
|
||||||
|
* MINIMAX_API_KEY environment variable must be set.
|
||||||
|
* MINIMAX_BASE_URL environment variable can be set to switch to the China mainland endpoint if needed.
|
||||||
|
*
|
||||||
|
* Endpoints:
|
||||||
|
* Global (default): https://api.minimax.io/v1
|
||||||
|
* China mainland: https://api.minimaxi.com/v1 (set MINIMAX_BASE_URL)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OpenMultiAgent } from '../src/index.js'
|
||||||
|
import type { AgentConfig, OrchestratorEvent } from '../src/types.js'
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Agent definitions (all using MiniMax-M2.7)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const architect: AgentConfig = {
|
||||||
|
name: 'architect',
|
||||||
|
model: 'MiniMax-M2.7',
|
||||||
|
provider: 'minimax',
|
||||||
|
systemPrompt: `You are a software architect with deep experience in Node.js and REST API design.
|
||||||
|
Your job is to design clear, production-quality API contracts and file/directory structures.
|
||||||
|
Output concise plans in markdown — no unnecessary prose.`,
|
||||||
|
tools: ['bash', 'file_write'],
|
||||||
|
maxTurns: 5,
|
||||||
|
temperature: 0.2,
|
||||||
|
}
|
||||||
|
|
||||||
|
const developer: AgentConfig = {
|
||||||
|
name: 'developer',
|
||||||
|
model: 'MiniMax-M2.7',
|
||||||
|
provider: 'minimax',
|
||||||
|
systemPrompt: `You are a TypeScript/Node.js developer. You implement what the architect specifies.
|
||||||
|
Write clean, runnable code with proper error handling. Use the tools to write files and run tests.`,
|
||||||
|
tools: ['bash', 'file_read', 'file_write', 'file_edit'],
|
||||||
|
maxTurns: 12,
|
||||||
|
temperature: 0.1,
|
||||||
|
}
|
||||||
|
|
||||||
|
const reviewer: AgentConfig = {
|
||||||
|
name: 'reviewer',
|
||||||
|
model: 'MiniMax-M2.7',
|
||||||
|
provider: 'minimax',
|
||||||
|
systemPrompt: `You are a senior code reviewer. Review code for correctness, security, and clarity.
|
||||||
|
Provide a structured review with: LGTM items, suggestions, and any blocking issues.
|
||||||
|
Read files using the tools before reviewing.`,
|
||||||
|
tools: ['bash', 'file_read', 'grep'],
|
||||||
|
maxTurns: 5,
|
||||||
|
temperature: 0.3,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Progress tracking
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const startTimes = new Map<string, number>()
|
||||||
|
|
||||||
|
function handleProgress(event: OrchestratorEvent): void {
|
||||||
|
const ts = new Date().toISOString().slice(11, 23) // HH:MM:SS.mmm
|
||||||
|
switch (event.type) {
|
||||||
|
case 'agent_start':
|
||||||
|
startTimes.set(event.agent ?? '', Date.now())
|
||||||
|
console.log(`[${ts}] AGENT START → ${event.agent}`)
|
||||||
|
break
|
||||||
|
case 'agent_complete': {
|
||||||
|
const elapsed = Date.now() - (startTimes.get(event.agent ?? '') ?? Date.now())
|
||||||
|
console.log(`[${ts}] AGENT DONE ← ${event.agent} (${elapsed}ms)`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'task_start':
|
||||||
|
console.log(`[${ts}] TASK START ↓ ${event.task}`)
|
||||||
|
break
|
||||||
|
case 'task_complete':
|
||||||
|
console.log(`[${ts}] TASK DONE ↑ ${event.task}`)
|
||||||
|
break
|
||||||
|
case 'message':
|
||||||
|
console.log(`[${ts}] MESSAGE • ${event.agent} → (team)`)
|
||||||
|
break
|
||||||
|
case 'error':
|
||||||
|
console.error(`[${ts}] ERROR ✗ agent=${event.agent} task=${event.task}`)
|
||||||
|
if (event.data instanceof Error) console.error(` ${event.data.message}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Orchestrate
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const orchestrator = new OpenMultiAgent({
|
||||||
|
defaultModel: 'MiniMax-M2.7',
|
||||||
|
defaultProvider: 'minimax',
|
||||||
|
maxConcurrency: 1, // sequential for readable output
|
||||||
|
onProgress: handleProgress,
|
||||||
|
})
|
||||||
|
|
||||||
|
const team = orchestrator.createTeam('api-team', {
|
||||||
|
name: 'api-team',
|
||||||
|
agents: [architect, developer, reviewer],
|
||||||
|
sharedMemory: true,
|
||||||
|
maxConcurrency: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`Team "${team.name}" created with agents: ${team.getAgents().map(a => a.name).join(', ')}`)
|
||||||
|
console.log('\nStarting team run...\n')
|
||||||
|
console.log('='.repeat(60))
|
||||||
|
|
||||||
|
const goal = `Create a minimal Express.js REST API in /tmp/express-api/ with:
|
||||||
|
- GET /health → { status: "ok" }
|
||||||
|
- GET /users → returns a hardcoded array of 2 user objects
|
||||||
|
- POST /users → accepts { name, email } body, logs it, returns 201
|
||||||
|
- Proper error handling middleware
|
||||||
|
- The server should listen on port 3001
|
||||||
|
- Include a package.json with the required dependencies`
|
||||||
|
|
||||||
|
const result = await orchestrator.runTeam(team, goal)
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(60))
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Results
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
console.log('\nTeam run complete.')
|
||||||
|
console.log(`Success: ${result.success}`)
|
||||||
|
console.log(`Total tokens — input: ${result.totalTokenUsage.input_tokens}, output: ${result.totalTokenUsage.output_tokens}`)
|
||||||
|
|
||||||
|
console.log('\nPer-agent results:')
|
||||||
|
for (const [agentName, agentResult] of result.agentResults) {
|
||||||
|
const status = agentResult.success ? 'OK' : 'FAILED'
|
||||||
|
const tools = agentResult.toolCalls.length
|
||||||
|
console.log(` ${agentName.padEnd(12)} [${status}] tool_calls=${tools}`)
|
||||||
|
if (!agentResult.success) {
|
||||||
|
console.log(` Error: ${agentResult.output.slice(0, 120)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample outputs
|
||||||
|
const developerResult = result.agentResults.get('developer')
|
||||||
|
if (developerResult?.success) {
|
||||||
|
console.log('\nDeveloper output (last 600 chars):')
|
||||||
|
console.log('─'.repeat(60))
|
||||||
|
const out = developerResult.output
|
||||||
|
console.log(out.length > 600 ? '...' + out.slice(-600) : out)
|
||||||
|
console.log('─'.repeat(60))
|
||||||
|
}
|
||||||
|
|
||||||
|
const reviewerResult = result.agentResults.get('reviewer')
|
||||||
|
if (reviewerResult?.success) {
|
||||||
|
console.log('\nReviewer output:')
|
||||||
|
console.log('─'.repeat(60))
|
||||||
|
console.log(reviewerResult.output)
|
||||||
|
console.log('─'.repeat(60))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
/**
|
||||||
|
* Example 18 — Multi-Agent Team Collaboration with DeepSeek
|
||||||
|
*
|
||||||
|
* Three specialized agents (architect, developer, reviewer) collaborate via `runTeam()`
|
||||||
|
* to build a minimal Express.js REST API. Every agent uses DeepSeek's flagship model.
|
||||||
|
*
|
||||||
|
* Run:
|
||||||
|
* npx tsx examples/18-deepseek.ts
|
||||||
|
*
|
||||||
|
* Prerequisites:
|
||||||
|
* DEEPSEEK_API_KEY environment variable must be set.
|
||||||
|
*
|
||||||
|
* Available models:
|
||||||
|
* deepseek-chat — DeepSeek-V3 (non-thinking mode, recommended for coding tasks)
|
||||||
|
* deepseek-reasoner — DeepSeek-V3 (thinking mode, for complex reasoning)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OpenMultiAgent } from '../src/index.js'
|
||||||
|
import type { AgentConfig, OrchestratorEvent } from '../src/types.js'
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Agent definitions (all using deepseek-chat)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const architect: AgentConfig = {
|
||||||
|
name: 'architect',
|
||||||
|
model: 'deepseek-reasoner',
|
||||||
|
provider: 'deepseek',
|
||||||
|
systemPrompt: `You are a software architect with deep experience in Node.js and REST API design.
|
||||||
|
Your job is to design clear, production-quality API contracts and file/directory structures.
|
||||||
|
Output concise plans in markdown — no unnecessary prose.`,
|
||||||
|
tools: ['bash', 'file_write'],
|
||||||
|
maxTurns: 5,
|
||||||
|
temperature: 0.2,
|
||||||
|
}
|
||||||
|
|
||||||
|
const developer: AgentConfig = {
|
||||||
|
name: 'developer',
|
||||||
|
model: 'deepseek-chat',
|
||||||
|
provider: 'deepseek',
|
||||||
|
systemPrompt: `You are a TypeScript/Node.js developer. You implement what the architect specifies.
|
||||||
|
Write clean, runnable code with proper error handling. Use the tools to write files and run tests.`,
|
||||||
|
tools: ['bash', 'file_read', 'file_write', 'file_edit'],
|
||||||
|
maxTurns: 12,
|
||||||
|
temperature: 0.1,
|
||||||
|
}
|
||||||
|
|
||||||
|
const reviewer: AgentConfig = {
|
||||||
|
name: 'reviewer',
|
||||||
|
model: 'deepseek-chat',
|
||||||
|
provider: 'deepseek',
|
||||||
|
systemPrompt: `You are a senior code reviewer. Review code for correctness, security, and clarity.
|
||||||
|
Provide a structured review with: LGTM items, suggestions, and any blocking issues.
|
||||||
|
Read files using the tools before reviewing.`,
|
||||||
|
tools: ['bash', 'file_read', 'grep'],
|
||||||
|
maxTurns: 5,
|
||||||
|
temperature: 0.3,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Progress tracking
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const startTimes = new Map<string, number>()
|
||||||
|
|
||||||
|
function handleProgress(event: OrchestratorEvent): void {
|
||||||
|
const ts = new Date().toISOString().slice(11, 23) // HH:MM:SS.mmm
|
||||||
|
switch (event.type) {
|
||||||
|
case 'agent_start':
|
||||||
|
startTimes.set(event.agent ?? '', Date.now())
|
||||||
|
console.log(`[${ts}] AGENT START → ${event.agent}`)
|
||||||
|
break
|
||||||
|
case 'agent_complete': {
|
||||||
|
const elapsed = Date.now() - (startTimes.get(event.agent ?? '') ?? Date.now())
|
||||||
|
console.log(`[${ts}] AGENT DONE ← ${event.agent} (${elapsed}ms)`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'task_start':
|
||||||
|
console.log(`[${ts}] TASK START ↓ ${event.task}`)
|
||||||
|
break
|
||||||
|
case 'task_complete':
|
||||||
|
console.log(`[${ts}] TASK DONE ↑ ${event.task}`)
|
||||||
|
break
|
||||||
|
case 'message':
|
||||||
|
console.log(`[${ts}] MESSAGE • ${event.agent} → (team)`)
|
||||||
|
break
|
||||||
|
case 'error':
|
||||||
|
console.error(`[${ts}] ERROR ✗ agent=${event.agent} task=${event.task}`)
|
||||||
|
if (event.data instanceof Error) console.error(` ${event.data.message}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Orchestrate
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const orchestrator = new OpenMultiAgent({
|
||||||
|
defaultModel: 'deepseek-chat',
|
||||||
|
defaultProvider: 'deepseek',
|
||||||
|
maxConcurrency: 1, // sequential for readable output
|
||||||
|
onProgress: handleProgress,
|
||||||
|
})
|
||||||
|
|
||||||
|
const team = orchestrator.createTeam('api-team', {
|
||||||
|
name: 'api-team',
|
||||||
|
agents: [architect, developer, reviewer],
|
||||||
|
sharedMemory: true,
|
||||||
|
maxConcurrency: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`Team "${team.name}" created with agents: ${team.getAgents().map(a => a.name).join(', ')}`)
|
||||||
|
console.log('\nStarting team run...\n')
|
||||||
|
console.log('='.repeat(60))
|
||||||
|
|
||||||
|
const goal = `Create a minimal Express.js REST API in /tmp/express-api/ with:
|
||||||
|
- GET /health → { status: "ok" }
|
||||||
|
- GET /users → returns a hardcoded array of 2 user objects
|
||||||
|
- POST /users → accepts { name, email } body, logs it, returns 201
|
||||||
|
- Proper error handling middleware
|
||||||
|
- The server should listen on port 3001
|
||||||
|
- Include a package.json with the required dependencies`
|
||||||
|
|
||||||
|
const result = await orchestrator.runTeam(team, goal)
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(60))
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Results
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
console.log('\nTeam run complete.')
|
||||||
|
console.log(`Success: ${result.success}`)
|
||||||
|
console.log(`Total tokens — input: ${result.totalTokenUsage.input_tokens}, output: ${result.totalTokenUsage.output_tokens}`)
|
||||||
|
|
||||||
|
console.log('\nPer-agent results:')
|
||||||
|
for (const [agentName, agentResult] of result.agentResults) {
|
||||||
|
const status = agentResult.success ? 'OK' : 'FAILED'
|
||||||
|
const tools = agentResult.toolCalls.length
|
||||||
|
console.log(` ${agentName.padEnd(12)} [${status}] tool_calls=${tools}`)
|
||||||
|
if (!agentResult.success) {
|
||||||
|
console.log(` Error: ${agentResult.output.slice(0, 120)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample outputs
|
||||||
|
const developerResult = result.agentResults.get('developer')
|
||||||
|
if (developerResult?.success) {
|
||||||
|
console.log('\nDeveloper output (last 600 chars):')
|
||||||
|
console.log('─'.repeat(60))
|
||||||
|
const out = developerResult.output
|
||||||
|
console.log(out.length > 600 ? '...' + out.slice(-600) : out)
|
||||||
|
console.log('─'.repeat(60))
|
||||||
|
}
|
||||||
|
|
||||||
|
const reviewerResult = result.agentResults.get('reviewer')
|
||||||
|
if (reviewerResult?.success) {
|
||||||
|
console.log('\nReviewer output:')
|
||||||
|
console.log('─'.repeat(60))
|
||||||
|
console.log(reviewerResult.output)
|
||||||
|
console.log('─'.repeat(60))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules/
|
||||||
|
.next/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
# with-vercel-ai-sdk
|
||||||
|
|
||||||
|
A Next.js demo showing **open-multi-agent** (OMA) and **Vercel AI SDK** working together:
|
||||||
|
|
||||||
|
- **OMA** orchestrates a research team (researcher agent + writer agent) via `runTeam()`
|
||||||
|
- **AI SDK** streams the result to a chat UI via `useChat` + `streamText`
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
```
|
||||||
|
User message
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
API route (app/api/chat/route.ts)
|
||||||
|
│
|
||||||
|
├─ Phase 1: OMA runTeam()
|
||||||
|
│ coordinator decomposes goal → researcher gathers info → writer drafts article
|
||||||
|
│
|
||||||
|
└─ Phase 2: AI SDK streamText()
|
||||||
|
streams the team's output to the browser
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Chat UI (app/page.tsx) — useChat hook renders streamed response
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. From repo root, install OMA dependencies
|
||||||
|
cd ../..
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 2. Back to this example
|
||||||
|
cd examples/with-vercel-ai-sdk
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 3. Set your API key
|
||||||
|
export ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
|
||||||
|
# 4. Run
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm run dev` automatically builds OMA before starting Next.js (via the `predev` script).
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000), type a topic, and watch the research team work.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js >= 18
|
||||||
|
- `ANTHROPIC_API_KEY` environment variable (used by both OMA and AI SDK)
|
||||||
|
|
||||||
|
## Key files
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|------|------|
|
||||||
|
| `app/api/chat/route.ts` | Backend — OMA orchestration + AI SDK streaming |
|
||||||
|
| `app/page.tsx` | Frontend — chat UI with `useChat` hook |
|
||||||
|
| `package.json` | References OMA via `file:../../` (local link) |
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { streamText, convertToModelMessages, type UIMessage } from 'ai'
|
||||||
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
|
||||||
|
import { OpenMultiAgent } from '@jackchen_me/open-multi-agent'
|
||||||
|
import type { AgentConfig } from '@jackchen_me/open-multi-agent'
|
||||||
|
|
||||||
|
export const maxDuration = 120
|
||||||
|
|
||||||
|
// --- DeepSeek via OpenAI-compatible API ---
|
||||||
|
const DEEPSEEK_BASE_URL = 'https://api.deepseek.com'
|
||||||
|
const DEEPSEEK_MODEL = 'deepseek-chat'
|
||||||
|
|
||||||
|
const deepseek = createOpenAICompatible({
|
||||||
|
name: 'deepseek',
|
||||||
|
baseURL: `${DEEPSEEK_BASE_URL}/v1`,
|
||||||
|
apiKey: process.env.DEEPSEEK_API_KEY,
|
||||||
|
})
|
||||||
|
|
||||||
|
const researcher: AgentConfig = {
|
||||||
|
name: 'researcher',
|
||||||
|
model: DEEPSEEK_MODEL,
|
||||||
|
provider: 'openai',
|
||||||
|
baseURL: DEEPSEEK_BASE_URL,
|
||||||
|
apiKey: process.env.DEEPSEEK_API_KEY,
|
||||||
|
systemPrompt: `You are a research specialist. Given a topic, provide thorough, factual research
|
||||||
|
with key findings, relevant data points, and important context.
|
||||||
|
Be concise but comprehensive. Output structured notes, not prose.`,
|
||||||
|
maxTurns: 3,
|
||||||
|
temperature: 0.2,
|
||||||
|
}
|
||||||
|
|
||||||
|
const writer: AgentConfig = {
|
||||||
|
name: 'writer',
|
||||||
|
model: DEEPSEEK_MODEL,
|
||||||
|
provider: 'openai',
|
||||||
|
baseURL: DEEPSEEK_BASE_URL,
|
||||||
|
apiKey: process.env.DEEPSEEK_API_KEY,
|
||||||
|
systemPrompt: `You are an expert writer. Using research from team members (available in shared memory),
|
||||||
|
write a well-structured, engaging article with clear headings and concise paragraphs.
|
||||||
|
Do not repeat raw research — synthesize it into readable prose.`,
|
||||||
|
maxTurns: 3,
|
||||||
|
temperature: 0.4,
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractText(message: UIMessage): string {
|
||||||
|
return message.parts
|
||||||
|
.filter((p): p is { type: 'text'; text: string } => p.type === 'text')
|
||||||
|
.map((p) => p.text)
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const { messages }: { messages: UIMessage[] } = await req.json()
|
||||||
|
const lastText = extractText(messages.at(-1)!)
|
||||||
|
|
||||||
|
// --- Phase 1: OMA multi-agent orchestration ---
|
||||||
|
const orchestrator = new OpenMultiAgent({
|
||||||
|
defaultModel: DEEPSEEK_MODEL,
|
||||||
|
defaultProvider: 'openai',
|
||||||
|
defaultBaseURL: DEEPSEEK_BASE_URL,
|
||||||
|
defaultApiKey: process.env.DEEPSEEK_API_KEY,
|
||||||
|
})
|
||||||
|
|
||||||
|
const team = orchestrator.createTeam('research-writing', {
|
||||||
|
name: 'research-writing',
|
||||||
|
agents: [researcher, writer],
|
||||||
|
sharedMemory: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const teamResult = await orchestrator.runTeam(
|
||||||
|
team,
|
||||||
|
`Research and write an article about: ${lastText}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
const teamOutput = teamResult.agentResults.get('coordinator')?.output ?? ''
|
||||||
|
|
||||||
|
// --- Phase 2: Stream result via Vercel AI SDK ---
|
||||||
|
const result = streamText({
|
||||||
|
model: deepseek(DEEPSEEK_MODEL),
|
||||||
|
system: `You are presenting research from a multi-agent team (researcher + writer).
|
||||||
|
The team has already done the work. Your only job is to relay their output to the user
|
||||||
|
in a well-formatted way. Keep the content faithful to the team output below.
|
||||||
|
At the very end, add a one-line note that this was produced by a researcher agent
|
||||||
|
and a writer agent collaborating via open-multi-agent.
|
||||||
|
|
||||||
|
## Team Output
|
||||||
|
${teamOutput}`,
|
||||||
|
messages: await convertToModelMessages(messages),
|
||||||
|
})
|
||||||
|
|
||||||
|
return result.toUIMessageStreamResponse()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'OMA + Vercel AI SDK',
|
||||||
|
description: 'Multi-agent research team powered by open-multi-agent, streamed via Vercel AI SDK',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body style={{ margin: 0, background: '#fafafa' }}>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useChat } from '@ai-sdk/react'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const { messages, sendMessage, status, error } = useChat()
|
||||||
|
const [input, setInput] = useState('')
|
||||||
|
|
||||||
|
const isLoading = status === 'submitted' || status === 'streaming'
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!input.trim() || isLoading) return
|
||||||
|
const text = input
|
||||||
|
setInput('')
|
||||||
|
await sendMessage({ text })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
style={{
|
||||||
|
maxWidth: 720,
|
||||||
|
margin: '0 auto',
|
||||||
|
padding: '32px 16px',
|
||||||
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1 style={{ fontSize: 22, marginBottom: 4 }}>Research Team</h1>
|
||||||
|
<p style={{ color: '#666', fontSize: 14, marginBottom: 28 }}>
|
||||||
|
Enter a topic. A <strong>researcher</strong> agent gathers information, a{' '}
|
||||||
|
<strong>writer</strong> agent composes an article — orchestrated by
|
||||||
|
open-multi-agent, streamed via Vercel AI SDK.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ minHeight: 120 }}>
|
||||||
|
{messages.map((m) => (
|
||||||
|
<div key={m.id} style={{ marginBottom: 24, lineHeight: 1.7 }}>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: 13, color: '#999', marginBottom: 4 }}>
|
||||||
|
{m.role === 'user' ? 'You' : 'Research Team'}
|
||||||
|
</div>
|
||||||
|
<div style={{ whiteSpace: 'pre-wrap', fontSize: 15 }}>
|
||||||
|
{m.parts
|
||||||
|
.filter((part): part is { type: 'text'; text: string } => part.type === 'text')
|
||||||
|
.map((part) => part.text)
|
||||||
|
.join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isLoading && status === 'submitted' && (
|
||||||
|
<div style={{ color: '#888', fontSize: 14, padding: '8px 0' }}>
|
||||||
|
Agents are collaborating — this may take a minute...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{ color: '#c00', fontSize: 14, padding: '8px 0' }}>
|
||||||
|
Error: {error.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} style={{ display: 'flex', gap: 8, marginTop: 32 }}>
|
||||||
|
<input
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
placeholder="Enter a topic to research..."
|
||||||
|
disabled={isLoading}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: '10px 14px',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
fontSize: 15,
|
||||||
|
outline: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || !input.trim()}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: 'none',
|
||||||
|
background: isLoading ? '#ccc' : '#111',
|
||||||
|
color: '#fff',
|
||||||
|
cursor: isLoading ? 'not-allowed' : 'pointer',
|
||||||
|
fontSize: 15,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { NextConfig } from 'next'
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
serverExternalPackages: ['@jackchen_me/open-multi-agent'],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default nextConfig
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "with-vercel-ai-sdk",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"predev": "cd ../.. && npm run build",
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/openai-compatible": "^2.0.41",
|
||||||
|
"@ai-sdk/react": "^3.0.0",
|
||||||
|
"@jackchen_me/open-multi-agent": "file:../../",
|
||||||
|
"ai": "^6.0.0",
|
||||||
|
"next": "^16.0.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"typescript": "^5.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"ES2022"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -448,8 +448,10 @@ export class AgentRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Apply denylist filter if set
|
// 3. Apply denylist filter if set
|
||||||
if (this.options.disallowedTools) {
|
const denied = this.options.disallowedTools
|
||||||
const denied = new Set(this.options.disallowedTools)
|
? new Set(this.options.disallowedTools)
|
||||||
|
: undefined
|
||||||
|
if (denied) {
|
||||||
filteredTools = filteredTools.filter(t => !denied.has(t.name))
|
filteredTools = filteredTools.filter(t => !denied.has(t.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -457,8 +459,11 @@ export class AgentRunner {
|
||||||
const frameworkDenied = new Set(AGENT_FRAMEWORK_DISALLOWED)
|
const frameworkDenied = new Set(AGENT_FRAMEWORK_DISALLOWED)
|
||||||
filteredTools = filteredTools.filter(t => !frameworkDenied.has(t.name))
|
filteredTools = filteredTools.filter(t => !frameworkDenied.has(t.name))
|
||||||
|
|
||||||
// Runtime-added custom tools stay available regardless of filtering rules.
|
// Runtime-added custom tools bypass preset / allowlist but respect denylist.
|
||||||
return [...filteredTools, ...runtimeCustomTools]
|
const finalRuntime = denied
|
||||||
|
? runtimeCustomTools.filter(t => !denied.has(t.name))
|
||||||
|
: runtimeCustomTools
|
||||||
|
return [...filteredTools, ...finalRuntime]
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ const PROVIDER_REFERENCE: ReadonlyArray<{
|
||||||
{ id: 'openai', apiKeyEnv: ['OPENAI_API_KEY'], baseUrlSupported: true, notes: 'Set baseURL for Ollama / vLLM / LM Studio; apiKey may be a placeholder.' },
|
{ id: 'openai', apiKeyEnv: ['OPENAI_API_KEY'], baseUrlSupported: true, notes: 'Set baseURL for Ollama / vLLM / LM Studio; apiKey may be a placeholder.' },
|
||||||
{ id: 'gemini', apiKeyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'], baseUrlSupported: false },
|
{ id: 'gemini', apiKeyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'], baseUrlSupported: false },
|
||||||
{ id: 'grok', apiKeyEnv: ['XAI_API_KEY'], baseUrlSupported: true },
|
{ id: 'grok', apiKeyEnv: ['XAI_API_KEY'], baseUrlSupported: true },
|
||||||
|
{ id: 'minimax', apiKeyEnv: ['MINIMAX_API_KEY'], baseUrlSupported: true, notes: 'Global endpoint: https://api.minimax.io/v1 (default). China endpoint: https://api.minimaxi.com/v1. Set MINIMAX_BASE_URL to choose, or pass baseURL in agent config.' },
|
||||||
|
{ id: 'deepseek', apiKeyEnv: ['DEEPSEEK_API_KEY'], baseUrlSupported: true, notes: 'OpenAI-compatible endpoint at https://api.deepseek.com/v1. Models: deepseek-chat (V3), deepseek-reasoner (thinking).' },
|
||||||
{
|
{
|
||||||
id: 'copilot',
|
id: 'copilot',
|
||||||
apiKeyEnv: ['GITHUB_COPILOT_TOKEN', 'GITHUB_TOKEN'],
|
apiKeyEnv: ['GITHUB_COPILOT_TOKEN', 'GITHUB_TOKEN'],
|
||||||
|
|
@ -259,6 +261,8 @@ const DEFAULT_MODEL_HINT: Record<SupportedProvider, string> = {
|
||||||
gemini: 'gemini-2.0-flash',
|
gemini: 'gemini-2.0-flash',
|
||||||
grok: 'grok-2-latest',
|
grok: 'grok-2-latest',
|
||||||
copilot: 'gpt-4o',
|
copilot: 'gpt-4o',
|
||||||
|
minimax: 'MiniMax-M2.7',
|
||||||
|
deepseek: 'deepseek-chat',
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdProvider(sub: string | undefined, arg: string | undefined, pretty: boolean): Promise<number> {
|
async function cmdProvider(sub: string | undefined, arg: string | undefined, pretty: boolean): Promise<number> {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ import type { LLMAdapter } from '../types.js'
|
||||||
* Additional providers can be integrated by implementing {@link LLMAdapter}
|
* Additional providers can be integrated by implementing {@link LLMAdapter}
|
||||||
* directly and bypassing this factory.
|
* directly and bypassing this factory.
|
||||||
*/
|
*/
|
||||||
export type SupportedProvider = 'anthropic' | 'copilot' | 'grok' | 'openai' | 'gemini'
|
export type SupportedProvider = 'anthropic' | 'copilot' | 'deepseek' | 'grok' | 'minimax' | 'openai' | 'gemini'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiate the appropriate {@link LLMAdapter} for the given provider.
|
* Instantiate the appropriate {@link LLMAdapter} for the given provider.
|
||||||
|
|
@ -49,6 +49,8 @@ export type SupportedProvider = 'anthropic' | 'copilot' | 'grok' | 'openai' | 'g
|
||||||
* - `openai` → `OPENAI_API_KEY`
|
* - `openai` → `OPENAI_API_KEY`
|
||||||
* - `gemini` → `GEMINI_API_KEY` / `GOOGLE_API_KEY`
|
* - `gemini` → `GEMINI_API_KEY` / `GOOGLE_API_KEY`
|
||||||
* - `grok` → `XAI_API_KEY`
|
* - `grok` → `XAI_API_KEY`
|
||||||
|
* - `minimax` → `MINIMAX_API_KEY`
|
||||||
|
* - `deepseek` → `DEEPSEEK_API_KEY`
|
||||||
* - `copilot` → `GITHUB_COPILOT_TOKEN` / `GITHUB_TOKEN`, or interactive
|
* - `copilot` → `GITHUB_COPILOT_TOKEN` / `GITHUB_TOKEN`, or interactive
|
||||||
* OAuth2 device flow if neither is set
|
* OAuth2 device flow if neither is set
|
||||||
*
|
*
|
||||||
|
|
@ -89,6 +91,14 @@ export async function createAdapter(
|
||||||
const { GrokAdapter } = await import('./grok.js')
|
const { GrokAdapter } = await import('./grok.js')
|
||||||
return new GrokAdapter(apiKey, baseURL)
|
return new GrokAdapter(apiKey, baseURL)
|
||||||
}
|
}
|
||||||
|
case 'minimax': {
|
||||||
|
const { MiniMaxAdapter } = await import('./minimax.js')
|
||||||
|
return new MiniMaxAdapter(apiKey, baseURL)
|
||||||
|
}
|
||||||
|
case 'deepseek': {
|
||||||
|
const { DeepSeekAdapter } = await import('./deepseek.js')
|
||||||
|
return new DeepSeekAdapter(apiKey, baseURL)
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
// The `never` cast here makes TypeScript enforce exhaustiveness.
|
// The `never` cast here makes TypeScript enforce exhaustiveness.
|
||||||
const _exhaustive: never = provider
|
const _exhaustive: never = provider
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* @fileoverview DeepSeek adapter.
|
||||||
|
*
|
||||||
|
* Thin wrapper around OpenAIAdapter that hard-codes the official DeepSeek
|
||||||
|
* OpenAI-compatible endpoint and DEEPSEEK_API_KEY environment variable fallback.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OpenAIAdapter } from './openai.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM adapter for DeepSeek models (deepseek-chat, deepseek-reasoner, and future models).
|
||||||
|
*
|
||||||
|
* Thread-safe. Can be shared across agents.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* provider: 'deepseek'
|
||||||
|
* model: 'deepseek-chat' (or 'deepseek-reasoner' for the thinking model)
|
||||||
|
*/
|
||||||
|
export class DeepSeekAdapter extends OpenAIAdapter {
|
||||||
|
readonly name = 'deepseek'
|
||||||
|
|
||||||
|
constructor(apiKey?: string, baseURL?: string) {
|
||||||
|
// Allow override of baseURL (for proxies or future changes) but default to official DeepSeek endpoint.
|
||||||
|
super(
|
||||||
|
apiKey ?? process.env['DEEPSEEK_API_KEY'],
|
||||||
|
baseURL ?? 'https://api.deepseek.com/v1'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* @fileoverview MiniMax adapter.
|
||||||
|
*
|
||||||
|
* Thin wrapper around OpenAIAdapter that hard-codes the official MiniMax
|
||||||
|
* OpenAI-compatible endpoint and MINIMAX_API_KEY environment variable fallback.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { OpenAIAdapter } from './openai.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM adapter for MiniMax models (MiniMax-M2.7 series and future models).
|
||||||
|
*
|
||||||
|
* Thread-safe. Can be shared across agents.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* provider: 'minimax'
|
||||||
|
* model: 'MiniMax-M2.7' (or any current MiniMax model name)
|
||||||
|
*/
|
||||||
|
export class MiniMaxAdapter extends OpenAIAdapter {
|
||||||
|
readonly name = 'minimax'
|
||||||
|
|
||||||
|
constructor(apiKey?: string, baseURL?: string) {
|
||||||
|
// Allow override of baseURL (for proxies or future changes) but default to official MiniMax endpoint.
|
||||||
|
super(
|
||||||
|
apiKey ?? process.env['MINIMAX_API_KEY'],
|
||||||
|
baseURL ?? process.env['MINIMAX_BASE_URL'] ?? 'https://api.minimax.io/v1'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -212,6 +212,11 @@ function resolveTokenBudget(primary?: number, fallback?: number): number | undef
|
||||||
function buildAgent(config: AgentConfig): Agent {
|
function buildAgent(config: AgentConfig): Agent {
|
||||||
const registry = new ToolRegistry()
|
const registry = new ToolRegistry()
|
||||||
registerBuiltInTools(registry)
|
registerBuiltInTools(registry)
|
||||||
|
if (config.customTools) {
|
||||||
|
for (const tool of config.customTools) {
|
||||||
|
registry.register(tool, { runtimeAdded: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
const executor = new ToolExecutor(registry, {
|
const executor = new ToolExecutor(registry, {
|
||||||
...(config.maxToolOutputChars !== undefined
|
...(config.maxToolOutputChars !== undefined
|
||||||
? { maxToolOutputChars: config.maxToolOutputChars }
|
? { maxToolOutputChars: config.maxToolOutputChars }
|
||||||
|
|
|
||||||
10
src/types.ts
10
src/types.ts
|
|
@ -229,6 +229,16 @@ export interface AgentConfig {
|
||||||
/** API key override; falls back to the provider's standard env var. */
|
/** API key override; falls back to the provider's standard env var. */
|
||||||
readonly apiKey?: string
|
readonly apiKey?: string
|
||||||
readonly systemPrompt?: string
|
readonly systemPrompt?: string
|
||||||
|
/**
|
||||||
|
* Custom tool definitions to register alongside built-in tools.
|
||||||
|
* Created via `defineTool()`. Custom tools bypass `tools` (allowlist)
|
||||||
|
* and `toolPreset` filtering, but can still be blocked by `disallowedTools`.
|
||||||
|
*
|
||||||
|
* Tool names must not collide with built-in tool names; a duplicate name
|
||||||
|
* will throw at registration time.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
readonly customTools?: readonly ToolDefinition<any>[]
|
||||||
/** Names of tools (from the tool registry) available to this agent. */
|
/** Names of tools (from the tool registry) available to this agent. */
|
||||||
readonly tools?: readonly string[]
|
readonly tools?: readonly string[]
|
||||||
/** Names of tools explicitly disallowed for this agent. */
|
/** Names of tools explicitly disallowed for this agent. */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mock OpenAI constructor (must be hoisted for Vitest)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const OpenAIMock = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
|
vi.mock('openai', () => ({
|
||||||
|
default: OpenAIMock,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { DeepSeekAdapter } from '../src/llm/deepseek.js'
|
||||||
|
import { createAdapter } from '../src/llm/adapter.js'
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DeepSeekAdapter tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('DeepSeekAdapter', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
OpenAIMock.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has name "deepseek"', () => {
|
||||||
|
const adapter = new DeepSeekAdapter()
|
||||||
|
expect(adapter.name).toBe('deepseek')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses DEEPSEEK_API_KEY by default', () => {
|
||||||
|
const original = process.env['DEEPSEEK_API_KEY']
|
||||||
|
process.env['DEEPSEEK_API_KEY'] = 'deepseek-test-key-123'
|
||||||
|
|
||||||
|
try {
|
||||||
|
new DeepSeekAdapter()
|
||||||
|
expect(OpenAIMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
apiKey: 'deepseek-test-key-123',
|
||||||
|
baseURL: 'https://api.deepseek.com/v1',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
if (original === undefined) {
|
||||||
|
delete process.env['DEEPSEEK_API_KEY']
|
||||||
|
} else {
|
||||||
|
process.env['DEEPSEEK_API_KEY'] = original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses official DeepSeek baseURL by default', () => {
|
||||||
|
new DeepSeekAdapter('some-key')
|
||||||
|
expect(OpenAIMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
apiKey: 'some-key',
|
||||||
|
baseURL: 'https://api.deepseek.com/v1',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows overriding apiKey and baseURL', () => {
|
||||||
|
new DeepSeekAdapter('custom-key', 'https://custom.endpoint/v1')
|
||||||
|
expect(OpenAIMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
apiKey: 'custom-key',
|
||||||
|
baseURL: 'https://custom.endpoint/v1',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('createAdapter("deepseek") returns DeepSeekAdapter instance', async () => {
|
||||||
|
const adapter = await createAdapter('deepseek')
|
||||||
|
expect(adapter).toBeInstanceOf(DeepSeekAdapter)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mock OpenAI constructor (must be hoisted for Vitest)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const OpenAIMock = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
|
vi.mock('openai', () => ({
|
||||||
|
default: OpenAIMock,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { MiniMaxAdapter } from '../src/llm/minimax.js'
|
||||||
|
import { createAdapter } from '../src/llm/adapter.js'
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// MiniMaxAdapter tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('MiniMaxAdapter', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
OpenAIMock.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has name "minimax"', () => {
|
||||||
|
const adapter = new MiniMaxAdapter()
|
||||||
|
expect(adapter.name).toBe('minimax')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses MINIMAX_API_KEY by default', () => {
|
||||||
|
const original = process.env['MINIMAX_API_KEY']
|
||||||
|
process.env['MINIMAX_API_KEY'] = 'minimax-test-key-123'
|
||||||
|
|
||||||
|
try {
|
||||||
|
new MiniMaxAdapter()
|
||||||
|
expect(OpenAIMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
apiKey: 'minimax-test-key-123',
|
||||||
|
baseURL: 'https://api.minimax.io/v1',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
if (original === undefined) {
|
||||||
|
delete process.env['MINIMAX_API_KEY']
|
||||||
|
} else {
|
||||||
|
process.env['MINIMAX_API_KEY'] = original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses official MiniMax global baseURL by default', () => {
|
||||||
|
new MiniMaxAdapter('some-key')
|
||||||
|
expect(OpenAIMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
apiKey: 'some-key',
|
||||||
|
baseURL: 'https://api.minimax.io/v1',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses MINIMAX_BASE_URL env var when set', () => {
|
||||||
|
const original = process.env['MINIMAX_BASE_URL']
|
||||||
|
process.env['MINIMAX_BASE_URL'] = 'https://api.minimaxi.com/v1'
|
||||||
|
|
||||||
|
try {
|
||||||
|
new MiniMaxAdapter('some-key')
|
||||||
|
expect(OpenAIMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
apiKey: 'some-key',
|
||||||
|
baseURL: 'https://api.minimaxi.com/v1',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
if (original === undefined) {
|
||||||
|
delete process.env['MINIMAX_BASE_URL']
|
||||||
|
} else {
|
||||||
|
process.env['MINIMAX_BASE_URL'] = original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows overriding apiKey and baseURL', () => {
|
||||||
|
new MiniMaxAdapter('custom-key', 'https://custom.endpoint/v1')
|
||||||
|
expect(OpenAIMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
apiKey: 'custom-key',
|
||||||
|
baseURL: 'https://custom.endpoint/v1',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('createAdapter("minimax") returns MiniMaxAdapter instance', async () => {
|
||||||
|
const adapter = await createAdapter('minimax')
|
||||||
|
expect(adapter).toBeInstanceOf(MiniMaxAdapter)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -155,6 +155,80 @@ describe('OpenMultiAgent', () => {
|
||||||
expect(oma.getStatus().completedTasks).toBe(1)
|
expect(oma.getStatus().completedTasks).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('registers customTools so they are available to the LLM', async () => {
|
||||||
|
mockAdapterResponses = ['used custom tool']
|
||||||
|
|
||||||
|
const { z } = await import('zod')
|
||||||
|
const { defineTool } = await import('../src/tool/framework.js')
|
||||||
|
|
||||||
|
const myTool = defineTool({
|
||||||
|
name: 'my_custom_tool',
|
||||||
|
description: 'A custom tool for testing',
|
||||||
|
inputSchema: z.object({ query: z.string() }),
|
||||||
|
execute: async ({ query }) => ({ data: query }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
|
||||||
|
await oma.runAgent(
|
||||||
|
{ ...agentConfig('solo'), customTools: [myTool] },
|
||||||
|
'Use the custom tool',
|
||||||
|
)
|
||||||
|
|
||||||
|
const toolNames = capturedChatOptions[0]?.tools?.map(t => t.name) ?? []
|
||||||
|
expect(toolNames).toContain('my_custom_tool')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('customTools bypass tools allowlist and toolPreset filtering', async () => {
|
||||||
|
mockAdapterResponses = ['done']
|
||||||
|
|
||||||
|
const { z } = await import('zod')
|
||||||
|
const { defineTool } = await import('../src/tool/framework.js')
|
||||||
|
|
||||||
|
const myTool = defineTool({
|
||||||
|
name: 'my_custom_tool',
|
||||||
|
description: 'A custom tool for testing',
|
||||||
|
inputSchema: z.object({ query: z.string() }),
|
||||||
|
execute: async ({ query }) => ({ data: query }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
|
||||||
|
|
||||||
|
// toolPreset 'readonly' only allows file_read, grep, glob — custom tool should still appear
|
||||||
|
await oma.runAgent(
|
||||||
|
{ ...agentConfig('solo'), customTools: [myTool], toolPreset: 'readonly' },
|
||||||
|
'test',
|
||||||
|
)
|
||||||
|
|
||||||
|
const toolNames = capturedChatOptions[0]?.tools?.map(t => t.name) ?? []
|
||||||
|
expect(toolNames).toContain('my_custom_tool')
|
||||||
|
// built-in tools outside the preset should be filtered
|
||||||
|
expect(toolNames).not.toContain('bash')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('customTools can be blocked by disallowedTools', async () => {
|
||||||
|
mockAdapterResponses = ['done']
|
||||||
|
|
||||||
|
const { z } = await import('zod')
|
||||||
|
const { defineTool } = await import('../src/tool/framework.js')
|
||||||
|
|
||||||
|
const myTool = defineTool({
|
||||||
|
name: 'my_custom_tool',
|
||||||
|
description: 'A custom tool for testing',
|
||||||
|
inputSchema: z.object({ query: z.string() }),
|
||||||
|
execute: async ({ query }) => ({ data: query }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
|
||||||
|
|
||||||
|
await oma.runAgent(
|
||||||
|
{ ...agentConfig('solo'), customTools: [myTool], disallowedTools: ['my_custom_tool'] },
|
||||||
|
'test',
|
||||||
|
)
|
||||||
|
|
||||||
|
const toolNames = capturedChatOptions[0]?.tools?.map(t => t.name) ?? []
|
||||||
|
expect(toolNames).not.toContain('my_custom_tool')
|
||||||
|
})
|
||||||
|
|
||||||
it('fires onProgress events', async () => {
|
it('fires onProgress events', async () => {
|
||||||
mockAdapterResponses = ['done']
|
mockAdapterResponses = ['done']
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,8 +216,8 @@ describe('Tool filtering', () => {
|
||||||
const tools = (runner as any).resolveTools() as LLMToolDef[]
|
const tools = (runner as any).resolveTools() as LLMToolDef[]
|
||||||
const toolNames = tools.map((t: LLMToolDef) => t.name).sort()
|
const toolNames = tools.map((t: LLMToolDef) => t.name).sort()
|
||||||
|
|
||||||
|
// custom_tool is runtime-added but disallowedTools still blocks it
|
||||||
expect(toolNames).toEqual([
|
expect(toolNames).toEqual([
|
||||||
'custom_tool',
|
|
||||||
'file_edit',
|
'file_edit',
|
||||||
'file_read',
|
'file_read',
|
||||||
'file_write',
|
'file_write',
|
||||||
|
|
@ -286,7 +286,7 @@ describe('Tool filtering', () => {
|
||||||
expect(toolNames).toEqual(['custom_tool'])
|
expect(toolNames).toEqual(['custom_tool'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('runtime-added tools bypass filtering regardless of tool name', () => {
|
it('runtime-added tools are blocked by disallowedTools', () => {
|
||||||
const runtimeBuiltinNamedRegistry = new ToolRegistry()
|
const runtimeBuiltinNamedRegistry = new ToolRegistry()
|
||||||
runtimeBuiltinNamedRegistry.register(defineTool({
|
runtimeBuiltinNamedRegistry.register(defineTool({
|
||||||
name: 'file_read',
|
name: 'file_read',
|
||||||
|
|
@ -306,7 +306,7 @@ describe('Tool filtering', () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const tools = (runtimeBuiltinNamedRunner as any).resolveTools() as LLMToolDef[]
|
const tools = (runtimeBuiltinNamedRunner as any).resolveTools() as LLMToolDef[]
|
||||||
expect(tools.map(t => t.name)).toEqual(['file_read'])
|
expect(tools.map(t => t.name)).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue