agents-dojo 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/agent-executor.js +74 -9
- package/dist/agent-logger.d.ts +26 -0
- package/dist/agent-logger.js +63 -0
- package/dist/cli.js +169 -7
- package/dist/log-writer.d.ts +26 -0
- package/dist/log-writer.js +123 -0
- package/dist/monitor-bus.d.ts +6 -0
- package/dist/monitor-ws.d.ts +7 -0
- package/dist/monitor-ws.js +81 -3
- package/dist/server.d.ts +2 -0
- package/dist/server.js +34 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,10 +24,14 @@ See `agents/_template_echo/` for a complete manifest reference.
|
|
|
24
24
|
|
|
25
25
|
## Monitor GUI
|
|
26
26
|
|
|
27
|
+
The Monitor GUI starts automatically when you run `agents-dojo`. It picks a free
|
|
28
|
+
port for the WebSocket and launches the Vite dev server (installing dependencies
|
|
29
|
+
on first run). The GUI URL is printed to the console.
|
|
30
|
+
|
|
27
31
|
```bash
|
|
28
|
-
agents-dojo
|
|
29
|
-
|
|
30
|
-
#
|
|
32
|
+
agents-dojo # monitor starts automatically
|
|
33
|
+
agents-dojo --monitor-port=9000 # use a specific WS port
|
|
34
|
+
agents-dojo --no-monitor # disable the monitor
|
|
31
35
|
```
|
|
32
36
|
|
|
33
37
|
## Architecture
|
package/dist/agent-executor.js
CHANGED
|
@@ -4,6 +4,26 @@ import { a2aToContentBlocks } from './part-mapper.js';
|
|
|
4
4
|
import { runClaude } from './claude-bridge.js';
|
|
5
5
|
import { createTranslator } from './event-translator.js';
|
|
6
6
|
import { recordTaskStart, recordTaskEnd } from './metrics.js';
|
|
7
|
+
import { logConversation, AgentConversationLogger } from './agent-logger.js';
|
|
8
|
+
// ── Global concurrency limiter ─────────────────────────────
|
|
9
|
+
// Prevents CPU overload by limiting simultaneous Claude SDK processes.
|
|
10
|
+
const MAX_CONCURRENT = 3;
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes per task
|
|
12
|
+
let running = 0;
|
|
13
|
+
const waitQueue = [];
|
|
14
|
+
function acquireSlot() {
|
|
15
|
+
if (running < MAX_CONCURRENT) {
|
|
16
|
+
running++;
|
|
17
|
+
return Promise.resolve();
|
|
18
|
+
}
|
|
19
|
+
return new Promise((resolve) => waitQueue.push(() => { running++; resolve(); }));
|
|
20
|
+
}
|
|
21
|
+
function releaseSlot() {
|
|
22
|
+
running--;
|
|
23
|
+
const next = waitQueue.shift();
|
|
24
|
+
if (next)
|
|
25
|
+
next();
|
|
26
|
+
}
|
|
7
27
|
export class DojoAgentExecutor {
|
|
8
28
|
agent;
|
|
9
29
|
options;
|
|
@@ -22,7 +42,6 @@ export class DojoAgentExecutor {
|
|
|
22
42
|
this.controllers.delete(taskId);
|
|
23
43
|
this.contextIds.delete(taskId);
|
|
24
44
|
// Publish canceled state to the bus so the client sees the transition.
|
|
25
|
-
// Guard against publishing if the bus is no longer accepting (best-effort).
|
|
26
45
|
try {
|
|
27
46
|
eventBus.publish({
|
|
28
47
|
kind: 'status-update',
|
|
@@ -39,9 +58,21 @@ export class DojoAgentExecutor {
|
|
|
39
58
|
catch {
|
|
40
59
|
// ignore — the SDK subprocess may already be torn down
|
|
41
60
|
}
|
|
61
|
+
// Also notify monitor
|
|
62
|
+
this.options.monitorBus?.emit({ type: 'task_status', taskId, state: 'canceled' });
|
|
42
63
|
};
|
|
43
64
|
async execute(requestContext, eventBus) {
|
|
44
65
|
const { taskId, contextId, userMessage } = requestContext;
|
|
66
|
+
// Wait for a concurrency slot
|
|
67
|
+
await acquireSlot();
|
|
68
|
+
// Set up task timeout
|
|
69
|
+
const timeoutTimer = setTimeout(() => {
|
|
70
|
+
const ctrl = this.controllers.get(taskId);
|
|
71
|
+
if (ctrl) {
|
|
72
|
+
console.warn(`[agents-dojo] Task ${taskId} timed out after ${DEFAULT_TIMEOUT_MS / 1000}s, aborting`);
|
|
73
|
+
ctrl.abort();
|
|
74
|
+
}
|
|
75
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
45
76
|
recordTaskStart();
|
|
46
77
|
// Publish the initial task snapshot. The A2A SDK's ResultManager only
|
|
47
78
|
// initializes its currentTask from a `kind: 'task'` event; without it,
|
|
@@ -58,15 +89,21 @@ export class DojoAgentExecutor {
|
|
|
58
89
|
history: [userMessage],
|
|
59
90
|
artifacts: [],
|
|
60
91
|
});
|
|
61
|
-
// Emit task_created to monitor bus.
|
|
62
|
-
|
|
92
|
+
// Emit task_created to monitor bus.
|
|
93
|
+
const preview = extractPreview(userMessage.parts);
|
|
63
94
|
this.options.monitorBus?.emit({
|
|
64
95
|
type: 'task_created',
|
|
65
96
|
taskId,
|
|
66
97
|
contextId,
|
|
67
98
|
from: 'user',
|
|
68
99
|
to: this.agent.manifest.id,
|
|
69
|
-
preview
|
|
100
|
+
preview,
|
|
101
|
+
});
|
|
102
|
+
// Also emit submitted state to monitor
|
|
103
|
+
this.options.monitorBus?.emit({
|
|
104
|
+
type: 'task_status',
|
|
105
|
+
taskId,
|
|
106
|
+
state: 'submitted',
|
|
70
107
|
});
|
|
71
108
|
// 1. Extract text content from A2A message
|
|
72
109
|
const parts = userMessage.parts;
|
|
@@ -86,12 +123,28 @@ export class DojoAgentExecutor {
|
|
|
86
123
|
this.controllers.set(taskId, controller);
|
|
87
124
|
this.contextIds.set(taskId, contextId);
|
|
88
125
|
// 4. Run Claude (iterate events)
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
let finalResponse = '';
|
|
128
|
+
let taskState = 'completed';
|
|
129
|
+
// Full SDK conversation logger — captures every message for transparency
|
|
130
|
+
const sdkLogger = new AgentConversationLogger(this.agent.agentDir, this.agent.manifest.id, taskId);
|
|
89
131
|
try {
|
|
90
132
|
for await (const sdkMsg of runClaude({
|
|
91
133
|
agent: this.agent,
|
|
92
134
|
contentBlocks,
|
|
93
135
|
contextId,
|
|
94
|
-
onEvent: (m) =>
|
|
136
|
+
onEvent: (m) => {
|
|
137
|
+
translator.onSdkEvent(m);
|
|
138
|
+
sdkLogger.onSdkMessage(m);
|
|
139
|
+
// Capture final text for summary log
|
|
140
|
+
const msg = m;
|
|
141
|
+
if (msg.type === 'assistant' && msg.message?.content) {
|
|
142
|
+
for (const block of msg.message.content) {
|
|
143
|
+
if (block.type === 'text')
|
|
144
|
+
finalResponse = block.text;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
95
148
|
abortController: controller,
|
|
96
149
|
})) {
|
|
97
150
|
// events are published via onEvent
|
|
@@ -100,13 +153,11 @@ export class DojoAgentExecutor {
|
|
|
100
153
|
}
|
|
101
154
|
catch (err) {
|
|
102
155
|
recordTaskEnd(false);
|
|
103
|
-
|
|
104
|
-
// want to report it as a generic failure — cancelTask already published
|
|
105
|
-
// the canceled state. Suppress the publish in that case.
|
|
156
|
+
taskState = 'failed';
|
|
106
157
|
const isAbort = controller.signal.aborted || (err instanceof Error && /abort/i.test(err.message));
|
|
107
158
|
if (!isAbort) {
|
|
108
|
-
// SDK subprocess crash, network failure, etc.
|
|
109
159
|
const reason = err instanceof Error ? err.message : String(err);
|
|
160
|
+
finalResponse = `Error: ${reason}`;
|
|
110
161
|
eventBus.publish({
|
|
111
162
|
kind: 'status-update',
|
|
112
163
|
taskId,
|
|
@@ -129,8 +180,22 @@ export class DojoAgentExecutor {
|
|
|
129
180
|
}
|
|
130
181
|
}
|
|
131
182
|
finally {
|
|
183
|
+
clearTimeout(timeoutTimer);
|
|
184
|
+
releaseSlot();
|
|
132
185
|
this.controllers.delete(taskId);
|
|
133
186
|
this.contextIds.delete(taskId);
|
|
187
|
+
// Flush full SDK conversation log (all LLM interactions)
|
|
188
|
+
sdkLogger.flush(taskState, Date.now() - startTime);
|
|
189
|
+
// Also save summary log
|
|
190
|
+
logConversation(this.agent.agentDir, {
|
|
191
|
+
taskId,
|
|
192
|
+
agentId: this.agent.manifest.id,
|
|
193
|
+
timestamp: new Date().toISOString(),
|
|
194
|
+
userMessage: extractPreview(userMessage.parts),
|
|
195
|
+
agentResponse: finalResponse.slice(0, 500),
|
|
196
|
+
state: taskState,
|
|
197
|
+
durationMs: Date.now() - startTime,
|
|
198
|
+
});
|
|
134
199
|
}
|
|
135
200
|
}
|
|
136
201
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A logger that captures every SDK message for a single task.
|
|
3
|
+
* Create one per task, call .onSdkMessage() for each message,
|
|
4
|
+
* then .flush() when the task completes.
|
|
5
|
+
*/
|
|
6
|
+
export declare class AgentConversationLogger {
|
|
7
|
+
private agentDir;
|
|
8
|
+
private agentId;
|
|
9
|
+
private taskId;
|
|
10
|
+
private messages;
|
|
11
|
+
constructor(agentDir: string, agentId: string, taskId: string);
|
|
12
|
+
/** Record a raw SDK message (assistant, user, result, etc.) */
|
|
13
|
+
onSdkMessage(msg: unknown): void;
|
|
14
|
+
/** Write all captured messages to the agent's log file. */
|
|
15
|
+
flush(state: 'completed' | 'failed' | 'canceled', durationMs: number): void;
|
|
16
|
+
}
|
|
17
|
+
export interface ConversationEntry {
|
|
18
|
+
taskId: string;
|
|
19
|
+
agentId: string;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
userMessage: string;
|
|
22
|
+
agentResponse: string;
|
|
23
|
+
state: 'completed' | 'failed';
|
|
24
|
+
durationMs: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function logConversation(agentDir: string, entry: ConversationEntry): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// src/agent-logger.ts
|
|
2
|
+
// Saves the complete LLM conversation (all SDK messages) for each agent task.
|
|
3
|
+
import { appendFileSync, mkdirSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
function todayStr() {
|
|
6
|
+
return new Date().toISOString().slice(0, 10);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A logger that captures every SDK message for a single task.
|
|
10
|
+
* Create one per task, call .onSdkMessage() for each message,
|
|
11
|
+
* then .flush() when the task completes.
|
|
12
|
+
*/
|
|
13
|
+
export class AgentConversationLogger {
|
|
14
|
+
agentDir;
|
|
15
|
+
agentId;
|
|
16
|
+
taskId;
|
|
17
|
+
messages = [];
|
|
18
|
+
constructor(agentDir, agentId, taskId) {
|
|
19
|
+
this.agentDir = agentDir;
|
|
20
|
+
this.agentId = agentId;
|
|
21
|
+
this.taskId = taskId;
|
|
22
|
+
}
|
|
23
|
+
/** Record a raw SDK message (assistant, user, result, etc.) */
|
|
24
|
+
onSdkMessage(msg) {
|
|
25
|
+
this.messages.push({
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
type: msg?.type ?? 'unknown',
|
|
28
|
+
data: msg,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Write all captured messages to the agent's log file. */
|
|
32
|
+
flush(state, durationMs) {
|
|
33
|
+
const logDir = join(this.agentDir, 'log');
|
|
34
|
+
mkdirSync(logDir, { recursive: true });
|
|
35
|
+
const file = join(logDir, `${todayStr()}.jsonl`);
|
|
36
|
+
const entry = {
|
|
37
|
+
taskId: this.taskId,
|
|
38
|
+
agentId: this.agentId,
|
|
39
|
+
timestamp: new Date().toISOString(),
|
|
40
|
+
state,
|
|
41
|
+
durationMs,
|
|
42
|
+
messageCount: this.messages.length,
|
|
43
|
+
messages: this.messages,
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error(`[agent-logger] Failed to write to ${file}:`, err);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function logConversation(agentDir, entry) {
|
|
54
|
+
const logDir = join(agentDir, 'log');
|
|
55
|
+
mkdirSync(logDir, { recursive: true });
|
|
56
|
+
const file = join(logDir, `${todayStr()}.jsonl`);
|
|
57
|
+
try {
|
|
58
|
+
appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
console.error(`[agent-logger] Failed to write:`, err);
|
|
62
|
+
}
|
|
63
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { resolve, join } from 'path';
|
|
2
|
+
import { resolve, join, dirname } from 'path';
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { spawn, execSync } from 'child_process';
|
|
4
6
|
import { createServer } from './server.js';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
5
9
|
// ── init command ──────────────────────────────────────────
|
|
6
10
|
const ECHO_MANIFEST = `{
|
|
7
11
|
"id": "echo",
|
|
@@ -140,7 +144,7 @@ function installCallAgentSkill(agentDir) {
|
|
|
140
144
|
export const DEFAULT_ARGS = {
|
|
141
145
|
agentsDir: './agents',
|
|
142
146
|
port: 41241,
|
|
143
|
-
monitorPort:
|
|
147
|
+
monitorPort: 0, // 0 = OS picks a free port
|
|
144
148
|
singleAgent: null,
|
|
145
149
|
help: false,
|
|
146
150
|
};
|
|
@@ -172,17 +176,149 @@ function printHelp() {
|
|
|
172
176
|
|
|
173
177
|
Commands:
|
|
174
178
|
init Create agents/echo/ example in the current directory
|
|
179
|
+
chat <agentId> <msg> One-shot chat with an agent (connects to a running server)
|
|
175
180
|
(default) Start the A2A server
|
|
176
181
|
|
|
177
182
|
Options:
|
|
178
183
|
--agents-dir <path> Directory containing agent subdirectories (default: ${DEFAULT_ARGS.agentsDir})
|
|
179
184
|
--port <port> A2A HTTP port (default: ${DEFAULT_ARGS.port})
|
|
180
|
-
--monitor-port <port> Monitor WebSocket port (default:
|
|
181
|
-
--no-monitor Disable Monitor
|
|
185
|
+
--monitor-port <port> Monitor WebSocket port (default: auto)
|
|
186
|
+
--no-monitor Disable Monitor GUI
|
|
182
187
|
--agent=<id> Single Agent mode: only load and serve the named agent
|
|
183
188
|
-h, --help Show this help
|
|
184
189
|
`);
|
|
185
190
|
}
|
|
191
|
+
// ── chat command ─────────────────────────────────────────
|
|
192
|
+
async function runChat(agentId, message, port) {
|
|
193
|
+
const { v4: uuidv4 } = await import('uuid');
|
|
194
|
+
const body = {
|
|
195
|
+
jsonrpc: '2.0',
|
|
196
|
+
id: 1,
|
|
197
|
+
method: 'message/send',
|
|
198
|
+
params: {
|
|
199
|
+
message: {
|
|
200
|
+
messageId: uuidv4(),
|
|
201
|
+
kind: 'message',
|
|
202
|
+
role: 'user',
|
|
203
|
+
parts: [{ kind: 'text', text: message }],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const res = await fetch(`http://localhost:${port}/a2a/${agentId}`, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify(body),
|
|
211
|
+
});
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
console.error(`Error: A2A server returned ${res.status}`);
|
|
214
|
+
const text = await res.text();
|
|
215
|
+
if (text)
|
|
216
|
+
console.error(text);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
const json = await res.json();
|
|
220
|
+
const result = json.result;
|
|
221
|
+
if (!result) {
|
|
222
|
+
console.error('Error: no result in response');
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
// Extract reply text
|
|
226
|
+
let replyText = '';
|
|
227
|
+
for (const a of (result.artifacts ?? [])) {
|
|
228
|
+
for (const p of (a.parts ?? [])) {
|
|
229
|
+
if (p.kind === 'text')
|
|
230
|
+
replyText += p.text;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!replyText) {
|
|
234
|
+
const statusMsg = result.status?.message;
|
|
235
|
+
if (statusMsg) {
|
|
236
|
+
for (const p of (statusMsg.parts ?? [])) {
|
|
237
|
+
if (p.kind === 'text')
|
|
238
|
+
replyText += p.text;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (replyText) {
|
|
243
|
+
console.log(replyText);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
console.log('(no response)');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// ── monitor GUI launcher ─────────────────────────────────
|
|
250
|
+
/** Resolve the monitor/ directory shipped alongside the package. */
|
|
251
|
+
function resolveMonitorDir() {
|
|
252
|
+
// __dirname is dist/ after build, so monitor/ is one level up
|
|
253
|
+
return resolve(__dirname, '..', 'monitor');
|
|
254
|
+
}
|
|
255
|
+
function startMonitorGui(monitorWsPort) {
|
|
256
|
+
const monitorDir = resolveMonitorDir();
|
|
257
|
+
if (!existsSync(join(monitorDir, 'package.json'))) {
|
|
258
|
+
console.warn('[agents-dojo] Monitor GUI not found — skipping.');
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
// Auto-install monitor deps if missing
|
|
262
|
+
if (!existsSync(join(monitorDir, 'node_modules'))) {
|
|
263
|
+
console.log('[agents-dojo] Installing monitor dependencies…');
|
|
264
|
+
try {
|
|
265
|
+
execSync('npm install --no-audit --no-fund', { cwd: monitorDir, stdio: 'inherit' });
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
console.warn('[agents-dojo] Failed to install monitor dependencies — skipping GUI.');
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const wsUrl = `ws://localhost:${monitorWsPort}/monitor`;
|
|
273
|
+
const child = spawn(join(monitorDir, 'node_modules', '.bin', 'vite'), [], {
|
|
274
|
+
cwd: monitorDir,
|
|
275
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
276
|
+
env: {
|
|
277
|
+
...process.env,
|
|
278
|
+
VITE_MONITOR_WS_URL: wsUrl,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
child.stdout?.on('data', (data) => {
|
|
282
|
+
const line = data.toString().trim();
|
|
283
|
+
if (line) {
|
|
284
|
+
// Extract and print the local URL from vite output
|
|
285
|
+
const urlMatch = line.match(/Local:\s+(https?:\/\/\S+)/);
|
|
286
|
+
if (urlMatch) {
|
|
287
|
+
console.log(`[agents-dojo] Monitor GUI: ${urlMatch[1]}`);
|
|
288
|
+
}
|
|
289
|
+
else if (line.includes('ready in') || line.includes('VITE')) {
|
|
290
|
+
// Print vite ready messages
|
|
291
|
+
console.log(`[agents-dojo] ${line}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
child.stderr?.on('data', (data) => {
|
|
296
|
+
const line = data.toString().trim();
|
|
297
|
+
if (line)
|
|
298
|
+
console.error(`[monitor] ${line}`);
|
|
299
|
+
});
|
|
300
|
+
child.on('error', (err) => {
|
|
301
|
+
console.warn(`[agents-dojo] Failed to start Monitor GUI: ${err.message}`);
|
|
302
|
+
});
|
|
303
|
+
return child;
|
|
304
|
+
}
|
|
305
|
+
/** Ensure child processes are killed on exit. */
|
|
306
|
+
function registerCleanup(child) {
|
|
307
|
+
const kill = () => {
|
|
308
|
+
if (!child.killed) {
|
|
309
|
+
child.kill('SIGTERM');
|
|
310
|
+
// Force kill after 2s if SIGTERM didn't work
|
|
311
|
+
setTimeout(() => {
|
|
312
|
+
if (!child.killed)
|
|
313
|
+
child.kill('SIGKILL');
|
|
314
|
+
}, 2000).unref();
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
process.on('exit', kill);
|
|
318
|
+
process.on('SIGINT', () => { kill(); process.exit(0); });
|
|
319
|
+
process.on('SIGTERM', () => { kill(); process.exit(0); });
|
|
320
|
+
}
|
|
321
|
+
// ── main ─────────────────────────────────────────────────
|
|
186
322
|
async function main() {
|
|
187
323
|
const rawArgs = process.argv.slice(2);
|
|
188
324
|
// Handle `agents-dojo init [dir]`
|
|
@@ -190,6 +326,24 @@ async function main() {
|
|
|
190
326
|
runInit(resolve(rawArgs[1] ?? '.'));
|
|
191
327
|
return;
|
|
192
328
|
}
|
|
329
|
+
// Handle `agents-dojo chat <agentId> <message...>`
|
|
330
|
+
if (rawArgs[0] === 'chat') {
|
|
331
|
+
const agentId = rawArgs[1];
|
|
332
|
+
const message = rawArgs.slice(2).join(' ');
|
|
333
|
+
if (!agentId || !message) {
|
|
334
|
+
console.error('Usage: agents-dojo chat <agentId> <message>');
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
// Check for --port flag in remaining args
|
|
338
|
+
let port = DEFAULT_ARGS.port;
|
|
339
|
+
for (let i = 2; i < rawArgs.length; i++) {
|
|
340
|
+
if (rawArgs[i] === '--port' && rawArgs[i + 1]) {
|
|
341
|
+
port = parseInt(rawArgs[++i], 10);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
await runChat(agentId, message, port);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
193
347
|
const args = parseArgs(rawArgs);
|
|
194
348
|
if (args.help) {
|
|
195
349
|
printHelp();
|
|
@@ -197,9 +351,6 @@ async function main() {
|
|
|
197
351
|
}
|
|
198
352
|
console.log(`[agents-dojo] agents-dir: ${args.agentsDir}`);
|
|
199
353
|
console.log(`[agents-dojo] A2A port: ${args.port}`);
|
|
200
|
-
if (args.monitorPort !== null) {
|
|
201
|
-
console.log(`[agents-dojo] Monitor port: ${args.monitorPort}`);
|
|
202
|
-
}
|
|
203
354
|
if (args.singleAgent) {
|
|
204
355
|
console.log(`[agents-dojo] Single agent mode: ${args.singleAgent}`);
|
|
205
356
|
}
|
|
@@ -210,6 +361,17 @@ async function main() {
|
|
|
210
361
|
singleAgent: args.singleAgent ?? undefined,
|
|
211
362
|
});
|
|
212
363
|
console.log(`[agents-dojo] ${server.registry.list().length} agents loaded: ${server.registry.list().join(', ')}`);
|
|
364
|
+
// Auto-start monitor GUI
|
|
365
|
+
if (server.monitorPort) {
|
|
366
|
+
console.log(`[agents-dojo] Monitor WS port: ${server.monitorPort}`);
|
|
367
|
+
const monitorChild = startMonitorGui(server.monitorPort);
|
|
368
|
+
if (monitorChild) {
|
|
369
|
+
registerCleanup(monitorChild);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
console.log(`[agents-dojo] Monitor disabled.`);
|
|
374
|
+
}
|
|
213
375
|
console.log(`[agents-dojo] Ready.`);
|
|
214
376
|
}
|
|
215
377
|
main().catch((err) => {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MonitorBus } from './monitor-bus.js';
|
|
2
|
+
/**
|
|
3
|
+
* A single chat message in the conversation log.
|
|
4
|
+
*/
|
|
5
|
+
export interface ChatMessage {
|
|
6
|
+
timestamp: string;
|
|
7
|
+
taskId: string;
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
content: string;
|
|
11
|
+
direction: 'incoming' | 'outgoing';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Subscribe to monitor bus and save chat messages (not raw events).
|
|
15
|
+
* Only task_created (incoming message) and task_status with working/completed
|
|
16
|
+
* (agent response) are saved as chat records.
|
|
17
|
+
*/
|
|
18
|
+
export declare function startMonitorLogger(bus: MonitorBus, logDir: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Read chat messages from logs.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readLogs(logDir: string, date?: string): ChatMessage[];
|
|
23
|
+
/**
|
|
24
|
+
* List available log dates.
|
|
25
|
+
*/
|
|
26
|
+
export declare function listLogDates(logDir: string): string[];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/log-writer.ts
|
|
2
|
+
// Saves agent conversations (who said what to whom) as chat-style records.
|
|
3
|
+
import { appendFileSync, mkdirSync, existsSync, readdirSync, readFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
function todayStr() {
|
|
6
|
+
return new Date().toISOString().slice(0, 10);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Subscribe to monitor bus and save chat messages (not raw events).
|
|
10
|
+
* Only task_created (incoming message) and task_status with working/completed
|
|
11
|
+
* (agent response) are saved as chat records.
|
|
12
|
+
*/
|
|
13
|
+
export function startMonitorLogger(bus, logDir) {
|
|
14
|
+
mkdirSync(logDir, { recursive: true });
|
|
15
|
+
// Track taskId → agent mapping for responses
|
|
16
|
+
const taskAgents = new Map();
|
|
17
|
+
// Track which agents are currently working (for detecting peer calls)
|
|
18
|
+
const workingAgents = new Set();
|
|
19
|
+
bus.subscribe((event) => {
|
|
20
|
+
const file = join(logDir, `${todayStr()}.jsonl`);
|
|
21
|
+
if (event.type === 'task_created') {
|
|
22
|
+
// Detect agent-to-agent call: if another agent is working, it's likely the caller
|
|
23
|
+
let from = event.from;
|
|
24
|
+
if (from === 'user') {
|
|
25
|
+
for (const agentId of workingAgents) {
|
|
26
|
+
if (agentId !== event.to) {
|
|
27
|
+
from = agentId;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
taskAgents.set(event.taskId, { from, to: event.to });
|
|
33
|
+
const msg = {
|
|
34
|
+
timestamp: new Date().toISOString(),
|
|
35
|
+
taskId: event.taskId,
|
|
36
|
+
from,
|
|
37
|
+
to: event.to,
|
|
38
|
+
content: event.preview,
|
|
39
|
+
direction: 'incoming',
|
|
40
|
+
};
|
|
41
|
+
writeLine(file, msg);
|
|
42
|
+
}
|
|
43
|
+
// Track working agents from tool_call events too
|
|
44
|
+
if (event.type === 'tool_call_start') {
|
|
45
|
+
const targetId = [...taskAgents.entries()].find(([tid]) => tid === event.taskId)?.[1]?.to;
|
|
46
|
+
if (targetId)
|
|
47
|
+
workingAgents.add(targetId);
|
|
48
|
+
}
|
|
49
|
+
if (event.type === 'task_status') {
|
|
50
|
+
// Track working agents
|
|
51
|
+
const targetId = [...taskAgents.entries()].find(([tid]) => tid === event.taskId)?.[1]?.to;
|
|
52
|
+
if (targetId) {
|
|
53
|
+
if (event.state === 'working')
|
|
54
|
+
workingAgents.add(targetId);
|
|
55
|
+
else if (event.state === 'completed' || event.state === 'failed' || event.state === 'canceled')
|
|
56
|
+
workingAgents.delete(targetId);
|
|
57
|
+
}
|
|
58
|
+
if (event.message) {
|
|
59
|
+
const mapping = taskAgents.get(event.taskId);
|
|
60
|
+
if (mapping) {
|
|
61
|
+
const msg = {
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
taskId: event.taskId,
|
|
64
|
+
from: mapping.to, // agent is responding
|
|
65
|
+
to: mapping.from, // back to the caller
|
|
66
|
+
content: event.message,
|
|
67
|
+
direction: 'outgoing',
|
|
68
|
+
};
|
|
69
|
+
writeLine(file, msg);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Clean up on terminal states
|
|
73
|
+
const terminalStates = ['completed', 'failed', 'canceled', 'rejected'];
|
|
74
|
+
if (terminalStates.includes(event.state)) {
|
|
75
|
+
taskAgents.delete(event.taskId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function writeLine(file, msg) {
|
|
81
|
+
try {
|
|
82
|
+
appendFileSync(file, JSON.stringify(msg) + '\n');
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error('[log-writer] Failed to write:', err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Read chat messages from logs.
|
|
90
|
+
*/
|
|
91
|
+
export function readLogs(logDir, date) {
|
|
92
|
+
if (!existsSync(logDir))
|
|
93
|
+
return [];
|
|
94
|
+
const files = date
|
|
95
|
+
? [`${date}.jsonl`]
|
|
96
|
+
: readdirSync(logDir).filter((f) => f.endsWith('.jsonl')).sort().reverse();
|
|
97
|
+
const entries = [];
|
|
98
|
+
for (const file of files) {
|
|
99
|
+
const path = join(logDir, file);
|
|
100
|
+
if (!existsSync(path))
|
|
101
|
+
continue;
|
|
102
|
+
const lines = readFileSync(path, 'utf-8').split('\n').filter(Boolean);
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
try {
|
|
105
|
+
entries.push(JSON.parse(line));
|
|
106
|
+
}
|
|
107
|
+
catch { /* skip */ }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return entries;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* List available log dates.
|
|
114
|
+
*/
|
|
115
|
+
export function listLogDates(logDir) {
|
|
116
|
+
if (!existsSync(logDir))
|
|
117
|
+
return [];
|
|
118
|
+
return readdirSync(logDir)
|
|
119
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
120
|
+
.map((f) => f.replace('.jsonl', ''))
|
|
121
|
+
.sort()
|
|
122
|
+
.reverse();
|
|
123
|
+
}
|
package/dist/monitor-bus.d.ts
CHANGED
|
@@ -30,6 +30,12 @@ export type MonitorEvent = {
|
|
|
30
30
|
taskId: string;
|
|
31
31
|
toolName: string;
|
|
32
32
|
success: boolean;
|
|
33
|
+
} | {
|
|
34
|
+
type: 'chat_response';
|
|
35
|
+
chatId: string;
|
|
36
|
+
agentId: string;
|
|
37
|
+
text: string;
|
|
38
|
+
final: boolean;
|
|
33
39
|
};
|
|
34
40
|
export type MonitorSubscriber = (event: MonitorEvent) => void;
|
|
35
41
|
export type Unsubscribe = () => void;
|
package/dist/monitor-ws.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface MonitorWsOptions {
|
|
|
7
7
|
bus: MonitorBus;
|
|
8
8
|
path: string;
|
|
9
9
|
registry: AgentRegistry;
|
|
10
|
+
/** A2A server port — needed to proxy chat commands. */
|
|
11
|
+
a2aPort: number;
|
|
10
12
|
}
|
|
11
13
|
export type MonitorCommand = {
|
|
12
14
|
type: 'reload';
|
|
@@ -16,6 +18,11 @@ export type MonitorCommand = {
|
|
|
16
18
|
agentId: string;
|
|
17
19
|
x: number;
|
|
18
20
|
y: number;
|
|
21
|
+
} | {
|
|
22
|
+
type: 'chat';
|
|
23
|
+
chatId: string;
|
|
24
|
+
agentId: string;
|
|
25
|
+
message: string;
|
|
19
26
|
};
|
|
20
27
|
export type MonitorCommandHandler = (cmd: MonitorCommand) => void | Promise<void>;
|
|
21
28
|
export interface MonitorWsHandle {
|
package/dist/monitor-ws.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// src/monitor-ws.ts
|
|
2
2
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
4
|
export function createMonitorWs(opts) {
|
|
4
5
|
const clients = new Set();
|
|
5
6
|
const wss = new WebSocketServer({ server: opts.server, path: opts.path });
|
|
6
|
-
function handleCommand(cmd) {
|
|
7
|
+
function handleCommand(cmd, senderWs) {
|
|
7
8
|
if (cmd.type === 'reload') {
|
|
8
9
|
const result = opts.registry.reload(cmd.agentId);
|
|
9
10
|
// WS protocol is fire-and-forget for commands; on failure, log so the
|
|
@@ -18,6 +19,83 @@ export function createMonitorWs(opts) {
|
|
|
18
19
|
if (!agent)
|
|
19
20
|
return;
|
|
20
21
|
agent.manifest.monitor = { ...agent.manifest.monitor, position: { x: cmd.x, y: cmd.y } };
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (cmd.type === 'chat') {
|
|
25
|
+
handleChat(cmd, senderWs).catch((err) => {
|
|
26
|
+
console.error(`[monitor-ws] chat error for "${cmd.agentId}":`, err);
|
|
27
|
+
const errEvent = {
|
|
28
|
+
type: 'chat_response',
|
|
29
|
+
chatId: cmd.chatId,
|
|
30
|
+
agentId: cmd.agentId,
|
|
31
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
32
|
+
final: true,
|
|
33
|
+
};
|
|
34
|
+
if (senderWs.readyState === WebSocket.OPEN) {
|
|
35
|
+
senderWs.send(JSON.stringify(errEvent));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Proxy a chat message to the A2A endpoint and stream the reply back. */
|
|
41
|
+
async function handleChat(cmd, senderWs) {
|
|
42
|
+
const body = {
|
|
43
|
+
jsonrpc: '2.0',
|
|
44
|
+
id: 1,
|
|
45
|
+
method: 'message/send',
|
|
46
|
+
params: {
|
|
47
|
+
message: {
|
|
48
|
+
messageId: uuidv4(),
|
|
49
|
+
kind: 'message',
|
|
50
|
+
role: 'user',
|
|
51
|
+
parts: [{ kind: 'text', text: cmd.message }],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
const res = await fetch(`http://localhost:${opts.a2aPort}/a2a/${cmd.agentId}`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify(body),
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
throw new Error(`A2A returned ${res.status}: ${await res.text()}`);
|
|
62
|
+
}
|
|
63
|
+
const json = await res.json();
|
|
64
|
+
// Extract the agent's reply text from the A2A JSON-RPC response
|
|
65
|
+
let replyText = '';
|
|
66
|
+
const result = json.result;
|
|
67
|
+
if (result) {
|
|
68
|
+
// Try artifacts first (main response content)
|
|
69
|
+
const artifacts = result.artifacts ?? [];
|
|
70
|
+
for (const a of artifacts) {
|
|
71
|
+
for (const p of (a.parts ?? [])) {
|
|
72
|
+
if (p.kind === 'text')
|
|
73
|
+
replyText += p.text;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Fall back to status message
|
|
77
|
+
if (!replyText) {
|
|
78
|
+
const statusMsg = result.status?.message;
|
|
79
|
+
if (statusMsg) {
|
|
80
|
+
for (const p of (statusMsg.parts ?? [])) {
|
|
81
|
+
if (p.kind === 'text')
|
|
82
|
+
replyText += p.text;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!replyText)
|
|
88
|
+
replyText = '(no response)';
|
|
89
|
+
// Send the response only to the requesting client
|
|
90
|
+
const event = {
|
|
91
|
+
type: 'chat_response',
|
|
92
|
+
chatId: cmd.chatId,
|
|
93
|
+
agentId: cmd.agentId,
|
|
94
|
+
text: replyText,
|
|
95
|
+
final: true,
|
|
96
|
+
};
|
|
97
|
+
if (senderWs.readyState === WebSocket.OPEN) {
|
|
98
|
+
senderWs.send(JSON.stringify(event));
|
|
21
99
|
}
|
|
22
100
|
}
|
|
23
101
|
wss.on('connection', (ws) => {
|
|
@@ -40,8 +118,8 @@ export function createMonitorWs(opts) {
|
|
|
40
118
|
const parsed = JSON.parse(data.toString());
|
|
41
119
|
if (parsed &&
|
|
42
120
|
typeof parsed === 'object' &&
|
|
43
|
-
(parsed.type === 'reload' || parsed.type === 'set_position')) {
|
|
44
|
-
handleCommand(parsed);
|
|
121
|
+
(parsed.type === 'reload' || parsed.type === 'set_position' || parsed.type === 'chat')) {
|
|
122
|
+
handleCommand(parsed, ws);
|
|
45
123
|
}
|
|
46
124
|
}
|
|
47
125
|
catch {
|
package/dist/server.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface DojoServer {
|
|
|
11
11
|
registry: AgentRegistry;
|
|
12
12
|
bus: ReturnType<typeof createMonitorBus>;
|
|
13
13
|
httpServer: ReturnType<typeof createHttpServer>;
|
|
14
|
+
/** Actual monitor WebSocket port (undefined when --no-monitor). */
|
|
15
|
+
monitorPort?: number;
|
|
14
16
|
close: () => Promise<void>;
|
|
15
17
|
}
|
|
16
18
|
export declare function createServer(opts: CreateServerOptions): Promise<DojoServer>;
|
package/dist/server.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { createServer as createHttpServer } from 'http';
|
|
2
|
+
import { resolve, join } from 'path';
|
|
2
3
|
import { AgentRegistry } from './agent-registry.js';
|
|
3
4
|
import { createA2AServer } from './a2a-server.js';
|
|
4
5
|
import { createReloadApi } from './reload-api.js';
|
|
5
6
|
import { createMetricsRouter, setAgentsLoaded } from './metrics.js';
|
|
6
7
|
import { createMonitorBus } from './monitor-bus.js';
|
|
7
8
|
import { createMonitorWs } from './monitor-ws.js';
|
|
9
|
+
import { startMonitorLogger, readLogs, listLogDates } from './log-writer.js';
|
|
8
10
|
export async function createServer(opts) {
|
|
9
|
-
// Create bus FIRST so handlers can reference it
|
|
10
11
|
const bus = createMonitorBus();
|
|
12
|
+
// Start monitor event logger
|
|
13
|
+
const logDir = join(resolve(opts.agentsDir), '..', 'monitor-logs');
|
|
14
|
+
startMonitorLogger(bus, logDir);
|
|
11
15
|
const registry = new AgentRegistry(opts.agentsDir);
|
|
12
16
|
registry.load();
|
|
13
17
|
setAgentsLoaded(registry.list().length);
|
|
@@ -18,22 +22,45 @@ export async function createServer(opts) {
|
|
|
18
22
|
registry.on('agent_reloaded', (e) => bus.emit({ type: 'agent_reloaded', ...e }));
|
|
19
23
|
const a2a = createA2AServer({ registry, singleAgent: opts.singleAgent, port: opts.port, monitorBus: bus });
|
|
20
24
|
const app = a2a.app;
|
|
25
|
+
// CORS for Monitor GUI (runs on a different port)
|
|
26
|
+
app.use('/logs', (_req, res, next) => {
|
|
27
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
28
|
+
res.header('Access-Control-Allow-Methods', 'GET');
|
|
29
|
+
next();
|
|
30
|
+
});
|
|
21
31
|
app.use('/agents', createReloadApi(registry));
|
|
22
32
|
app.use('/metrics', createMetricsRouter());
|
|
33
|
+
// Logs REST API
|
|
34
|
+
app.get('/logs', (_req, res) => {
|
|
35
|
+
const date = _req.query.date;
|
|
36
|
+
res.json({ entries: readLogs(logDir, date) });
|
|
37
|
+
});
|
|
38
|
+
app.get('/logs/dates', (_req, res) => {
|
|
39
|
+
res.json({ dates: listLogDates(logDir) });
|
|
40
|
+
});
|
|
23
41
|
const httpServer = createHttpServer(app);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
// Listen on main server first so we know the actual port (important when port=0)
|
|
43
|
+
await new Promise((r) => httpServer.listen(opts.port ?? 41241, r));
|
|
44
|
+
const actualA2APort = httpServer.address().port;
|
|
45
|
+
let monitorHttp;
|
|
46
|
+
let actualMonitorPort;
|
|
47
|
+
if (opts.monitorPort !== undefined) {
|
|
48
|
+
// Bind monitor on a separate server (port 0 = OS picks a free port)
|
|
49
|
+
monitorHttp = createHttpServer();
|
|
50
|
+
createMonitorWs({ server: monitorHttp, bus, path: '/monitor', registry, a2aPort: actualA2APort });
|
|
28
51
|
await new Promise((r) => monitorHttp.listen(opts.monitorPort, r));
|
|
52
|
+
actualMonitorPort = monitorHttp.address().port;
|
|
29
53
|
}
|
|
30
|
-
await new Promise((r) => httpServer.listen(opts.port ?? 41241, r));
|
|
31
54
|
return {
|
|
32
55
|
registry,
|
|
33
56
|
bus,
|
|
34
57
|
httpServer,
|
|
58
|
+
monitorPort: actualMonitorPort,
|
|
35
59
|
close: () => new Promise((resolve) => {
|
|
36
|
-
|
|
60
|
+
const closeMonitor = monitorHttp
|
|
61
|
+
? new Promise((r) => monitorHttp.close(() => r()))
|
|
62
|
+
: Promise.resolve();
|
|
63
|
+
httpServer.close(() => closeMonitor.then(resolve));
|
|
37
64
|
}),
|
|
38
65
|
};
|
|
39
66
|
}
|