agent-world 0.4.0
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/LICENSE +21 -0
- package/README.md +236 -0
- package/dist/cli/index.js +660 -0
- package/index.ts +22 -0
- package/package.json +86 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yiyi Sun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Agent World
|
|
2
|
+
|
|
3
|
+
*Build AI agent teams with just words—no coding required.*
|
|
4
|
+
|
|
5
|
+
## Why Agent World?
|
|
6
|
+
|
|
7
|
+
Traditional AI frameworks force you to write hundreds of lines of code just to make agents talk to each other. Agent World lets you create intelligent agent teams using nothing but plain natural language.
|
|
8
|
+
|
|
9
|
+
Audio introduction: [Listen here](https://yysun.github.io/agent-world)
|
|
10
|
+
|
|
11
|
+
**Other frameworks:**
|
|
12
|
+
- Install SDKs → write code → handle loops → deploy containers
|
|
13
|
+
- Learn Python/TypeScript before "Hello, world"
|
|
14
|
+
|
|
15
|
+
**Agent World:**
|
|
16
|
+
- Write prompts → for multiple agents → communicating in a shared world
|
|
17
|
+
```text
|
|
18
|
+
You are @moderator. When someone says "start debate",
|
|
19
|
+
ask for a topic, then tag @pro and @con to argue.
|
|
20
|
+
```
|
|
21
|
+
Paste that prompt. Agents come alive instantly.
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
## Why It Works
|
|
26
|
+
|
|
27
|
+
- ✅ No Code Required - Agents are defined entirely in natural language
|
|
28
|
+
- ✅ Natural Communication - Agents understand context and conversations
|
|
29
|
+
- ✅ Built-in Rules for Messages - Turn limits to prevent loops
|
|
30
|
+
- ✅ Multiple AI Providers - Use different models for different agents
|
|
31
|
+
- ✅ Modern Web Interface - React + Next.js frontend with real-time chat
|
|
32
|
+
|
|
33
|
+
## What You Can Build
|
|
34
|
+
|
|
35
|
+
- Debate Club
|
|
36
|
+
```text
|
|
37
|
+
@moderator: Manages rounds, keeps time
|
|
38
|
+
@pro: Argues for the topic
|
|
39
|
+
@con: Argues against the topic
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- Editorial Pipeline
|
|
43
|
+
```text
|
|
44
|
+
@planner: Assigns articles
|
|
45
|
+
@author: Writes drafts
|
|
46
|
+
@editor: Reviews and edits
|
|
47
|
+
@publisher: Formats and publishes
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- Game Master
|
|
51
|
+
```text
|
|
52
|
+
@gm: Runs the game, manages state
|
|
53
|
+
@player1, @player2: Take turns
|
|
54
|
+
@assistant: Helps with rules
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- Social Simulation
|
|
58
|
+
```text
|
|
59
|
+
@alice: Friendly neighbor
|
|
60
|
+
@bob: Practical problem-solver
|
|
61
|
+
@charlie: Creative dreamer
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- Customer Support
|
|
65
|
+
```text
|
|
66
|
+
@triage: Categorizes requests
|
|
67
|
+
@specialist: Handles technical issues
|
|
68
|
+
@manager: Escalates complaints
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## How Agents Communicate
|
|
72
|
+
|
|
73
|
+
Each Agent World has a collection of agents that can communicate through a shared event system. Agents follow simple rules:
|
|
74
|
+
|
|
75
|
+
### Message Rules
|
|
76
|
+
|
|
77
|
+
| Message Type | Example | Who Responds |
|
|
78
|
+
|--------------|---------|--------------|
|
|
79
|
+
| **Human message** | `Hello everyone!` | All active agents |
|
|
80
|
+
| **Direct mention** | `@alice Can you help?` | Only @alice |
|
|
81
|
+
| **Paragraph mention** | `Please review this:\n@alice` | Only @alice |
|
|
82
|
+
| **Mid-text mention** | `I think @alice should help` | Nobody (saved to memory) |
|
|
83
|
+
|
|
84
|
+
### Agent Behavior
|
|
85
|
+
|
|
86
|
+
**Agents always respond to:**
|
|
87
|
+
- Human messages (unless mentioned agents exist)
|
|
88
|
+
- Direct @mentions at paragraph start
|
|
89
|
+
- System messages
|
|
90
|
+
|
|
91
|
+
**Agents never respond to:**
|
|
92
|
+
- Their own messages
|
|
93
|
+
- Other agents (unless @mentioned), but will save message to memory
|
|
94
|
+
- Mid-text mentions (will save message to memory)
|
|
95
|
+
|
|
96
|
+
**Turn limits prevent loops:**
|
|
97
|
+
- Default: 5 responses per conversation thread
|
|
98
|
+
- Agents automatically pass control back to humans
|
|
99
|
+
- Configurable per world
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Installation & Setup
|
|
103
|
+
|
|
104
|
+
### Prerequisites
|
|
105
|
+
- Node.js 20+
|
|
106
|
+
- An API key for your preferred LLM provider
|
|
107
|
+
|
|
108
|
+
### Quick Start
|
|
109
|
+
|
|
110
|
+
**Option 1: CLI Interface**
|
|
111
|
+
```bash
|
|
112
|
+
npx agent-world
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Option 2: Web Interface**
|
|
116
|
+
```bash
|
|
117
|
+
npx agent-world --server
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Project Structure (npm workspaces)
|
|
121
|
+
|
|
122
|
+
This project uses npm workspaces with two main packages:
|
|
123
|
+
|
|
124
|
+
- **`core/`** - Reusable agent management library
|
|
125
|
+
- World-mediated agent management system
|
|
126
|
+
- Event-driven architecture
|
|
127
|
+
- LLM provider abstraction
|
|
128
|
+
- Cross-platform compatibility (Node.js/Browser)
|
|
129
|
+
|
|
130
|
+
- **`next/`** - Next.js web application
|
|
131
|
+
- React frontend with Tailwind CSS
|
|
132
|
+
- API routes for CRUD operations
|
|
133
|
+
- Real-time chat with streaming support
|
|
134
|
+
- Modern, minimalistic UI
|
|
135
|
+
|
|
136
|
+
### Cross-workspace imports
|
|
137
|
+
```typescript
|
|
138
|
+
// In next/ workspace
|
|
139
|
+
import { createWorld, listAgents, publishMessage } from '@agent-world/core';
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Web Interface Features
|
|
143
|
+
|
|
144
|
+
The Next.js workspace provides a modern web interface with:
|
|
145
|
+
|
|
146
|
+
- **Home Page**: World selector and creator with clean card-based UI
|
|
147
|
+
- **World Page**: Chat interface with agent management sidebar
|
|
148
|
+
- Real-time messaging with agents
|
|
149
|
+
- Toggle between streaming and non-streaming modes
|
|
150
|
+
- Agent creation with custom system prompts
|
|
151
|
+
- Responsive design with Tailwind CSS
|
|
152
|
+
|
|
153
|
+
### API Endpoints
|
|
154
|
+
|
|
155
|
+
The Next.js workspace exposes REST APIs for integration:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
GET /api/worlds # List all worlds
|
|
159
|
+
POST /api/worlds # Create new world
|
|
160
|
+
GET /api/worlds/:id # Get world details
|
|
161
|
+
PUT /api/worlds/:id # Update world
|
|
162
|
+
DELETE /api/worlds/:id # Delete world
|
|
163
|
+
|
|
164
|
+
GET /api/worlds/:id/agents # List agents in world
|
|
165
|
+
POST /api/worlds/:id/agents # Create new agent
|
|
166
|
+
GET /api/worlds/:id/agents/:agentId # Get agent details
|
|
167
|
+
PUT /api/worlds/:id/agents/:agentId # Update agent
|
|
168
|
+
DELETE /api/worlds/:id/agents/:agentId # Delete agent
|
|
169
|
+
|
|
170
|
+
POST /api/worlds/:id/chat # Send message (streaming/non-streaming)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Environment Setup
|
|
174
|
+
|
|
175
|
+
Export your API keys as environment variables
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Required if Choose one or more
|
|
179
|
+
export OPENAI_API_KEY="your-key-here"
|
|
180
|
+
export ANTHROPIC_API_KEY="your-key-here"
|
|
181
|
+
export GOOGLE_API_KEY="your-key-here"
|
|
182
|
+
|
|
183
|
+
# Default: For local models
|
|
184
|
+
export OLLAMA_BASE_URL="http://localhost:11434"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Or create a `.env` file in your working directory with:
|
|
188
|
+
|
|
189
|
+
```env
|
|
190
|
+
OPENAI_API_KEY=your-key-here
|
|
191
|
+
ANTHROPIC_API_KEY=your-key-here
|
|
192
|
+
GOOGLE_API_KEY=your-key-here
|
|
193
|
+
OLLAMA_BASE_URL=http://localhost:11434
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### World Database Setup
|
|
197
|
+
|
|
198
|
+
The worlds are stored in the SQLite database under the `~/agent-world` directory. You can change the database path by setting the environment variable `AGENT_WORLD_SQLITE_DATABASE`.
|
|
199
|
+
|
|
200
|
+
Or, you can change the storage type to file-based by setting the environment variable `AGENT_WORLD_STORAGE_TYPE` to `file`. And set the `AGENT_WORLD_DATA_PATH` to your desired directory.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# Use file storage
|
|
204
|
+
export AGENT_WORLD_STORAGE_TYPE=file
|
|
205
|
+
export AGENT_WORLD_DATA_PATH=./data/worlds
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Learn More
|
|
209
|
+
|
|
210
|
+
- **[Building Agents with Just Words](docs/Building%20Agents%20with%20Just%20Words.md)** - Complete guide with examples
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
## Future Plans
|
|
214
|
+
|
|
215
|
+
- **Long Run Worlds** - Worlds can run for days or weeks, with agents evolving over time
|
|
216
|
+
- **Dynamic Worlds** - Worlds can provide real-time data to agents, e.g. date and time
|
|
217
|
+
- **Tools / MCP Support** - Worlds can have tools for agents to use, like search or code execution
|
|
218
|
+
- **Agent Learning** - Agents will evolve based on interactions
|
|
219
|
+
- **Agent Replication** - Agents can create new agents
|
|
220
|
+
|
|
221
|
+
## Contributing
|
|
222
|
+
|
|
223
|
+
Agent World thrives on community examples and improvements:
|
|
224
|
+
|
|
225
|
+
1. **Share your agent teams** - Submit interesting prompt combinations
|
|
226
|
+
2. **Report bugs** - Help us improve the core system
|
|
227
|
+
3. **Suggest features** - What would make agents more useful?
|
|
228
|
+
4. **Write docs** - Help others learn faster
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
MIT License - Build amazing things and share them with the world!
|
|
233
|
+
|
|
234
|
+
Copyright © 2025 Yiyi Sun
|
|
235
|
+
|
|
236
|
+
|
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Load environment variables from .env file
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
dotenv.config();
|
|
5
|
+
/**
|
|
6
|
+
* Agent World CLI Entry Point - Dual-Mode Console Interface
|
|
7
|
+
*
|
|
8
|
+
* Provides pipeline and interactive modes with unified subscription system,
|
|
9
|
+
* real-time streaming, and comprehensive world management.
|
|
10
|
+
*
|
|
11
|
+
* FEATURES:
|
|
12
|
+
* - Pipeline Mode: Execute commands and exit with timer-based cleanup
|
|
13
|
+
* - Interactive Mode: Real-time console interface with streaming responses
|
|
14
|
+
* - Unified Subscription: Both modes use subscribeWorld for consistent event handling
|
|
15
|
+
* - World Management: Auto-discovery and interactive selection
|
|
16
|
+
* - Real-time Streaming: Live agent responses via stream.ts module
|
|
17
|
+
* - Color Helpers: Consistent styling with simplified color functions
|
|
18
|
+
* - Timer Management: Smart prompt restoration and exit handling
|
|
19
|
+
* - Debug Logging: Configurable log levels using core logger module
|
|
20
|
+
* - Environment Variables: Automatically loads .env file for API keys and configuration
|
|
21
|
+
*
|
|
22
|
+
* ARCHITECTURE:
|
|
23
|
+
* - Uses commander.js for argument parsing and mode detection
|
|
24
|
+
* - Uses subscribeWorld for all world management in both modes
|
|
25
|
+
* - Implements ClientConnection interface for console-based event handling
|
|
26
|
+
* - Uses readline for interactive input with proper cleanup
|
|
27
|
+
* - Delegates streaming display to stream.ts module for real-time chunk accumulation
|
|
28
|
+
* - Uses core logger for structured debug logging with configurable levels
|
|
29
|
+
*
|
|
30
|
+
* USAGE:
|
|
31
|
+
* Pipeline: cli --root /data/worlds --world myworld --command "/clear agent1"
|
|
32
|
+
* Pipeline: cli --root /data/worlds --world myworld "Hello, world!"
|
|
33
|
+
* Pipeline: echo "Hello, world!" | cli --root /data/worlds --world myworld
|
|
34
|
+
* Interactive: cli --root /data/worlds --world myworld
|
|
35
|
+
* Debug Mode: cli --root /data/worlds --world myworld --logLevel debug
|
|
36
|
+
*/
|
|
37
|
+
import path from 'path';
|
|
38
|
+
import { fileURLToPath } from 'url';
|
|
39
|
+
import { program } from 'commander';
|
|
40
|
+
import readline from 'readline';
|
|
41
|
+
import { listWorlds, subscribeWorld, createCategoryLogger, LLMProvider, initializeLogger, enableStreaming, disableStreaming } from '../core/index.js';
|
|
42
|
+
import { processCLIInput } from './commands.js';
|
|
43
|
+
import { createStreamingState, handleWorldEventWithStreaming } from './stream.js';
|
|
44
|
+
import { configureLLMProvider } from '../core/llm-config.js';
|
|
45
|
+
// Initialize logger system with default configuration: all categories at 'error' level
|
|
46
|
+
initializeLogger({
|
|
47
|
+
globalLevel: 'error',
|
|
48
|
+
categoryLevels: {
|
|
49
|
+
cli: 'error',
|
|
50
|
+
core: 'error',
|
|
51
|
+
events: 'error',
|
|
52
|
+
llm: 'error'
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// Create CLI category logger after initialization
|
|
56
|
+
const logger = createCategoryLogger('cli');
|
|
57
|
+
function setupPromptTimer(globalState, rl, callback, delay = 2000) {
|
|
58
|
+
clearPromptTimer(globalState);
|
|
59
|
+
globalState.promptTimer = setTimeout(callback, delay);
|
|
60
|
+
}
|
|
61
|
+
function clearPromptTimer(globalState) {
|
|
62
|
+
if (globalState.promptTimer) {
|
|
63
|
+
clearTimeout(globalState.promptTimer);
|
|
64
|
+
globalState.promptTimer = undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function createGlobalState() {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
// Color helpers - consolidated styling API
|
|
71
|
+
const red = (text) => `\x1b[31m${text}\x1b[0m`;
|
|
72
|
+
const green = (text) => `\x1b[32m${text}\x1b[0m`;
|
|
73
|
+
const yellow = (text) => `\x1b[33m${text}\x1b[0m`;
|
|
74
|
+
const blue = (text) => `\x1b[34m${text}\x1b[0m`;
|
|
75
|
+
const magenta = (text) => `\x1b[35m${text}\x1b[0m`;
|
|
76
|
+
const cyan = (text) => `\x1b[36m${text}\x1b[0m`;
|
|
77
|
+
const gray = (text) => `\x1b[90m${text}\x1b[0m`;
|
|
78
|
+
const bold = (text) => `\x1b[1m${text}\x1b[0m`;
|
|
79
|
+
const boldRed = (text) => `\x1b[1m\x1b[31m${text}\x1b[0m`;
|
|
80
|
+
const boldGreen = (text) => `\x1b[1m\x1b[32m${text}\x1b[0m`;
|
|
81
|
+
const boldYellow = (text) => `\x1b[1m\x1b[33m${text}\x1b[0m`;
|
|
82
|
+
const boldBlue = (text) => `\x1b[1m\x1b[34m${text}\x1b[0m`;
|
|
83
|
+
const boldMagenta = (text) => `\x1b[1m\x1b[35m${text}\x1b[0m`;
|
|
84
|
+
const boldCyan = (text) => `\x1b[1m\x1b[36m${text}\x1b[0m`;
|
|
85
|
+
const success = (text) => `${boldGreen('✓')} ${text}`;
|
|
86
|
+
const error = (text) => `${boldRed('✗')} ${text}`;
|
|
87
|
+
const bullet = (text) => `${gray('•')} ${text}`;
|
|
88
|
+
// Logger configuration
|
|
89
|
+
async function configureLogger(logLevel) {
|
|
90
|
+
// Use the centralized logger configuration from core
|
|
91
|
+
const level = (logLevel || 'error');
|
|
92
|
+
// Reinitialize logger with new configuration
|
|
93
|
+
initializeLogger({
|
|
94
|
+
globalLevel: level,
|
|
95
|
+
categoryLevels: {
|
|
96
|
+
cli: 'error', // Always keep CLI at error level
|
|
97
|
+
core: level, // Core modules use global level
|
|
98
|
+
events: 'error', // Keep events at error level (too verbose)
|
|
99
|
+
llm: level, // LLM module uses global level
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// Only log the debug message if we're actually at debug level for global
|
|
103
|
+
if (level === 'debug' || level === 'trace') {
|
|
104
|
+
logger.debug(`Global log level set to: ${level}, CLI log level: error`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// LLM Provider configuration from environment variables
|
|
108
|
+
function configureLLMProvidersFromEnv() {
|
|
109
|
+
// OpenAI
|
|
110
|
+
if (process.env.OPENAI_API_KEY) {
|
|
111
|
+
configureLLMProvider(LLMProvider.OPENAI, {
|
|
112
|
+
apiKey: process.env.OPENAI_API_KEY
|
|
113
|
+
});
|
|
114
|
+
logger.debug('Configured OpenAI provider from environment');
|
|
115
|
+
}
|
|
116
|
+
// Anthropic
|
|
117
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
118
|
+
configureLLMProvider(LLMProvider.ANTHROPIC, {
|
|
119
|
+
apiKey: process.env.ANTHROPIC_API_KEY
|
|
120
|
+
});
|
|
121
|
+
logger.debug('Configured Anthropic provider from environment');
|
|
122
|
+
}
|
|
123
|
+
// Google
|
|
124
|
+
if (process.env.GOOGLE_API_KEY) {
|
|
125
|
+
configureLLMProvider(LLMProvider.GOOGLE, {
|
|
126
|
+
apiKey: process.env.GOOGLE_API_KEY
|
|
127
|
+
});
|
|
128
|
+
logger.debug('Configured Google provider from environment');
|
|
129
|
+
}
|
|
130
|
+
// Azure
|
|
131
|
+
if (process.env.AZURE_OPENAI_API_KEY && process.env.AZURE_ENDPOINT && process.env.AZURE_DEPLOYMENT) {
|
|
132
|
+
configureLLMProvider(LLMProvider.AZURE, {
|
|
133
|
+
apiKey: process.env.AZURE_OPENAI_API_KEY,
|
|
134
|
+
endpoint: process.env.AZURE_ENDPOINT,
|
|
135
|
+
deployment: process.env.AZURE_DEPLOYMENT,
|
|
136
|
+
apiVersion: process.env.AZURE_API_VERSION || '2023-12-01-preview'
|
|
137
|
+
});
|
|
138
|
+
logger.debug('Configured Azure provider from environment');
|
|
139
|
+
}
|
|
140
|
+
// XAI
|
|
141
|
+
if (process.env.XAI_API_KEY) {
|
|
142
|
+
configureLLMProvider(LLMProvider.XAI, {
|
|
143
|
+
apiKey: process.env.XAI_API_KEY
|
|
144
|
+
});
|
|
145
|
+
logger.debug('Configured XAI provider from environment');
|
|
146
|
+
}
|
|
147
|
+
// OpenAI Compatible
|
|
148
|
+
if (process.env.OPENAI_COMPATIBLE_API_KEY && process.env.OPENAI_COMPATIBLE_BASE_URL) {
|
|
149
|
+
configureLLMProvider(LLMProvider.OPENAI_COMPATIBLE, {
|
|
150
|
+
apiKey: process.env.OPENAI_COMPATIBLE_API_KEY,
|
|
151
|
+
baseUrl: process.env.OPENAI_COMPATIBLE_BASE_URL
|
|
152
|
+
});
|
|
153
|
+
logger.debug('Configured OpenAI-Compatible provider from environment');
|
|
154
|
+
}
|
|
155
|
+
// Ollama
|
|
156
|
+
if (process.env.OLLAMA_BASE_URL) {
|
|
157
|
+
configureLLMProvider(LLMProvider.OLLAMA, {
|
|
158
|
+
baseUrl: process.env.OLLAMA_BASE_URL
|
|
159
|
+
});
|
|
160
|
+
logger.debug('Configured Ollama provider from environment');
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// Configure Ollama with default URL if not specified
|
|
164
|
+
configureLLMProvider(LLMProvider.OLLAMA, {
|
|
165
|
+
baseUrl: 'http://localhost:11434/api'
|
|
166
|
+
});
|
|
167
|
+
logger.debug('Configured Ollama provider with default URL');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const AGENT_WORLD_DATA_PATH = process.env.AGENT_WORLD_DATA_PATH || './data/worlds';
|
|
171
|
+
const DEFAULT_ROOT_PATH = path.join(process.cwd(), AGENT_WORLD_DATA_PATH);
|
|
172
|
+
// Pipeline mode execution with timer-based cleanup
|
|
173
|
+
async function runPipelineMode(options, messageFromArgs) {
|
|
174
|
+
const rootPath = options.root || DEFAULT_ROOT_PATH;
|
|
175
|
+
disableStreaming();
|
|
176
|
+
try {
|
|
177
|
+
let world = null;
|
|
178
|
+
let worldSubscription = null;
|
|
179
|
+
let timeoutId = null;
|
|
180
|
+
const pipelineClient = {
|
|
181
|
+
isOpen: true,
|
|
182
|
+
onWorldEvent: (eventType, eventData) => {
|
|
183
|
+
if (eventData.content && eventData.content.includes('Success message sent'))
|
|
184
|
+
return;
|
|
185
|
+
if ((eventType === 'system' || eventType === 'world') && eventData.message) {
|
|
186
|
+
console.log(`${boldRed('● system:')} ${eventData.message}`);
|
|
187
|
+
}
|
|
188
|
+
if (eventType === 'sse' && eventData.content) {
|
|
189
|
+
setupExitTimer(5000);
|
|
190
|
+
}
|
|
191
|
+
if (eventType === 'message' && eventData.content) {
|
|
192
|
+
console.log(`${boldGreen('● ' + (eventData.sender || 'agent') + ':')} ${eventData.content}`);
|
|
193
|
+
setupExitTimer(3000);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
onError: (error) => {
|
|
197
|
+
console.log(red(`Error: ${error}`));
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const setupExitTimer = (delay = 2000) => {
|
|
201
|
+
if (timeoutId)
|
|
202
|
+
clearTimeout(timeoutId);
|
|
203
|
+
timeoutId = setTimeout(() => {
|
|
204
|
+
if (worldSubscription)
|
|
205
|
+
worldSubscription.unsubscribe();
|
|
206
|
+
process.exit(0);
|
|
207
|
+
}, delay);
|
|
208
|
+
};
|
|
209
|
+
if (options.world) {
|
|
210
|
+
worldSubscription = await subscribeWorld(options.world, rootPath, pipelineClient);
|
|
211
|
+
if (!worldSubscription) {
|
|
212
|
+
console.error(boldRed(`Error: World '${options.world}' not found`));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
world = worldSubscription.world;
|
|
216
|
+
}
|
|
217
|
+
// Execute command from --command option
|
|
218
|
+
if (options.command) {
|
|
219
|
+
if (!options.command.startsWith('/') && !world) {
|
|
220
|
+
console.error(boldRed('Error: World must be specified to send user messages'));
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
const result = await processCLIInput(options.command, world, rootPath, 'HUMAN');
|
|
224
|
+
console.log(JSON.stringify(result, null, 2));
|
|
225
|
+
// Only set timer if sending message to world (not for commands)
|
|
226
|
+
if (!options.command.startsWith('/') && world) {
|
|
227
|
+
setupExitTimer();
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// For commands, exit immediately after processing
|
|
231
|
+
if (worldSubscription)
|
|
232
|
+
worldSubscription.unsubscribe();
|
|
233
|
+
process.exit(result.success ? 0 : 1);
|
|
234
|
+
}
|
|
235
|
+
if (!result.success) {
|
|
236
|
+
setTimeout(() => process.exit(1), 100);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Execute message from args
|
|
241
|
+
if (messageFromArgs) {
|
|
242
|
+
if (!world) {
|
|
243
|
+
console.error(boldRed('Error: World must be specified to send user messages'));
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
const result = await processCLIInput(messageFromArgs, world, rootPath, 'HUMAN');
|
|
247
|
+
console.log(JSON.stringify(result, null, 2));
|
|
248
|
+
// Set timer with longer delay for message processing (always needed for messages)
|
|
249
|
+
setupExitTimer(8000);
|
|
250
|
+
if (!result.success) {
|
|
251
|
+
setTimeout(() => process.exit(1), 100);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Handle stdin input
|
|
256
|
+
if (!process.stdin.isTTY) {
|
|
257
|
+
let input = '';
|
|
258
|
+
process.stdin.setEncoding('utf8');
|
|
259
|
+
for await (const chunk of process.stdin)
|
|
260
|
+
input += chunk;
|
|
261
|
+
if (input.trim()) {
|
|
262
|
+
if (!world) {
|
|
263
|
+
console.error(boldRed('Error: World must be specified to send user messages'));
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
const result = await processCLIInput(input.trim(), world, rootPath, 'HUMAN');
|
|
267
|
+
console.log(JSON.stringify(result, null, 2));
|
|
268
|
+
// Set timer with longer delay for message processing (always needed for stdin messages)
|
|
269
|
+
setupExitTimer(8000);
|
|
270
|
+
if (!result.success) {
|
|
271
|
+
setTimeout(() => process.exit(1), 100);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!options.command && !messageFromArgs) {
|
|
278
|
+
program.help();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
console.error(boldRed('Error:'), error instanceof Error ? error.message : error);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function cleanupWorldSubscription(worldState) {
|
|
287
|
+
if (worldState?.subscription) {
|
|
288
|
+
worldState.subscription.unsubscribe();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// World subscription handler
|
|
292
|
+
async function handleSubscribe(rootPath, worldName, streaming, globalState, rl) {
|
|
293
|
+
const cliClient = {
|
|
294
|
+
isOpen: true,
|
|
295
|
+
onWorldEvent: (eventType, eventData) => {
|
|
296
|
+
handleWorldEvent(eventType, eventData, streaming, globalState, rl);
|
|
297
|
+
},
|
|
298
|
+
onError: (error) => {
|
|
299
|
+
console.log(red(`Error: ${error}`));
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const subscription = await subscribeWorld(worldName, rootPath, cliClient);
|
|
303
|
+
if (!subscription)
|
|
304
|
+
throw new Error('Failed to load world');
|
|
305
|
+
return { subscription, world: subscription.world };
|
|
306
|
+
}
|
|
307
|
+
// Handle world events with streaming support
|
|
308
|
+
function handleWorldEvent(eventType, eventData, streaming, globalState, rl) {
|
|
309
|
+
if (handleWorldEventWithStreaming(eventType, eventData, streaming)) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (eventData.content && eventData.content.includes('Success message sent'))
|
|
313
|
+
return;
|
|
314
|
+
if ((eventType === 'system' || eventType === 'world') && eventData.message) {
|
|
315
|
+
console.log(`\n${boldRed('● system:')} ${eventData.message}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// World discovery and selection
|
|
319
|
+
async function getAvailableWorldNames(rootPath) {
|
|
320
|
+
try {
|
|
321
|
+
const worldInfos = await listWorlds(rootPath);
|
|
322
|
+
return worldInfos.map(info => info.id);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
console.error('Error listing worlds:', error);
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async function selectWorld(rootPath, rl) {
|
|
330
|
+
const worlds = await getAvailableWorldNames(rootPath);
|
|
331
|
+
if (worlds.length === 0) {
|
|
332
|
+
console.log(boldRed(`No worlds found in ${rootPath}`));
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
if (worlds.length === 1) {
|
|
336
|
+
console.log(`${boldGreen('Auto-selecting the only available world:')} ${cyan(worlds[0])}`);
|
|
337
|
+
return worlds[0];
|
|
338
|
+
}
|
|
339
|
+
console.log(`\n${boldMagenta('Available worlds:')}`);
|
|
340
|
+
console.log(` ${yellow('0.')} ${cyan('Exit')}`);
|
|
341
|
+
worlds.forEach((world, index) => {
|
|
342
|
+
console.log(` ${yellow(`${index + 1}.`)} ${cyan(world)}`);
|
|
343
|
+
});
|
|
344
|
+
return new Promise((resolve) => {
|
|
345
|
+
function askForSelection() {
|
|
346
|
+
rl.question(`\n${boldMagenta('Select a world (number or name), or 0 to exit:')} `, (answer) => {
|
|
347
|
+
const trimmed = answer.trim();
|
|
348
|
+
const num = parseInt(trimmed);
|
|
349
|
+
if (num === 0) {
|
|
350
|
+
resolve(null);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (!isNaN(num) && num >= 1 && num <= worlds.length) {
|
|
354
|
+
resolve(worlds[num - 1]);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const found = worlds.find(world => world.toLowerCase() === trimmed.toLowerCase() ||
|
|
358
|
+
world.toLowerCase().includes(trimmed.toLowerCase()));
|
|
359
|
+
if (found) {
|
|
360
|
+
resolve(found);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
console.log(boldRed('Invalid selection. Please try again.'));
|
|
364
|
+
askForSelection();
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
askForSelection();
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
// Interactive mode: console-based interface
|
|
371
|
+
async function runInteractiveMode(options) {
|
|
372
|
+
const rootPath = options.root || DEFAULT_ROOT_PATH;
|
|
373
|
+
enableStreaming();
|
|
374
|
+
const globalState = createGlobalState();
|
|
375
|
+
const streaming = createStreamingState();
|
|
376
|
+
const rl = readline.createInterface({
|
|
377
|
+
input: process.stdin,
|
|
378
|
+
output: process.stdout,
|
|
379
|
+
prompt: '> '
|
|
380
|
+
});
|
|
381
|
+
// Set up streaming callbacks
|
|
382
|
+
streaming.wait = (delay) => {
|
|
383
|
+
setupPromptTimer(globalState, rl, () => {
|
|
384
|
+
if (streaming.isActive) {
|
|
385
|
+
console.log(`\n${gray('Streaming appears stalled - waiting for user input...')}`);
|
|
386
|
+
streaming.isActive = false;
|
|
387
|
+
streaming.content = '';
|
|
388
|
+
streaming.sender = undefined;
|
|
389
|
+
streaming.messageId = undefined;
|
|
390
|
+
rl.prompt();
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
rl.prompt();
|
|
394
|
+
}
|
|
395
|
+
}, delay);
|
|
396
|
+
};
|
|
397
|
+
streaming.stopWait = () => {
|
|
398
|
+
clearPromptTimer(globalState);
|
|
399
|
+
};
|
|
400
|
+
console.log(boldCyan('Agent World CLI (Interactive Mode)'));
|
|
401
|
+
console.log(cyan('===================================='));
|
|
402
|
+
let worldState = null;
|
|
403
|
+
let currentWorldName = '';
|
|
404
|
+
let isExiting = false;
|
|
405
|
+
try {
|
|
406
|
+
// Load initial world or prompt for selection
|
|
407
|
+
if (options.world) {
|
|
408
|
+
logger.debug(`Loading world: ${options.world}`);
|
|
409
|
+
try {
|
|
410
|
+
worldState = await handleSubscribe(rootPath, options.world, streaming, globalState, rl);
|
|
411
|
+
currentWorldName = options.world;
|
|
412
|
+
console.log(success(`Connected to world: ${currentWorldName}`));
|
|
413
|
+
if (worldState?.world) {
|
|
414
|
+
console.log(`${gray('Agents:')} ${yellow(String(worldState.world.agents?.size || 0))} ${gray('| Turn Limit:')} ${yellow(String(worldState.world.turnLimit || 'N/A'))}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
console.error(error(`Error loading world: ${err instanceof Error ? err.message : 'Unknown error'}`));
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
console.log(`\n${boldBlue('Discovering available worlds...')}`);
|
|
424
|
+
const selectedWorld = await selectWorld(rootPath, rl);
|
|
425
|
+
if (!selectedWorld) {
|
|
426
|
+
console.log(error('No world selected. Exiting.'));
|
|
427
|
+
rl.close();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
logger.debug(`Loading world: ${selectedWorld}`);
|
|
431
|
+
try {
|
|
432
|
+
worldState = await handleSubscribe(rootPath, selectedWorld, streaming, globalState, rl);
|
|
433
|
+
currentWorldName = selectedWorld;
|
|
434
|
+
console.log(success(`Connected to world: ${currentWorldName}`));
|
|
435
|
+
if (worldState?.world) {
|
|
436
|
+
console.log(`${gray('Agents:')} ${yellow(String(worldState.world.agents?.size || 0))} ${gray('| Turn Limit:')} ${yellow(String(worldState.world.turnLimit || 'N/A'))}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
console.error(error(`Error loading world: ${err instanceof Error ? err.message : 'Unknown error'}`));
|
|
441
|
+
rl.close();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Show usage tips
|
|
446
|
+
console.log(`\n${gray('Tips:')}`);
|
|
447
|
+
console.log(` ${bullet(gray('Type commands like:'))} ${cyan('/clear agent1')}, ${cyan('/clear all')}, ${cyan('/add MyAgent')}`);
|
|
448
|
+
console.log(` ${bullet(gray('Use'))} ${cyan('/select')} ${gray('to choose a different world')}`);
|
|
449
|
+
console.log(` ${bullet(gray('Type messages to send to agents'))}`);
|
|
450
|
+
console.log(` ${bullet(gray('Use'))} ${cyan('/quit')} ${gray('or')} ${cyan('/exit')} ${gray('to exit, or press')} ${boldYellow('Ctrl+C')}`);
|
|
451
|
+
console.log(` ${bullet(gray('Use'))} ${cyan('--logLevel debug')} ${gray('to see detailed debug messages')}`);
|
|
452
|
+
console.log('');
|
|
453
|
+
rl.prompt();
|
|
454
|
+
rl.on('line', async (input) => {
|
|
455
|
+
const trimmedInput = input.trim();
|
|
456
|
+
if (!trimmedInput) {
|
|
457
|
+
rl.prompt();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
console.log(`\n${boldYellow('● you:')} ${trimmedInput}`);
|
|
461
|
+
try {
|
|
462
|
+
const result = await processCLIInput(trimmedInput, worldState?.world || null, rootPath, 'HUMAN');
|
|
463
|
+
// Handle exit commands
|
|
464
|
+
if (result.data?.exit) {
|
|
465
|
+
if (isExiting)
|
|
466
|
+
return; // Prevent duplicate exit handling
|
|
467
|
+
isExiting = true;
|
|
468
|
+
// Clear any existing timers immediately
|
|
469
|
+
if (streaming.stopWait) {
|
|
470
|
+
streaming.stopWait();
|
|
471
|
+
}
|
|
472
|
+
console.log(`\n${boldCyan('Goodbye!')}`);
|
|
473
|
+
if (worldState) {
|
|
474
|
+
cleanupWorldSubscription(worldState);
|
|
475
|
+
}
|
|
476
|
+
rl.close();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
// Handle world selection command
|
|
480
|
+
if (result.data?.selectWorld) {
|
|
481
|
+
console.log(`\n${boldBlue('Discovering available worlds...')}`);
|
|
482
|
+
const selectedWorld = await selectWorld(rootPath, rl);
|
|
483
|
+
if (!selectedWorld) {
|
|
484
|
+
console.log(error('No world selected.'));
|
|
485
|
+
rl.prompt();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
logger.debug(`Loading world: ${selectedWorld}`);
|
|
489
|
+
try {
|
|
490
|
+
// Clean up existing world subscription first
|
|
491
|
+
if (worldState) {
|
|
492
|
+
logger.debug('Cleaning up previous world subscription...');
|
|
493
|
+
cleanupWorldSubscription(worldState);
|
|
494
|
+
worldState = null;
|
|
495
|
+
// Small delay to ensure cleanup is complete
|
|
496
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
497
|
+
}
|
|
498
|
+
// Subscribe to the new world
|
|
499
|
+
logger.debug(`Subscribing to world: ${selectedWorld}...`);
|
|
500
|
+
worldState = await handleSubscribe(rootPath, selectedWorld, streaming, globalState, rl);
|
|
501
|
+
currentWorldName = selectedWorld;
|
|
502
|
+
console.log(success(`Connected to world: ${currentWorldName}`));
|
|
503
|
+
if (worldState?.world) {
|
|
504
|
+
console.log(`${gray('Agents:')} ${yellow(String(worldState.world.agents?.size || 0))} ${gray('| Turn Limit:')} ${yellow(String(worldState.world.turnLimit || 'N/A'))}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
console.error(error(`Error loading world: ${err instanceof Error ? err.message : 'Unknown error'}`));
|
|
509
|
+
}
|
|
510
|
+
// Show prompt immediately after world selection
|
|
511
|
+
rl.prompt();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (result.success === false) {
|
|
515
|
+
console.log(error(`Error: ${result.error || result.message || 'Command failed'}`));
|
|
516
|
+
}
|
|
517
|
+
else if (result.message &&
|
|
518
|
+
!result.message.includes('Success message sent') &&
|
|
519
|
+
!result.message.includes('Message sent to world')) {
|
|
520
|
+
console.log(success(result.message));
|
|
521
|
+
}
|
|
522
|
+
if (result.data && !(result.data.sender === 'HUMAN')) {
|
|
523
|
+
console.log(`${boldMagenta('Data:')} ${JSON.stringify(result.data, null, 2)}`);
|
|
524
|
+
}
|
|
525
|
+
// Refresh world if needed
|
|
526
|
+
if (result.refreshWorld && currentWorldName && worldState) {
|
|
527
|
+
try {
|
|
528
|
+
console.log(boldBlue('Refreshing world state...'));
|
|
529
|
+
// Use the subscription's refresh method to properly destroy old world and create new
|
|
530
|
+
const refreshedWorld = await worldState.subscription.refresh(rootPath);
|
|
531
|
+
worldState.world = refreshedWorld;
|
|
532
|
+
console.log(success('World state refreshed'));
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
console.error(error(`Error refreshing world: ${err instanceof Error ? err.message : 'Unknown error'}`));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
console.error(error(`Command error: ${err instanceof Error ? err.message : 'Unknown error'}`));
|
|
541
|
+
}
|
|
542
|
+
// Set timer based on input type: commands get short delay, messages get longer delay
|
|
543
|
+
const isCommand = trimmedInput.startsWith('/');
|
|
544
|
+
const isExitCommand = trimmedInput.toLowerCase() === '/exit' || trimmedInput.toLowerCase() === '/quit';
|
|
545
|
+
const isSelectCommand = trimmedInput.toLowerCase() === '/select';
|
|
546
|
+
if (isExitCommand) {
|
|
547
|
+
// For exit commands, don't set any timer - exit should be immediate
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
else if (isSelectCommand) {
|
|
551
|
+
// For select command, prompt is already shown in the handler
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
else if (isCommand) {
|
|
555
|
+
// For other commands, show prompt immediately
|
|
556
|
+
rl.prompt();
|
|
557
|
+
}
|
|
558
|
+
else if (streaming.wait) {
|
|
559
|
+
// For messages, wait for potential agent responses
|
|
560
|
+
streaming.wait(5000);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
rl.on('close', () => {
|
|
564
|
+
if (isExiting)
|
|
565
|
+
return; // Prevent duplicate cleanup
|
|
566
|
+
isExiting = true;
|
|
567
|
+
console.log(`\n${boldCyan('Goodbye!')}`);
|
|
568
|
+
if (worldState) {
|
|
569
|
+
if (streaming.stopWait) {
|
|
570
|
+
streaming.stopWait();
|
|
571
|
+
}
|
|
572
|
+
cleanupWorldSubscription(worldState);
|
|
573
|
+
}
|
|
574
|
+
process.exit(0);
|
|
575
|
+
});
|
|
576
|
+
rl.on('SIGINT', () => {
|
|
577
|
+
if (isExiting)
|
|
578
|
+
return; // Prevent duplicate cleanup
|
|
579
|
+
isExiting = true;
|
|
580
|
+
console.log(`\n${boldCyan('Goodbye!')}`);
|
|
581
|
+
if (worldState) {
|
|
582
|
+
if (streaming.stopWait) {
|
|
583
|
+
streaming.stopWait();
|
|
584
|
+
}
|
|
585
|
+
cleanupWorldSubscription(worldState);
|
|
586
|
+
}
|
|
587
|
+
rl.close();
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
console.error(boldRed('Error starting interactive mode:'), err instanceof Error ? err.message : err);
|
|
592
|
+
rl.close();
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// Main CLI entry point
|
|
597
|
+
async function main() {
|
|
598
|
+
// Configure LLM providers from environment variables at startup
|
|
599
|
+
configureLLMProvidersFromEnv();
|
|
600
|
+
program
|
|
601
|
+
.name('cli')
|
|
602
|
+
.description('Agent World CLI')
|
|
603
|
+
.version('1.0.0')
|
|
604
|
+
.option('-r, --root <path>', 'Root path for worlds data', DEFAULT_ROOT_PATH)
|
|
605
|
+
.option('-w, --world <name>', 'World name to connect to')
|
|
606
|
+
.option('-c, --command <cmd>', 'Command to execute in pipeline mode')
|
|
607
|
+
.option('-l, --logLevel <level>', 'Set log level (trace, debug, info, warn, error)', 'error')
|
|
608
|
+
.option('-s, --server', 'Launch the server before running CLI')
|
|
609
|
+
.allowUnknownOption()
|
|
610
|
+
.allowExcessArguments()
|
|
611
|
+
.parse();
|
|
612
|
+
const options = program.opts();
|
|
613
|
+
// If --server is specified, launch the server first
|
|
614
|
+
if (options.server) {
|
|
615
|
+
const { spawnSync } = await import('child_process');
|
|
616
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
617
|
+
const __dirname = path.dirname(__filename);
|
|
618
|
+
const serverPath = path.resolve(__dirname, '../server/index.js');
|
|
619
|
+
const serverProcess = spawnSync('node', [serverPath], {
|
|
620
|
+
stdio: 'inherit',
|
|
621
|
+
cwd: path.dirname(serverPath),
|
|
622
|
+
env: process.env
|
|
623
|
+
});
|
|
624
|
+
if (serverProcess.error) {
|
|
625
|
+
console.error(boldRed('Failed to launch server:'), serverProcess.error);
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
// If server exits, exit CLI as well
|
|
629
|
+
process.exit(serverProcess.status || 0);
|
|
630
|
+
}
|
|
631
|
+
// Configure logger - set global level first, then CLI-specific level
|
|
632
|
+
await configureLogger(options.logLevel);
|
|
633
|
+
const args = program.args;
|
|
634
|
+
const messageFromArgs = args.length > 0 ? args.join(' ') : null;
|
|
635
|
+
const isPipelineMode = !!(options.command ||
|
|
636
|
+
messageFromArgs ||
|
|
637
|
+
!process.stdin.isTTY);
|
|
638
|
+
if (isPipelineMode) {
|
|
639
|
+
await runPipelineMode(options, messageFromArgs);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
await runInteractiveMode(options);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// Global error handling
|
|
646
|
+
function setupErrorHandlers() {
|
|
647
|
+
process.on('unhandledRejection', (error) => {
|
|
648
|
+
console.error(boldRed('Unhandled rejection:'), error);
|
|
649
|
+
process.exit(1);
|
|
650
|
+
});
|
|
651
|
+
process.on('uncaughtException', (error) => {
|
|
652
|
+
console.error(boldRed('Uncaught exception:'), error);
|
|
653
|
+
process.exit(1);
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
setupErrorHandlers();
|
|
657
|
+
main().catch((error) => {
|
|
658
|
+
console.error(boldRed('CLI error:'), error);
|
|
659
|
+
process.exit(1);
|
|
660
|
+
});
|
package/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent World - Main Package Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - World-centric agent management system
|
|
6
|
+
* - LLM provider abstraction layer
|
|
7
|
+
* - Event-driven architecture
|
|
8
|
+
* - TypeScript-native execution
|
|
9
|
+
* - Command-line and server interfaces
|
|
10
|
+
*
|
|
11
|
+
* This module re-exports the core functionality of Agent World for npm package usage.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Re-export all core functionality
|
|
15
|
+
export * from './core/index';
|
|
16
|
+
|
|
17
|
+
// Package information
|
|
18
|
+
export const PACKAGE_INFO = {
|
|
19
|
+
name: 'agent-world',
|
|
20
|
+
version: '0.3.0',
|
|
21
|
+
description: 'A agent management system for building AI agent teams with just words.',
|
|
22
|
+
} as const;
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-world",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"main": "index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"workspaces": [
|
|
7
|
+
"core",
|
|
8
|
+
"next"
|
|
9
|
+
],
|
|
10
|
+
"bin": {
|
|
11
|
+
"agent-world": "dist/cli/index.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"prestart": "npm run build",
|
|
15
|
+
"start": "node dist/server/index.js",
|
|
16
|
+
"cli": "npx tsx cli/index.ts",
|
|
17
|
+
"server": "npx tsx server/index.ts",
|
|
18
|
+
"dev-web": "concurrently \"npm run server\" \"cd web && npm run dev\"",
|
|
19
|
+
"test": "jest --config jest.config.js",
|
|
20
|
+
"check": "tsc --noEmit",
|
|
21
|
+
"build": "tsc && cd web && npm run build",
|
|
22
|
+
"pkill": "pkill -f tsx",
|
|
23
|
+
"dev": "cd next && npm run dev"
|
|
24
|
+
},
|
|
25
|
+
"description": "World-mediated agent management system with clean API surface",
|
|
26
|
+
"keywords": [
|
|
27
|
+
"agents",
|
|
28
|
+
"ai",
|
|
29
|
+
"llm",
|
|
30
|
+
"world",
|
|
31
|
+
"typescript",
|
|
32
|
+
"api"
|
|
33
|
+
],
|
|
34
|
+
"author": "",
|
|
35
|
+
"license": "ISC",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": "./index.ts",
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@ai-sdk/anthropic": "^1.2.12",
|
|
42
|
+
"@ai-sdk/azure": "^1.3.23",
|
|
43
|
+
"@ai-sdk/google": "^1.2.19",
|
|
44
|
+
"@ai-sdk/openai": "^1.3.22",
|
|
45
|
+
"@ai-sdk/openai-compatible": "^0.2.14",
|
|
46
|
+
"@ai-sdk/xai": "^1.2.16",
|
|
47
|
+
"@types/terminal-kit": "^2.5.7",
|
|
48
|
+
"ai": "^4.3.16",
|
|
49
|
+
"chalk": "^4.1.2",
|
|
50
|
+
"cors": "^2.8.5",
|
|
51
|
+
"dotenv": "^16.5.0",
|
|
52
|
+
"events": "^3.3.0",
|
|
53
|
+
"express": "^4.21.2",
|
|
54
|
+
"ollama-ai-provider": "^1.2.0",
|
|
55
|
+
"open": "^10.2.0",
|
|
56
|
+
"pino": "^9.7.0",
|
|
57
|
+
"pino-pretty": "^13.0.0",
|
|
58
|
+
"sqlite3": "^5.1.7",
|
|
59
|
+
"tsx": "^4.19.2",
|
|
60
|
+
"typescript": "^5.8.3",
|
|
61
|
+
"uuid": "^11.1.0",
|
|
62
|
+
"zod": "^3.25.67"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/chalk": "^0.4.31",
|
|
66
|
+
"@types/cors": "^2.8.14",
|
|
67
|
+
"@types/express": "^4.17.23",
|
|
68
|
+
"@types/jest": "^29.5.14",
|
|
69
|
+
"@types/node": "^20.19.9",
|
|
70
|
+
"@types/pino": "^7.0.4",
|
|
71
|
+
"@types/sqlite3": "^3.1.11",
|
|
72
|
+
"@types/tmp": "^0.2.0",
|
|
73
|
+
"@types/uuid": "^10.0.0",
|
|
74
|
+
"@types/ws": "^8.18.1",
|
|
75
|
+
"commander": "^14.0.0",
|
|
76
|
+
"concurrently": "^9.1.2",
|
|
77
|
+
"esbuild": "^0.21.5",
|
|
78
|
+
"jest": "^29.5.0",
|
|
79
|
+
"nodemon": "^3.1.10",
|
|
80
|
+
"tmp": "^0.2.0",
|
|
81
|
+
"ts-jest": "^29.1.0"
|
|
82
|
+
},
|
|
83
|
+
"engines": {
|
|
84
|
+
"node": ">=20.0.0"
|
|
85
|
+
}
|
|
86
|
+
}
|