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 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
+ ![GitHub](docs/Screenshot-agents.png)
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
+ }