agent-world 0.6.2 → 0.8.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/README.md +42 -5
- package/dist/cli/index.js +550 -145
- package/package.json +19 -8
package/README.md
CHANGED
|
@@ -149,13 +149,50 @@ export OLLAMA_BASE_URL="http://localhost:11434"
|
|
|
149
149
|
|
|
150
150
|
Or create a `.env` file in your working directory with:
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
152
|
+
## Testing
|
|
153
|
+
|
|
154
|
+
**Run all tests:**
|
|
155
|
+
```bash
|
|
156
|
+
npm test # Run all unit tests
|
|
157
|
+
npm run test:watch # Watch mode with hot reload
|
|
158
|
+
npm run test:ui # Visual test UI
|
|
159
|
+
npm run test:coverage # Generate coverage report
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Run specific tests:**
|
|
163
|
+
```bash
|
|
164
|
+
npm test -- tests/core/events/ # Test a directory
|
|
165
|
+
npm test -- message-saving # Test files matching pattern
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Integration tests:**
|
|
169
|
+
```bash
|
|
170
|
+
npm run test:integration # Run integration tests with real filesystem
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Agent World uses Vitest for fast, modern testing with native TypeScript support.
|
|
174
|
+
|
|
175
|
+
## Logging and Debugging
|
|
176
|
+
|
|
177
|
+
Agent World uses **scenario-based logging** to help you debug specific issues without noise. Enable only the logs you need for your current task.
|
|
178
|
+
|
|
179
|
+
### Quick Examples
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Database migration issues
|
|
183
|
+
LOG_STORAGE_MIGRATION=info npm run server
|
|
184
|
+
|
|
185
|
+
# MCP server problems
|
|
186
|
+
LOG_MCP=debug npm run server
|
|
187
|
+
|
|
188
|
+
# Agent response debugging
|
|
189
|
+
LOG_EVENTS_AGENT=debug LOG_LLM=debug npm run server
|
|
157
190
|
```
|
|
158
191
|
|
|
192
|
+
**For complete logging documentation**, see [Logging Guide](docs/logging-guide.md).
|
|
193
|
+
|
|
194
|
+
## Learn More
|
|
195
|
+
|
|
159
196
|
### World Database Setup
|
|
160
197
|
|
|
161
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`.
|
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* cli/index.ts
|
|
4
|
+
* Summary: CLI entrypoint and interactive world subscription logic with event-driven prompt display.
|
|
5
|
+
* Implementation: Uses subscribeWorld to obtain a managed WorldSubscription, then subscribes directly to world.eventEmitter for CLI-specific handling.
|
|
6
|
+
* Architecture: Event-driven prompt display using world activity events instead of timers.
|
|
7
|
+
*/
|
|
2
8
|
// Load environment variables from .env file
|
|
3
9
|
import dotenv from 'dotenv';
|
|
4
10
|
dotenv.config({ quiet: true });
|
|
@@ -9,13 +15,18 @@ dotenv.config({ quiet: true });
|
|
|
9
15
|
* real-time streaming, and comprehensive world management.
|
|
10
16
|
*
|
|
11
17
|
* FEATURES:
|
|
12
|
-
* - Pipeline Mode: Execute commands and exit with
|
|
18
|
+
* - Pipeline Mode: Execute commands and exit with pure event-driven completion tracking
|
|
19
|
+
* - Uses world activity events (response-start, response-end, idle) for completion detection
|
|
20
|
+
* - No visible activity progress (clean output for scripting)
|
|
21
|
+
* - Extended timeout (120s) with quick exit on no activity (2s)
|
|
22
|
+
* - Silent timeout handling (no error messages for clean pipelines)
|
|
13
23
|
* - Interactive Mode: Real-time console interface with streaming responses
|
|
24
|
+
* - Full activity progress display with world events, tool execution, and streaming
|
|
25
|
+
* - Event-driven prompt display using world idle events
|
|
14
26
|
* - Unified Subscription: Both modes use subscribeWorld for consistent event handling
|
|
15
27
|
* - World Management: Auto-discovery and interactive selection
|
|
16
|
-
* - Real-time Streaming: Live agent responses via stream.ts module
|
|
28
|
+
* - Real-time Streaming: Live agent responses via stream.ts module (interactive only)
|
|
17
29
|
* - Color Helpers: Consistent styling with simplified color functions
|
|
18
|
-
* - Timer Management: Smart prompt restoration and exit handling
|
|
19
30
|
* - Debug Logging: Configurable log levels using core logger module
|
|
20
31
|
* - Environment Variables: Automatically loads .env file for API keys and configuration
|
|
21
32
|
*
|
|
@@ -26,6 +37,8 @@ dotenv.config({ quiet: true });
|
|
|
26
37
|
* - Uses readline for interactive input with proper cleanup
|
|
27
38
|
* - Delegates streaming display to stream.ts module for real-time chunk accumulation
|
|
28
39
|
* - Uses core logger for structured debug logging with configurable levels
|
|
40
|
+
* - Event-driven prompt display: listens to world 'idle' events instead of using timers
|
|
41
|
+
* - WorldActivityMonitor tracks agent processing and signals when world becomes idle
|
|
29
42
|
*
|
|
30
43
|
* USAGE:
|
|
31
44
|
* Pipeline: cli --root /data/worlds --world myworld --command "/clear agent1"
|
|
@@ -39,24 +52,17 @@ import { fileURLToPath } from 'url';
|
|
|
39
52
|
import { program } from 'commander';
|
|
40
53
|
import readline from 'readline';
|
|
41
54
|
import { listWorlds, subscribeWorld, createCategoryLogger, LLMProvider, enableStreaming, disableStreaming } from '../core/index.js';
|
|
55
|
+
import { EventType } from '../core/types.js';
|
|
42
56
|
import { getDefaultRootPath } from '../core/storage/storage-factory.js';
|
|
43
|
-
import { processCLIInput } from './commands.js';
|
|
44
|
-
import { createStreamingState, handleWorldEventWithStreaming, } from './stream.js';
|
|
57
|
+
import { processCLIInput, displayChatMessages } from './commands.js';
|
|
58
|
+
import { createStreamingState, handleWorldEventWithStreaming, handleToolEvents, handleActivityEvents, } from './stream.js';
|
|
45
59
|
import { configureLLMProvider } from '../core/llm-config.js';
|
|
46
60
|
// Create CLI category logger after logger auto-initialization
|
|
47
61
|
const logger = createCategoryLogger('cli');
|
|
48
|
-
function setupPromptTimer(globalState, rl, callback, delay = 2000) {
|
|
49
|
-
clearPromptTimer(globalState);
|
|
50
|
-
globalState.promptTimer = setTimeout(callback, delay);
|
|
51
|
-
}
|
|
52
|
-
function clearPromptTimer(globalState) {
|
|
53
|
-
if (globalState.promptTimer) {
|
|
54
|
-
clearTimeout(globalState.promptTimer);
|
|
55
|
-
globalState.promptTimer = undefined;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
62
|
function createGlobalState() {
|
|
59
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
awaitingResponse: false
|
|
65
|
+
};
|
|
60
66
|
}
|
|
61
67
|
// Color helpers - consolidated styling API
|
|
62
68
|
const red = (text) => `\x1b[31m${text}\x1b[0m`;
|
|
@@ -76,6 +82,175 @@ const boldCyan = (text) => `\x1b[1m\x1b[36m${text}\x1b[0m`;
|
|
|
76
82
|
const success = (text) => `${boldGreen('✓')} ${text}`;
|
|
77
83
|
const error = (text) => `${boldRed('✗')} ${text}`;
|
|
78
84
|
const bullet = (text) => `${gray('•')} ${text}`;
|
|
85
|
+
class WorldActivityMonitor {
|
|
86
|
+
lastEvent = null;
|
|
87
|
+
waiters = new Set();
|
|
88
|
+
captureSnapshot() {
|
|
89
|
+
return {
|
|
90
|
+
activityId: this.lastEvent?.activityId ?? 0,
|
|
91
|
+
type: this.lastEvent?.type ?? null
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
handle(event) {
|
|
95
|
+
// Check for valid event types
|
|
96
|
+
if (!event || (event.type !== 'response-start' && event.type !== 'response-end' && event.type !== 'idle')) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const timestampMsRaw = event.timestamp ? Date.parse(event.timestamp) : Date.now();
|
|
100
|
+
const timestampMs = Number.isFinite(timestampMsRaw) ? timestampMsRaw : Date.now();
|
|
101
|
+
this.lastEvent = {
|
|
102
|
+
...event,
|
|
103
|
+
timestampMs
|
|
104
|
+
};
|
|
105
|
+
for (const waiter of Array.from(this.waiters)) {
|
|
106
|
+
// Track when we see response-start after the target activity
|
|
107
|
+
if (event.type === 'response-start' && event.activityId > waiter.activityId) {
|
|
108
|
+
waiter.seenProcessing = true;
|
|
109
|
+
if (waiter.noActivityTimeoutId) {
|
|
110
|
+
clearTimeout(waiter.noActivityTimeoutId);
|
|
111
|
+
waiter.noActivityTimeoutId = undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Resolve waiter on idle event if conditions are met
|
|
115
|
+
if (event.type === 'idle') {
|
|
116
|
+
const shouldResolve = event.activityId > waiter.activityId ||
|
|
117
|
+
(event.activityId === waiter.activityId && waiter.seenProcessing);
|
|
118
|
+
if (shouldResolve) {
|
|
119
|
+
this.finishWaiter(waiter, true);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async waitForIdle(options = {}) {
|
|
125
|
+
const { snapshot = this.captureSnapshot(), timeoutMs = 120_000, // Default 2 minutes for complex operations
|
|
126
|
+
noActivityTimeoutMs = 2_000 // Default 2 seconds for quick exit
|
|
127
|
+
} = options;
|
|
128
|
+
const targetActivityId = snapshot.activityId;
|
|
129
|
+
// Already idle after target activity
|
|
130
|
+
if (this.lastEvent && this.lastEvent.type === 'idle' && this.lastEvent.activityId > targetActivityId) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const waiter = {
|
|
135
|
+
activityId: targetActivityId,
|
|
136
|
+
resolveCallback: () => {
|
|
137
|
+
cleanup();
|
|
138
|
+
resolve();
|
|
139
|
+
},
|
|
140
|
+
rejectCallback: (error) => {
|
|
141
|
+
cleanup();
|
|
142
|
+
reject(error);
|
|
143
|
+
},
|
|
144
|
+
seenProcessing: false
|
|
145
|
+
};
|
|
146
|
+
const cleanup = () => {
|
|
147
|
+
if (waiter.timeoutId) {
|
|
148
|
+
clearTimeout(waiter.timeoutId);
|
|
149
|
+
waiter.timeoutId = undefined;
|
|
150
|
+
}
|
|
151
|
+
if (waiter.noActivityTimeoutId) {
|
|
152
|
+
clearTimeout(waiter.noActivityTimeoutId);
|
|
153
|
+
waiter.noActivityTimeoutId = undefined;
|
|
154
|
+
}
|
|
155
|
+
this.waiters.delete(waiter);
|
|
156
|
+
};
|
|
157
|
+
const last = this.lastEvent;
|
|
158
|
+
if (!last) {
|
|
159
|
+
waiter.noActivityTimeoutId = setTimeout(() => this.finishWaiter(waiter, true), noActivityTimeoutMs);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// Track if we've seen response-start after target activity
|
|
163
|
+
if (last.type === 'response-start' && last.activityId > targetActivityId) {
|
|
164
|
+
waiter.seenProcessing = true;
|
|
165
|
+
}
|
|
166
|
+
// If already idle at target activity, set short timeout
|
|
167
|
+
if (last.type === 'idle' && last.activityId === targetActivityId) {
|
|
168
|
+
waiter.noActivityTimeoutId = setTimeout(() => this.finishWaiter(waiter, true), noActivityTimeoutMs);
|
|
169
|
+
}
|
|
170
|
+
// If already idle after target activity, resolve immediately
|
|
171
|
+
if (last.type === 'idle' && last.activityId > targetActivityId) {
|
|
172
|
+
resolve();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
waiter.timeoutId = setTimeout(() => {
|
|
177
|
+
this.finishWaiter(waiter, false, new Error('Timed out waiting for world to become idle'));
|
|
178
|
+
}, timeoutMs);
|
|
179
|
+
this.waiters.add(waiter);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
reset(reason = 'World subscription reset') {
|
|
183
|
+
const error = new Error(reason);
|
|
184
|
+
for (const waiter of Array.from(this.waiters)) {
|
|
185
|
+
this.finishWaiter(waiter, false, error);
|
|
186
|
+
}
|
|
187
|
+
this.lastEvent = null;
|
|
188
|
+
}
|
|
189
|
+
getActiveSources() {
|
|
190
|
+
return this.lastEvent?.activeSources ?? [];
|
|
191
|
+
}
|
|
192
|
+
finishWaiter(waiter, resolve, error) {
|
|
193
|
+
if (!this.waiters.has(waiter)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (waiter.timeoutId) {
|
|
197
|
+
clearTimeout(waiter.timeoutId);
|
|
198
|
+
waiter.timeoutId = undefined;
|
|
199
|
+
}
|
|
200
|
+
if (waiter.noActivityTimeoutId) {
|
|
201
|
+
clearTimeout(waiter.noActivityTimeoutId);
|
|
202
|
+
waiter.noActivityTimeoutId = undefined;
|
|
203
|
+
}
|
|
204
|
+
this.waiters.delete(waiter);
|
|
205
|
+
if (resolve) {
|
|
206
|
+
waiter.resolveCallback();
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
waiter.rejectCallback(error ?? new Error('World activity waiter cancelled'));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function parseActivitySource(source) {
|
|
214
|
+
if (!source)
|
|
215
|
+
return null;
|
|
216
|
+
if (source.startsWith('agent:')) {
|
|
217
|
+
return { type: 'agent', name: source.slice('agent:'.length) };
|
|
218
|
+
}
|
|
219
|
+
if (source.startsWith('message:')) {
|
|
220
|
+
return { type: 'message', name: source.slice('message:'.length) };
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
class ActivityProgressRenderer {
|
|
225
|
+
activeAgents = new Set();
|
|
226
|
+
handle(event) {
|
|
227
|
+
if (!event)
|
|
228
|
+
return;
|
|
229
|
+
// Reset on idle
|
|
230
|
+
if (event.type === 'idle') {
|
|
231
|
+
this.reset();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const details = parseActivitySource(event.source);
|
|
235
|
+
if (!details || details.type !== 'agent') {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// Track agent start on response-start
|
|
239
|
+
if (event.type === 'response-start' && !this.activeAgents.has(details.name)) {
|
|
240
|
+
this.activeAgents.add(details.name);
|
|
241
|
+
}
|
|
242
|
+
// Track agent end on response-end
|
|
243
|
+
if (event.type === 'response-end' && this.activeAgents.has(details.name)) {
|
|
244
|
+
this.activeAgents.delete(details.name);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
reset() {
|
|
248
|
+
if (this.activeAgents.size > 0) {
|
|
249
|
+
this.activeAgents.clear();
|
|
250
|
+
// console.log(gray('All agents finished.'));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
79
254
|
// LLM Provider configuration from environment variables
|
|
80
255
|
function configureLLMProvidersFromEnv() {
|
|
81
256
|
// OpenAI
|
|
@@ -156,53 +331,105 @@ function printCLIResult(result) {
|
|
|
156
331
|
console.log(error(result.error));
|
|
157
332
|
}
|
|
158
333
|
}
|
|
159
|
-
|
|
334
|
+
/**
|
|
335
|
+
* Attach CLI event listeners to world EventEmitter
|
|
336
|
+
*
|
|
337
|
+
* @param world - World instance to attach listeners to
|
|
338
|
+
* @param streaming - Streaming state for interactive mode (null for pipeline mode)
|
|
339
|
+
* @param globalState - Global state for interactive mode (null for pipeline mode)
|
|
340
|
+
* @param activityMonitor - Activity monitor for tracking world events
|
|
341
|
+
* @param progressRenderer - Progress renderer for displaying activity
|
|
342
|
+
* @param rl - Readline interface for interactive mode (undefined for pipeline mode)
|
|
343
|
+
* @returns Map of event types to listener functions for cleanup
|
|
344
|
+
*/
|
|
345
|
+
function attachCLIListeners(world, streaming, globalState, activityMonitor, progressRenderer, rl) {
|
|
346
|
+
const listeners = new Map();
|
|
347
|
+
// World activity events
|
|
348
|
+
const worldListener = (eventData) => {
|
|
349
|
+
activityMonitor.handle(eventData);
|
|
350
|
+
// Only render activity progress in interactive mode
|
|
351
|
+
if (streaming && globalState && rl) {
|
|
352
|
+
progressRenderer.handle(eventData);
|
|
353
|
+
handleWorldEvent(EventType.WORLD, eventData, streaming, globalState, activityMonitor, progressRenderer, rl);
|
|
354
|
+
}
|
|
355
|
+
// Pipeline mode: silently track events for completion detection
|
|
356
|
+
};
|
|
357
|
+
world.eventEmitter.on(EventType.WORLD, worldListener);
|
|
358
|
+
listeners.set(EventType.WORLD, worldListener);
|
|
359
|
+
// Message events
|
|
360
|
+
const messageListener = (eventData) => {
|
|
361
|
+
if (eventData.content && eventData.content.includes('Success message sent'))
|
|
362
|
+
return;
|
|
363
|
+
if (streaming && globalState && rl) {
|
|
364
|
+
handleWorldEvent(EventType.MESSAGE, eventData, streaming, globalState, activityMonitor, progressRenderer, rl);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
// Pipeline mode: simple console output
|
|
368
|
+
if (eventData.sender === 'system') {
|
|
369
|
+
console.log(`${boldRed('● system:')} ${eventData.content}`);
|
|
370
|
+
}
|
|
371
|
+
if (eventData.content) {
|
|
372
|
+
console.log(`${boldGreen('● ' + (eventData.sender || 'agent') + ':')} ${eventData.content}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
world.eventEmitter.on(EventType.MESSAGE, messageListener);
|
|
377
|
+
listeners.set(EventType.MESSAGE, messageListener);
|
|
378
|
+
// SSE events (interactive mode only - pipeline mode uses non-streaming LLM calls)
|
|
379
|
+
if (streaming && globalState && rl) {
|
|
380
|
+
const sseListener = (eventData) => {
|
|
381
|
+
handleWorldEvent(EventType.SSE, eventData, streaming, globalState, activityMonitor, progressRenderer, rl);
|
|
382
|
+
};
|
|
383
|
+
world.eventEmitter.on(EventType.SSE, sseListener);
|
|
384
|
+
listeners.set(EventType.SSE, sseListener);
|
|
385
|
+
}
|
|
386
|
+
// System events
|
|
387
|
+
const systemListener = (eventData) => {
|
|
388
|
+
if (eventData.content && eventData.content.includes('Success message sent'))
|
|
389
|
+
return;
|
|
390
|
+
if (streaming && globalState && rl) {
|
|
391
|
+
handleWorldEvent(EventType.SYSTEM, eventData, streaming, globalState, activityMonitor, progressRenderer, rl);
|
|
392
|
+
}
|
|
393
|
+
else if (eventData.message || eventData.content) {
|
|
394
|
+
// Pipeline mode: system messages are handled by message listener
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
world.eventEmitter.on(EventType.SYSTEM, systemListener);
|
|
398
|
+
listeners.set(EventType.SYSTEM, systemListener);
|
|
399
|
+
return listeners;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Cleanup CLI event listeners from world EventEmitter
|
|
403
|
+
*
|
|
404
|
+
* @param world - World instance to remove listeners from
|
|
405
|
+
* @param listeners - Map of event types to listener functions
|
|
406
|
+
*/
|
|
407
|
+
function detachCLIListeners(world, listeners) {
|
|
408
|
+
for (const [eventType, listener] of listeners.entries()) {
|
|
409
|
+
world.eventEmitter.removeListener(eventType, listener);
|
|
410
|
+
}
|
|
411
|
+
listeners.clear();
|
|
412
|
+
}
|
|
413
|
+
// Pipeline mode execution with event-driven completion tracking
|
|
160
414
|
async function runPipelineMode(options, messageFromArgs) {
|
|
161
415
|
disableStreaming();
|
|
416
|
+
let world = null;
|
|
417
|
+
let worldSubscription = null;
|
|
418
|
+
let cliListeners = null;
|
|
419
|
+
const activityMonitor = new WorldActivityMonitor();
|
|
420
|
+
const progressRenderer = new ActivityProgressRenderer();
|
|
162
421
|
try {
|
|
163
|
-
let world = null;
|
|
164
|
-
let worldSubscription = null;
|
|
165
|
-
let timeoutId = null;
|
|
166
|
-
const pipelineClient = {
|
|
167
|
-
isOpen: true,
|
|
168
|
-
onWorldEvent: (eventType, eventData) => {
|
|
169
|
-
if (eventData.content && eventData.content.includes('Success message sent'))
|
|
170
|
-
return;
|
|
171
|
-
if ((eventType === 'system' || eventType === 'world') && (eventData.message || eventData.content)) {
|
|
172
|
-
// existing logic
|
|
173
|
-
}
|
|
174
|
-
else if (eventType === 'message' && eventData.sender === 'system') {
|
|
175
|
-
const msg = eventData.content;
|
|
176
|
-
console.log(`${boldRed('● system:')} ${msg}`);
|
|
177
|
-
}
|
|
178
|
-
if (eventType === 'sse' && eventData.content) {
|
|
179
|
-
setupExitTimer(5000);
|
|
180
|
-
}
|
|
181
|
-
if (eventType === 'message' && eventData.content) {
|
|
182
|
-
console.log(`${boldGreen('● ' + (eventData.sender || 'agent') + ':')} ${eventData.content}`);
|
|
183
|
-
setupExitTimer(3000);
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
onError: (error) => {
|
|
187
|
-
console.log(red(`Error: ${error}`));
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
const setupExitTimer = (delay = 2000) => {
|
|
191
|
-
if (timeoutId)
|
|
192
|
-
clearTimeout(timeoutId);
|
|
193
|
-
timeoutId = setTimeout(() => {
|
|
194
|
-
if (worldSubscription)
|
|
195
|
-
worldSubscription.unsubscribe();
|
|
196
|
-
process.exit(0);
|
|
197
|
-
}, delay);
|
|
198
|
-
};
|
|
199
422
|
if (options.world) {
|
|
200
|
-
|
|
423
|
+
// Subscribe to world lifecycle but do not request forwarding callbacks
|
|
424
|
+
worldSubscription = await subscribeWorld(options.world, { isOpen: true });
|
|
201
425
|
if (!worldSubscription) {
|
|
202
426
|
console.error(boldRed(`Error: World '${options.world}' not found`));
|
|
203
427
|
process.exit(1);
|
|
204
428
|
}
|
|
205
429
|
world = worldSubscription.world;
|
|
430
|
+
// Attach direct listeners to the world.eventEmitter for pipeline handling
|
|
431
|
+
// Note: Pipeline mode uses non-streaming LLM calls, so SSE events are not needed
|
|
432
|
+
cliListeners = attachCLIListeners(world, null, null, activityMonitor, progressRenderer);
|
|
206
433
|
}
|
|
207
434
|
// Execute command from --command option
|
|
208
435
|
if (options.command) {
|
|
@@ -210,14 +437,24 @@ async function runPipelineMode(options, messageFromArgs) {
|
|
|
210
437
|
console.error(boldRed('Error: World must be specified to send user messages'));
|
|
211
438
|
process.exit(1);
|
|
212
439
|
}
|
|
213
|
-
const
|
|
440
|
+
const snapshot = activityMonitor.captureSnapshot();
|
|
441
|
+
const result = await processCLIInput(options.command, world, 'human');
|
|
214
442
|
printCLIResult(result);
|
|
215
|
-
// Only set timer if sending message to world (not for commands)
|
|
216
443
|
if (!options.command.startsWith('/') && world) {
|
|
217
|
-
|
|
444
|
+
try {
|
|
445
|
+
// Event-driven completion: wait for world idle state
|
|
446
|
+
await activityMonitor.waitForIdle({
|
|
447
|
+
snapshot,
|
|
448
|
+
timeoutMs: 120_000, // Extended timeout for complex operations
|
|
449
|
+
noActivityTimeoutMs: 2_000 // Quick exit if no activity detected
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
// Silent exit on timeout - events may have completed
|
|
454
|
+
logger.debug('Pipeline mode completion timeout', { error: error.message });
|
|
455
|
+
}
|
|
218
456
|
}
|
|
219
457
|
else {
|
|
220
|
-
// For commands, exit immediately after processing
|
|
221
458
|
if (worldSubscription)
|
|
222
459
|
worldSubscription.unsubscribe();
|
|
223
460
|
process.exit(result.success ? 0 : 1);
|
|
@@ -233,10 +470,21 @@ async function runPipelineMode(options, messageFromArgs) {
|
|
|
233
470
|
console.error(boldRed('Error: World must be specified to send user messages'));
|
|
234
471
|
process.exit(1);
|
|
235
472
|
}
|
|
236
|
-
const
|
|
473
|
+
const snapshot = activityMonitor.captureSnapshot();
|
|
474
|
+
const result = await processCLIInput(messageFromArgs, world, 'human');
|
|
237
475
|
printCLIResult(result);
|
|
238
|
-
|
|
239
|
-
|
|
476
|
+
try {
|
|
477
|
+
// Event-driven completion: wait for world idle state
|
|
478
|
+
await activityMonitor.waitForIdle({
|
|
479
|
+
snapshot,
|
|
480
|
+
timeoutMs: 120_000,
|
|
481
|
+
noActivityTimeoutMs: 2_000
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
// Silent exit on timeout - events may have completed
|
|
486
|
+
logger.debug('Pipeline mode completion timeout', { error: error.message });
|
|
487
|
+
}
|
|
240
488
|
if (!result.success) {
|
|
241
489
|
setTimeout(() => process.exit(1), 100);
|
|
242
490
|
return;
|
|
@@ -253,10 +501,21 @@ async function runPipelineMode(options, messageFromArgs) {
|
|
|
253
501
|
console.error(boldRed('Error: World must be specified to send user messages'));
|
|
254
502
|
process.exit(1);
|
|
255
503
|
}
|
|
504
|
+
const snapshot = activityMonitor.captureSnapshot();
|
|
256
505
|
const result = await processCLIInput(input.trim(), world, 'HUMAN');
|
|
257
506
|
printCLIResult(result);
|
|
258
|
-
|
|
259
|
-
|
|
507
|
+
try {
|
|
508
|
+
// Event-driven completion: wait for world idle state
|
|
509
|
+
await activityMonitor.waitForIdle({
|
|
510
|
+
snapshot,
|
|
511
|
+
timeoutMs: 120_000,
|
|
512
|
+
noActivityTimeoutMs: 2_000
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
// Silent exit on timeout - events may have completed
|
|
517
|
+
logger.debug('Pipeline mode completion timeout', { error: error.message });
|
|
518
|
+
}
|
|
260
519
|
if (!result.success) {
|
|
261
520
|
setTimeout(() => process.exit(1), 100);
|
|
262
521
|
return;
|
|
@@ -267,9 +526,22 @@ async function runPipelineMode(options, messageFromArgs) {
|
|
|
267
526
|
if (!options.command && !messageFromArgs) {
|
|
268
527
|
program.help();
|
|
269
528
|
}
|
|
529
|
+
if (worldSubscription) {
|
|
530
|
+
if (cliListeners && world) {
|
|
531
|
+
detachCLIListeners(world, cliListeners);
|
|
532
|
+
}
|
|
533
|
+
await worldSubscription.unsubscribe();
|
|
534
|
+
}
|
|
535
|
+
process.exit(0);
|
|
270
536
|
}
|
|
271
537
|
catch (error) {
|
|
272
538
|
console.error(boldRed('Error:'), error instanceof Error ? error.message : error);
|
|
539
|
+
if (worldSubscription) {
|
|
540
|
+
if (cliListeners && world) {
|
|
541
|
+
detachCLIListeners(world, cliListeners);
|
|
542
|
+
}
|
|
543
|
+
await worldSubscription.unsubscribe();
|
|
544
|
+
}
|
|
273
545
|
process.exit(1);
|
|
274
546
|
}
|
|
275
547
|
}
|
|
@@ -278,36 +550,78 @@ function cleanupWorldSubscription(worldState) {
|
|
|
278
550
|
worldState.subscription.unsubscribe();
|
|
279
551
|
}
|
|
280
552
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
553
|
+
/**
|
|
554
|
+
* Subscribe to world and attach CLI event listeners for interactive mode
|
|
555
|
+
*
|
|
556
|
+
* @param rootPath - Root path for world storage (unused, kept for compatibility)
|
|
557
|
+
* @param worldName - Name of the world to subscribe to
|
|
558
|
+
* @param streaming - Streaming state for real-time response display
|
|
559
|
+
* @param globalState - Global state for timer management
|
|
560
|
+
* @param activityMonitor - Activity monitor for tracking world events
|
|
561
|
+
* @param progressRenderer - Progress renderer for displaying activity
|
|
562
|
+
* @param rl - Readline interface for interactive input
|
|
563
|
+
* @returns WorldState with subscription and world instance
|
|
564
|
+
*/
|
|
565
|
+
async function handleSubscribe(rootPath, worldName, streaming, globalState, activityMonitor, progressRenderer, rl) {
|
|
566
|
+
// Subscribe to world lifecycle but do not request forwarding callbacks
|
|
567
|
+
const subscription = await subscribeWorld(worldName, { isOpen: true });
|
|
293
568
|
if (!subscription)
|
|
294
569
|
throw new Error('Failed to load world');
|
|
295
|
-
|
|
570
|
+
const world = subscription.world;
|
|
571
|
+
// Attach direct listeners to the world.eventEmitter for CLI handling
|
|
572
|
+
// Interactive mode needs all event types including SSE for streaming responses
|
|
573
|
+
attachCLIListeners(world, streaming, globalState, activityMonitor, progressRenderer, rl);
|
|
574
|
+
return { subscription, world };
|
|
296
575
|
}
|
|
297
576
|
// Handle world events with streaming support
|
|
298
|
-
function handleWorldEvent(eventType, eventData, streaming, globalState, rl) {
|
|
577
|
+
function handleWorldEvent(eventType, eventData, streaming, globalState, activityMonitor, progressRenderer, rl) {
|
|
578
|
+
if (eventType === 'world') {
|
|
579
|
+
const payload = eventData;
|
|
580
|
+
// Handle activity events (new format: type = 'response-start' | 'response-end' | 'idle')
|
|
581
|
+
if (payload.type === 'response-start' || payload.type === 'response-end' || payload.type === 'idle') {
|
|
582
|
+
activityMonitor.handle(payload);
|
|
583
|
+
progressRenderer.handle(payload);
|
|
584
|
+
// Call handleActivityEvents for verbose activity logging
|
|
585
|
+
handleActivityEvents(payload);
|
|
586
|
+
if (payload.type === 'idle' && rl && globalState.awaitingResponse) {
|
|
587
|
+
globalState.awaitingResponse = false;
|
|
588
|
+
console.log(); // Empty line before prompt
|
|
589
|
+
rl.prompt();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Handle tool events (migrated from sse channel)
|
|
593
|
+
else if (payload.type === 'tool-start' || payload.type === 'tool-result' || payload.type === 'tool-error' || payload.type === 'tool-progress') {
|
|
594
|
+
handleToolEvents(payload);
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
299
598
|
if (handleWorldEventWithStreaming(eventType, eventData, streaming)) {
|
|
300
599
|
return;
|
|
301
600
|
}
|
|
302
601
|
if (eventData.content && eventData.content.includes('Success message sent'))
|
|
303
602
|
return;
|
|
603
|
+
// Handle regular message events from agents (non-streaming or after streaming ends)
|
|
604
|
+
if (eventType === 'message' && eventData.sender && eventData.content) {
|
|
605
|
+
// Skip user messages to prevent echo
|
|
606
|
+
if (eventData.sender === 'human' || eventData.sender.startsWith('user')) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
// Skip if this message was already displayed via streaming
|
|
610
|
+
if (streaming.messageId === eventData.messageId) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
// Display system messages
|
|
614
|
+
if (eventData.sender === 'system') {
|
|
615
|
+
console.log(`${boldRed('● system:')} ${eventData.content}`);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
// Display agent messages (fallback for non-streaming or missed messages)
|
|
619
|
+
console.log(`\n${boldGreen(`● ${eventData.sender}:`)} ${eventData.content}\n`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
304
622
|
if ((eventType === 'system' || eventType === 'world') && (eventData.message || eventData.content)) {
|
|
305
623
|
// existing logic
|
|
306
624
|
}
|
|
307
|
-
else if (eventType === 'message' && eventData.sender === 'system') {
|
|
308
|
-
const msg = eventData.content;
|
|
309
|
-
console.log(`${boldRed('● system:')} ${msg}`);
|
|
310
|
-
}
|
|
311
625
|
}
|
|
312
626
|
// World discovery and selection
|
|
313
627
|
async function getAvailableWorldNames(rootPath) {
|
|
@@ -361,36 +675,64 @@ async function selectWorld(rootPath, rl) {
|
|
|
361
675
|
askForSelection();
|
|
362
676
|
});
|
|
363
677
|
}
|
|
678
|
+
// Chat discovery and selection
|
|
679
|
+
async function selectChat(world, chats, currentChatId, rl) {
|
|
680
|
+
if (chats.length === 0) {
|
|
681
|
+
console.log(boldRed(`No chats found in world '${world.name}'`));
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
if (chats.length === 1) {
|
|
685
|
+
console.log(`${boldGreen('Auto-selecting the only available chat:')} ${cyan(chats[0].name)}`);
|
|
686
|
+
return chats[0].id;
|
|
687
|
+
}
|
|
688
|
+
console.log(`\n${boldMagenta('Available chats:')}`);
|
|
689
|
+
console.log(` ${yellow('0.')} ${cyan('Cancel')}`);
|
|
690
|
+
chats.forEach((chat, index) => {
|
|
691
|
+
const isCurrent = currentChatId && chat.id === currentChatId;
|
|
692
|
+
const currentIndicator = isCurrent ? ' (current)' : '';
|
|
693
|
+
const msgCount = chat.messageCount || 0;
|
|
694
|
+
console.log(` ${yellow(`${index + 1}.`)} ${cyan(`${chat.name}${currentIndicator} - (${msgCount}`)}`);
|
|
695
|
+
});
|
|
696
|
+
return new Promise((resolve) => {
|
|
697
|
+
function askForSelection() {
|
|
698
|
+
rl.question(`\n${boldMagenta('Select a chat (number or name), or 0 to cancel:')} `, (answer) => {
|
|
699
|
+
const trimmed = answer.trim();
|
|
700
|
+
const num = parseInt(trimmed);
|
|
701
|
+
if (num === 0) {
|
|
702
|
+
resolve(null);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (!isNaN(num) && num >= 1 && num <= chats.length) {
|
|
706
|
+
resolve(chats[num - 1].id);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const found = chats.find(chat => chat.name.toLowerCase() === trimmed.toLowerCase() ||
|
|
710
|
+
chat.name.toLowerCase().includes(trimmed.toLowerCase()) ||
|
|
711
|
+
chat.id === trimmed);
|
|
712
|
+
if (found) {
|
|
713
|
+
resolve(found.id);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
console.log(boldRed('Invalid selection. Please try again.'));
|
|
717
|
+
askForSelection();
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
askForSelection();
|
|
721
|
+
});
|
|
722
|
+
}
|
|
364
723
|
// Interactive mode: console-based interface
|
|
365
724
|
async function runInteractiveMode(options) {
|
|
366
725
|
const rootPath = options.root || DEFAULT_ROOT_PATH;
|
|
367
726
|
enableStreaming();
|
|
368
727
|
const globalState = createGlobalState();
|
|
369
728
|
const streaming = createStreamingState();
|
|
729
|
+
const activityMonitor = new WorldActivityMonitor();
|
|
730
|
+
const progressRenderer = new ActivityProgressRenderer();
|
|
370
731
|
const rl = readline.createInterface({
|
|
371
732
|
input: process.stdin,
|
|
372
733
|
output: process.stdout,
|
|
373
734
|
prompt: '> '
|
|
374
735
|
});
|
|
375
|
-
// Set up streaming callbacks
|
|
376
|
-
streaming.wait = (delay) => {
|
|
377
|
-
setupPromptTimer(globalState, rl, () => {
|
|
378
|
-
if (streaming.isActive) {
|
|
379
|
-
console.log(`\n${gray('Streaming appears stalled - waiting for user input...')}`);
|
|
380
|
-
streaming.isActive = false;
|
|
381
|
-
streaming.content = '';
|
|
382
|
-
streaming.sender = undefined;
|
|
383
|
-
streaming.messageId = undefined;
|
|
384
|
-
rl.prompt();
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
rl.prompt();
|
|
388
|
-
}
|
|
389
|
-
}, delay);
|
|
390
|
-
};
|
|
391
|
-
streaming.stopWait = () => {
|
|
392
|
-
clearPromptTimer(globalState);
|
|
393
|
-
};
|
|
394
736
|
console.log(boldCyan('Agent World CLI (Interactive Mode)'));
|
|
395
737
|
console.log(cyan('===================================='));
|
|
396
738
|
let worldState = null;
|
|
@@ -401,7 +743,9 @@ async function runInteractiveMode(options) {
|
|
|
401
743
|
if (options.world) {
|
|
402
744
|
logger.debug(`Loading world: ${options.world}`);
|
|
403
745
|
try {
|
|
404
|
-
|
|
746
|
+
activityMonitor.reset();
|
|
747
|
+
progressRenderer.reset();
|
|
748
|
+
worldState = await handleSubscribe(rootPath, options.world, streaming, globalState, activityMonitor, progressRenderer, rl);
|
|
405
749
|
currentWorldName = options.world;
|
|
406
750
|
console.log(success(`Connected to world: ${currentWorldName}`));
|
|
407
751
|
if (worldState?.world) {
|
|
@@ -423,7 +767,9 @@ async function runInteractiveMode(options) {
|
|
|
423
767
|
}
|
|
424
768
|
logger.debug(`Loading world: ${selectedWorld}`);
|
|
425
769
|
try {
|
|
426
|
-
|
|
770
|
+
activityMonitor.reset();
|
|
771
|
+
progressRenderer.reset();
|
|
772
|
+
worldState = await handleSubscribe(rootPath, selectedWorld, streaming, globalState, activityMonitor, progressRenderer, rl);
|
|
427
773
|
currentWorldName = selectedWorld;
|
|
428
774
|
console.log(success(`Connected to world: ${currentWorldName}`));
|
|
429
775
|
if (worldState?.world) {
|
|
@@ -440,16 +786,22 @@ async function runInteractiveMode(options) {
|
|
|
440
786
|
console.log(`\n${gray('Quick Start:')}`);
|
|
441
787
|
console.log(` ${bullet(gray('World commands:'))} ${cyan('/world list')}, ${cyan('/world create')}, ${cyan('/world select')}`);
|
|
442
788
|
console.log(` ${bullet(gray('Agent commands:'))} ${cyan('/agent list')}, ${cyan('/agent create Ava')}, ${cyan('/agent update Ava')}`);
|
|
443
|
-
console.log(` ${bullet(gray('Chat commands:'))} ${cyan('/chat list
|
|
789
|
+
console.log(` ${bullet(gray('Chat commands:'))} ${cyan('/chat list')}, ${cyan('/chat select')}, ${cyan('/chat create')}, ${cyan('/chat export')}`);
|
|
444
790
|
console.log(` ${bullet(gray('Need help?'))} ${cyan('/help world')}, ${cyan('/help agent')}, ${cyan('/help chat')}`);
|
|
445
791
|
console.log(` ${bullet(gray('Type messages to talk with the world'))}`);
|
|
446
792
|
console.log(` ${bullet(gray('Exit with'))} ${cyan('/quit')} ${gray('or')} ${cyan('/exit')} ${gray('or press')} ${boldYellow('Ctrl+C')}`);
|
|
447
793
|
console.log(` ${bullet(gray('Enable debug logs via'))} ${cyan('--logLevel debug')}`);
|
|
448
794
|
console.log('');
|
|
795
|
+
// Display current chat messages after Quick Start tips
|
|
796
|
+
if (worldState?.world) {
|
|
797
|
+
await displayChatMessages(worldState.world.id, worldState.world.currentChatId);
|
|
798
|
+
}
|
|
799
|
+
console.log(); // Empty line before prompt
|
|
449
800
|
rl.prompt();
|
|
450
801
|
rl.on('line', async (input) => {
|
|
451
802
|
const trimmedInput = input.trim();
|
|
452
803
|
if (!trimmedInput) {
|
|
804
|
+
console.log(); // Empty line before prompt
|
|
453
805
|
rl.prompt();
|
|
454
806
|
return;
|
|
455
807
|
}
|
|
@@ -459,10 +811,6 @@ async function runInteractiveMode(options) {
|
|
|
459
811
|
if (isExiting)
|
|
460
812
|
return;
|
|
461
813
|
isExiting = true;
|
|
462
|
-
// Clear any existing timers immediately
|
|
463
|
-
clearPromptTimer(globalState);
|
|
464
|
-
if (streaming.stopWait)
|
|
465
|
-
streaming.stopWait();
|
|
466
814
|
console.log(`\n${boldCyan('Goodbye!')}`);
|
|
467
815
|
if (worldState)
|
|
468
816
|
cleanupWorldSubscription(worldState);
|
|
@@ -470,6 +818,12 @@ async function runInteractiveMode(options) {
|
|
|
470
818
|
process.exit(0);
|
|
471
819
|
}
|
|
472
820
|
console.log(`\n${boldYellow('● you:')} ${trimmedInput}`);
|
|
821
|
+
const isCommand = trimmedInput.startsWith('/');
|
|
822
|
+
let snapshot = null;
|
|
823
|
+
if (!isCommand) {
|
|
824
|
+
globalState.awaitingResponse = true;
|
|
825
|
+
snapshot = activityMonitor.captureSnapshot();
|
|
826
|
+
}
|
|
473
827
|
try {
|
|
474
828
|
const result = await processCLIInput(trimmedInput, worldState?.world || null, 'HUMAN');
|
|
475
829
|
// Handle exit commands from result (redundant, but keep for safety)
|
|
@@ -477,9 +831,6 @@ async function runInteractiveMode(options) {
|
|
|
477
831
|
if (isExiting)
|
|
478
832
|
return; // Prevent duplicate exit handling
|
|
479
833
|
isExiting = true;
|
|
480
|
-
clearPromptTimer(globalState);
|
|
481
|
-
if (streaming.stopWait)
|
|
482
|
-
streaming.stopWait();
|
|
483
834
|
console.log(`\n${boldCyan('Goodbye!')}`);
|
|
484
835
|
if (worldState)
|
|
485
836
|
cleanupWorldSubscription(worldState);
|
|
@@ -492,6 +843,7 @@ async function runInteractiveMode(options) {
|
|
|
492
843
|
const selectedWorld = await selectWorld(rootPath, rl);
|
|
493
844
|
if (!selectedWorld) {
|
|
494
845
|
console.log(error('No world selected.'));
|
|
846
|
+
console.log(); // Empty line before prompt
|
|
495
847
|
rl.prompt();
|
|
496
848
|
return;
|
|
497
849
|
}
|
|
@@ -507,17 +859,60 @@ async function runInteractiveMode(options) {
|
|
|
507
859
|
}
|
|
508
860
|
// Subscribe to the new world
|
|
509
861
|
logger.debug(`Subscribing to world: ${selectedWorld}...`);
|
|
510
|
-
|
|
862
|
+
activityMonitor.reset();
|
|
863
|
+
progressRenderer.reset();
|
|
864
|
+
worldState = await handleSubscribe(rootPath, selectedWorld, streaming, globalState, activityMonitor, progressRenderer, rl);
|
|
511
865
|
currentWorldName = selectedWorld;
|
|
512
866
|
console.log(success(`Connected to world: ${currentWorldName}`));
|
|
513
867
|
if (worldState?.world) {
|
|
514
868
|
console.log(`${gray('Agents:')} ${yellow(String(worldState.world.agents?.size || 0))} ${gray('| Turn Limit:')} ${yellow(String(worldState.world.turnLimit || 'N/A'))}`);
|
|
869
|
+
// Display current chat messages
|
|
870
|
+
await displayChatMessages(worldState.world.id, worldState.world.currentChatId);
|
|
515
871
|
}
|
|
516
872
|
}
|
|
517
873
|
catch (err) {
|
|
518
874
|
console.error(error(`Error loading world: ${err instanceof Error ? err.message : 'Unknown error'}`));
|
|
519
875
|
}
|
|
520
876
|
// Show prompt immediately after world selection
|
|
877
|
+
console.log(); // Empty line before prompt
|
|
878
|
+
rl.prompt();
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
// Handle chat selection command
|
|
882
|
+
if (result.data?.selectChat && worldState?.world) {
|
|
883
|
+
const { chats, currentChatId } = result.data;
|
|
884
|
+
const selectedChatId = await selectChat(worldState.world, chats, currentChatId, rl);
|
|
885
|
+
if (!selectedChatId) {
|
|
886
|
+
console.log(error('No chat selected.'));
|
|
887
|
+
console.log(); // Empty line before prompt
|
|
888
|
+
rl.prompt();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
try {
|
|
892
|
+
// Restore the selected chat
|
|
893
|
+
const { restoreChat } = await import('../core/index.js');
|
|
894
|
+
const restored = await restoreChat(worldState.world.id, selectedChatId);
|
|
895
|
+
if (!restored) {
|
|
896
|
+
console.log(error(`Failed to restore chat '${selectedChatId}'`));
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
const selectedChat = chats.find((c) => c.id === selectedChatId);
|
|
900
|
+
const chatName = selectedChat?.name || selectedChatId;
|
|
901
|
+
console.log(success(`Chat '${chatName}' selected and loaded`));
|
|
902
|
+
// Display chat messages
|
|
903
|
+
await displayChatMessages(worldState.world.id, selectedChatId);
|
|
904
|
+
// Refresh world state
|
|
905
|
+
// console.log(boldBlue('Refreshing world state...'));
|
|
906
|
+
const refreshedWorld = await worldState.subscription.refresh(rootPath);
|
|
907
|
+
worldState.world = refreshedWorld;
|
|
908
|
+
console.log(success('World state refreshed'));
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
catch (err) {
|
|
912
|
+
console.error(error(`Error loading chat: ${err instanceof Error ? err.message : 'Unknown error'}`));
|
|
913
|
+
}
|
|
914
|
+
// Show prompt immediately after chat selection
|
|
915
|
+
console.log(); // Empty line before prompt
|
|
521
916
|
rl.prompt();
|
|
522
917
|
return;
|
|
523
918
|
}
|
|
@@ -529,29 +924,26 @@ async function runInteractiveMode(options) {
|
|
|
529
924
|
!result.message.includes('Message sent to world')) {
|
|
530
925
|
console.log(success(result.message));
|
|
531
926
|
}
|
|
532
|
-
if (result.data && !(result.data.sender === '
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
927
|
+
// if (result.data && !(result.data.sender === 'human')) {
|
|
928
|
+
// // Print a concise summary of result.data if present and not already in message
|
|
929
|
+
// if (result.data) {
|
|
930
|
+
// if (typeof result.data === 'string') {
|
|
931
|
+
// console.log(`${boldMagenta('Data:')} ${result.data}`);
|
|
932
|
+
// } else if (result.data.name) {
|
|
933
|
+
// // If it's an agent or world object
|
|
934
|
+
// console.log(`${boldMagenta('Data:')} ${result.data.name}`);
|
|
935
|
+
// } else if (Array.isArray(result.data)) {
|
|
936
|
+
// console.log(`${boldMagenta('Data:')} ${result.data.length} items`);
|
|
937
|
+
// } else {
|
|
938
|
+
// // Fallback: print keys
|
|
939
|
+
// console.log(`${boldMagenta('Data:')} ${Object.keys(result.data).join(', ')}`);
|
|
940
|
+
// }
|
|
941
|
+
// }
|
|
942
|
+
// }
|
|
551
943
|
// Refresh world if needed
|
|
552
944
|
if (result.refreshWorld && currentWorldName && worldState) {
|
|
553
945
|
try {
|
|
554
|
-
console.log(boldBlue('Refreshing world state...'));
|
|
946
|
+
// console.log(boldBlue('Refreshing world state...'));
|
|
555
947
|
// Use the subscription's refresh method to properly destroy old world and create new
|
|
556
948
|
const refreshedWorld = await worldState.subscription.refresh(rootPath);
|
|
557
949
|
worldState.world = refreshedWorld;
|
|
@@ -564,30 +956,46 @@ async function runInteractiveMode(options) {
|
|
|
564
956
|
}
|
|
565
957
|
catch (err) {
|
|
566
958
|
console.error(error(`Command error: ${err instanceof Error ? err.message : 'Unknown error'}`));
|
|
959
|
+
if (!isCommand && globalState.awaitingResponse) {
|
|
960
|
+
globalState.awaitingResponse = false;
|
|
961
|
+
rl.prompt();
|
|
962
|
+
}
|
|
963
|
+
snapshot = null;
|
|
567
964
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
965
|
+
if (!isCommand && snapshot) {
|
|
966
|
+
try {
|
|
967
|
+
await activityMonitor.waitForIdle({ snapshot });
|
|
968
|
+
}
|
|
969
|
+
catch (error) {
|
|
970
|
+
console.error(red(`Timed out waiting for responses: ${error.message}`));
|
|
971
|
+
}
|
|
972
|
+
finally {
|
|
973
|
+
if (globalState.awaitingResponse) {
|
|
974
|
+
globalState.awaitingResponse = false;
|
|
975
|
+
console.log(); // Empty line before prompt
|
|
976
|
+
rl.prompt();
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
// For commands, show prompt immediately. For messages, world events will trigger the prompt
|
|
982
|
+
const isSelectCommand = trimmedInput.toLowerCase() === '/select' ||
|
|
983
|
+
trimmedInput.toLowerCase() === '/world select';
|
|
571
984
|
if (isSelectCommand) {
|
|
572
|
-
// For select command, prompt is already shown in the handler
|
|
985
|
+
// For world select command, prompt is already shown in the handler
|
|
573
986
|
return;
|
|
574
987
|
}
|
|
575
988
|
else if (isCommand) {
|
|
576
989
|
// For other commands, show prompt immediately
|
|
990
|
+
console.log(); // Empty line before prompt
|
|
577
991
|
rl.prompt();
|
|
578
992
|
}
|
|
579
|
-
|
|
580
|
-
// For messages, wait for potential agent responses
|
|
581
|
-
streaming.wait(5000);
|
|
582
|
-
}
|
|
993
|
+
// For messages, waitForIdle() above will handle prompt display via world 'idle' event
|
|
583
994
|
});
|
|
584
995
|
rl.on('close', () => {
|
|
585
996
|
if (isExiting)
|
|
586
997
|
return; // Prevent duplicate cleanup
|
|
587
998
|
isExiting = true;
|
|
588
|
-
clearPromptTimer(globalState);
|
|
589
|
-
if (streaming.stopWait)
|
|
590
|
-
streaming.stopWait();
|
|
591
999
|
console.log(`\n${boldCyan('Goodbye!')}`);
|
|
592
1000
|
if (worldState)
|
|
593
1001
|
cleanupWorldSubscription(worldState);
|
|
@@ -598,9 +1006,6 @@ async function runInteractiveMode(options) {
|
|
|
598
1006
|
return; // Prevent duplicate cleanup
|
|
599
1007
|
isExiting = true;
|
|
600
1008
|
console.log(`\n${boldCyan('Shutting down...')}`);
|
|
601
|
-
clearPromptTimer(globalState);
|
|
602
|
-
if (streaming.stopWait)
|
|
603
|
-
streaming.stopWait();
|
|
604
1009
|
console.log(`\n${boldCyan('Goodbye!')}`);
|
|
605
1010
|
if (worldState)
|
|
606
1011
|
cleanupWorldSubscription(worldState);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-world",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -28,10 +28,19 @@
|
|
|
28
28
|
"prestart": "npm run build",
|
|
29
29
|
"start": "node dist/server/index.js",
|
|
30
30
|
"cli": "npx tsx cli/index.ts",
|
|
31
|
+
"cli:watch": "npx tsx --watch cli/index.ts",
|
|
31
32
|
"server": "npx tsx server/index.ts",
|
|
32
|
-
"dev": "
|
|
33
|
-
"
|
|
33
|
+
"dev": "npm-run-all --parallel server:watch web:dev",
|
|
34
|
+
"server:watch": "npx tsx --watch server/index.ts",
|
|
35
|
+
"web:dev": "npm-run-all --sequential web:wait web:start",
|
|
36
|
+
"web:wait": "wait-on http://127.0.0.1:3000/health",
|
|
37
|
+
"web:start": "npm run dev --workspace=web",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
40
|
+
"test:ui": "vitest --ui",
|
|
41
|
+
"test:coverage": "vitest run --coverage",
|
|
34
42
|
"test:db": "npx tsx tests/db/migration-tests.ts",
|
|
43
|
+
"integration": "vitest run --config vitest.integration.config.ts",
|
|
35
44
|
"check": "tsc --noEmit && npm run check --workspace=web",
|
|
36
45
|
"build": "npm run build --workspace=core && tsc --project tsconfig.build.json && npm run build --workspace=web",
|
|
37
46
|
"pkill": "pkill -f tsx",
|
|
@@ -60,12 +69,14 @@
|
|
|
60
69
|
"devDependencies": {
|
|
61
70
|
"@types/cors": "^2.8.19",
|
|
62
71
|
"@types/express": "^4.17.23",
|
|
63
|
-
"@types/
|
|
64
|
-
"@
|
|
72
|
+
"@types/node": "^24.9.1",
|
|
73
|
+
"@vitest/coverage-v8": "^2.1.9",
|
|
74
|
+
"@vitest/ui": "^2.1.9",
|
|
65
75
|
"commander": "^14.0.0",
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
76
|
+
"npm-run-all": "^4.1.5",
|
|
77
|
+
"vite-tsconfig-paths": "^5.1.4",
|
|
78
|
+
"vitest": "^2.1.9",
|
|
79
|
+
"wait-on": "^9.0.1"
|
|
69
80
|
},
|
|
70
81
|
"engines": {
|
|
71
82
|
"node": ">=20.0.0"
|