agents-dojo 0.1.1 → 0.1.3
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/dist/agent-executor.js +74 -9
- package/dist/agent-loader.js +86 -1
- package/dist/agent-logger.d.ts +26 -0
- package/dist/agent-logger.js +63 -0
- package/dist/claude-bridge.js +2 -0
- package/dist/cli.js +84 -0
- package/dist/event-translator.js +9 -0
- package/dist/log-writer.d.ts +26 -0
- package/dist/log-writer.js +123 -0
- package/dist/server.js +19 -2
- package/package.json +1 -1
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
|
}
|
package/dist/agent-loader.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/agent-loader.ts
|
|
2
|
-
import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
|
|
2
|
+
import { readFileSync, readdirSync, existsSync, statSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join, resolve, isAbsolute } from 'path';
|
|
4
4
|
import stripJsonComments from 'strip-json-comments';
|
|
5
5
|
import { AgentManifestSchema } from './manifest-schema.js';
|
|
@@ -49,6 +49,8 @@ function resolveRef(agentDir, relPath) {
|
|
|
49
49
|
export function loadAgent(agentDir) {
|
|
50
50
|
const manifest = loadManifest(agentDir);
|
|
51
51
|
const fixedContextContent = loadContext(agentDir, manifest.fixedContext);
|
|
52
|
+
// Install built-in call-agent skill so this agent can communicate with peers
|
|
53
|
+
installBuiltinSkills(agentDir);
|
|
52
54
|
return {
|
|
53
55
|
manifest,
|
|
54
56
|
agentDir,
|
|
@@ -60,6 +62,89 @@ export function loadAgent(agentDir) {
|
|
|
60
62
|
sandboxPath: resolveRef(agentDir, manifest.sandbox),
|
|
61
63
|
};
|
|
62
64
|
}
|
|
65
|
+
const CALL_AGENT_SKILL_MD = `---
|
|
66
|
+
name: call-agent
|
|
67
|
+
description: "Call another agent in the same AgentsDojo server via A2A protocol. Use when you need to delegate a task, ask for help, or collaborate with a peer agent."
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
# Calling Other Agents
|
|
71
|
+
|
|
72
|
+
You can communicate with other agents running in the same AgentsDojo server.
|
|
73
|
+
|
|
74
|
+
## List Available Agents
|
|
75
|
+
|
|
76
|
+
\`\`\`bash
|
|
77
|
+
curl -s http://localhost:41241/agents
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
## Send a Message and Get Response
|
|
81
|
+
|
|
82
|
+
Use this helper script. It sends the message, then polls for the result (avoids blocking timeouts).
|
|
83
|
+
|
|
84
|
+
\`\`\`bash
|
|
85
|
+
python3 -c "
|
|
86
|
+
import urllib.request, json, uuid, time, sys
|
|
87
|
+
|
|
88
|
+
agent_id = 'AGENT_ID'
|
|
89
|
+
message = '''YOUR MESSAGE'''
|
|
90
|
+
url = f'http://localhost:41241/a2a/{agent_id}'
|
|
91
|
+
|
|
92
|
+
# Step 1: Send message
|
|
93
|
+
req_body = json.dumps({'jsonrpc':'2.0','id':1,'method':'message/send','params':{'message':{
|
|
94
|
+
'messageId':str(uuid.uuid4()),'kind':'message','role':'user',
|
|
95
|
+
'parts':[{'kind':'text','text':message}]
|
|
96
|
+
}}}).encode()
|
|
97
|
+
try:
|
|
98
|
+
req = urllib.request.Request(url, data=req_body, headers={'Content-Type':'application/json'})
|
|
99
|
+
resp = json.loads(urllib.request.urlopen(req, timeout=120).read())
|
|
100
|
+
result = resp.get('result',{})
|
|
101
|
+
except Exception as e:
|
|
102
|
+
print(f'Error sending: {e}', file=sys.stderr)
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
|
|
105
|
+
# Step 2: If task not completed yet, poll with tasks/get
|
|
106
|
+
task_id = result.get('id','')
|
|
107
|
+
state = result.get('status',{}).get('state','')
|
|
108
|
+
if state not in ('completed','failed') and task_id:
|
|
109
|
+
for _ in range(60):
|
|
110
|
+
time.sleep(2)
|
|
111
|
+
poll_body = json.dumps({'jsonrpc':'2.0','id':2,'method':'tasks/get','params':{'id':task_id}}).encode()
|
|
112
|
+
try:
|
|
113
|
+
poll_req = urllib.request.Request(url, data=poll_body, headers={'Content-Type':'application/json'})
|
|
114
|
+
poll_resp = json.loads(urllib.request.urlopen(poll_req, timeout=10).read())
|
|
115
|
+
result = poll_resp.get('result', result)
|
|
116
|
+
state = result.get('status',{}).get('state','')
|
|
117
|
+
if state in ('completed','failed'): break
|
|
118
|
+
except: pass
|
|
119
|
+
|
|
120
|
+
# Step 3: Extract response text
|
|
121
|
+
for a in result.get('artifacts',[]):
|
|
122
|
+
for p in a.get('parts',[]):
|
|
123
|
+
if p.get('kind')=='text': print(p['text'])
|
|
124
|
+
msg = result.get('status',{}).get('message',{})
|
|
125
|
+
if isinstance(msg, dict):
|
|
126
|
+
for p in msg.get('parts',[]):
|
|
127
|
+
if p.get('kind')=='text': print(p['text'])
|
|
128
|
+
"
|
|
129
|
+
\`\`\`
|
|
130
|
+
|
|
131
|
+
Replace AGENT_ID and YOUR MESSAGE with the target agent and your message.
|
|
132
|
+
|
|
133
|
+
## Rules
|
|
134
|
+
|
|
135
|
+
- Be specific about what you need from the other agent
|
|
136
|
+
- Include relevant context — the other agent has no shared memory with you
|
|
137
|
+
- Don't call agents in a loop — if one can't help, handle it yourself
|
|
138
|
+
`;
|
|
139
|
+
function installBuiltinSkills(agentDir) {
|
|
140
|
+
const skillDir = join(agentDir, '.claude', 'skills', 'call-agent');
|
|
141
|
+
const skillFile = join(skillDir, 'SKILL.md');
|
|
142
|
+
// Only write if missing or outdated (check by size as a simple heuristic)
|
|
143
|
+
if (!existsSync(skillFile)) {
|
|
144
|
+
mkdirSync(skillDir, { recursive: true });
|
|
145
|
+
writeFileSync(skillFile, CALL_AGENT_SKILL_MD);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
63
148
|
export function loadAgents(agentsDir) {
|
|
64
149
|
const result = new Map();
|
|
65
150
|
const absDir = resolve(agentsDir);
|
|
@@ -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/claude-bridge.js
CHANGED
|
@@ -8,6 +8,8 @@ export async function* runClaude(params) {
|
|
|
8
8
|
? `${agent.fixedContextContent}\n\n${m.systemPromptAppend}`
|
|
9
9
|
: agent.fixedContextContent;
|
|
10
10
|
const env = {
|
|
11
|
+
// Preserve system PATH so agents can use curl, python3, node etc.
|
|
12
|
+
PATH: process.env.PATH,
|
|
11
13
|
...(m.env ?? {}),
|
|
12
14
|
};
|
|
13
15
|
// Only override CLAUDE_CONFIG_DIR if the user explicitly specified configDir.
|
package/dist/cli.js
CHANGED
|
@@ -42,6 +42,8 @@ function runInit(targetDir) {
|
|
|
42
42
|
mkdirSync(echoDir, { recursive: true });
|
|
43
43
|
writeFileSync(join(echoDir, 'manifest.jsonc'), ECHO_MANIFEST);
|
|
44
44
|
writeFileSync(join(echoDir, 'context.md'), ECHO_CONTEXT);
|
|
45
|
+
// Install built-in call-agent skill
|
|
46
|
+
installCallAgentSkill(echoDir);
|
|
45
47
|
console.log(`Created agents/echo/ with manifest.jsonc and context.md.
|
|
46
48
|
|
|
47
49
|
Next steps:
|
|
@@ -53,6 +55,88 @@ To add your own agent:
|
|
|
53
55
|
# create manifest.jsonc and context.md inside it
|
|
54
56
|
`);
|
|
55
57
|
}
|
|
58
|
+
// ── Built-in skill: call-agent ────────────────────────────
|
|
59
|
+
const CALL_AGENT_SKILL = `---
|
|
60
|
+
name: call-agent
|
|
61
|
+
description: "Call another agent in the same AgentsDojo server via A2A protocol. Use this when you need to delegate a task, ask for help, or collaborate with another agent. TRIGGER when: you need expertise from another agent, the task requires skills outside your role, or you want to discuss/validate something with a peer."
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
# Calling Other Agents
|
|
65
|
+
|
|
66
|
+
You can communicate with other agents running in the same AgentsDojo server.
|
|
67
|
+
|
|
68
|
+
## Discover Available Agents
|
|
69
|
+
|
|
70
|
+
\`\`\`bash
|
|
71
|
+
curl -s http://localhost:41241/a2a/*/card 2>/dev/null || \\
|
|
72
|
+
for id in $(curl -s http://localhost:41241/agents 2>/dev/null | python3 -c "import json,sys;[print(a) for a in json.load(sys.stdin)]" 2>/dev/null); do
|
|
73
|
+
echo "--- $id ---"
|
|
74
|
+
curl -s "http://localhost:41241/a2a/$id/card" 2>/dev/null | python3 -c "import json,sys;d=json.load(sys.stdin);print(f\\"{d['name']}: {d['description']}\\");" 2>/dev/null
|
|
75
|
+
done
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
## Send a Message to Another Agent
|
|
79
|
+
|
|
80
|
+
To ask another agent a question or delegate a task, use this curl command:
|
|
81
|
+
|
|
82
|
+
\`\`\`bash
|
|
83
|
+
curl -s -X POST http://localhost:41241/a2a/<AGENT_ID> \\
|
|
84
|
+
-H "Content-Type: application/json" \\
|
|
85
|
+
-d '{
|
|
86
|
+
"jsonrpc": "2.0",
|
|
87
|
+
"id": 1,
|
|
88
|
+
"method": "message/send",
|
|
89
|
+
"params": {
|
|
90
|
+
"message": {
|
|
91
|
+
"messageId": "'$(python3 -c "import uuid;print(uuid.uuid4())")'",
|
|
92
|
+
"kind": "message",
|
|
93
|
+
"role": "user",
|
|
94
|
+
"parts": [{"kind": "text", "text": "YOUR MESSAGE HERE"}]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}'
|
|
98
|
+
\`\`\`
|
|
99
|
+
|
|
100
|
+
Replace \`<AGENT_ID>\` with the target agent's ID (e.g., \`alice\`, \`bob\`, \`reviewer\`).
|
|
101
|
+
|
|
102
|
+
## Parse the Response
|
|
103
|
+
|
|
104
|
+
The response is a JSON-RPC result. Extract the agent's reply text:
|
|
105
|
+
|
|
106
|
+
\`\`\`bash
|
|
107
|
+
curl -s -X POST http://localhost:41241/a2a/<AGENT_ID> \\
|
|
108
|
+
-H "Content-Type: application/json" \\
|
|
109
|
+
-d '{ ... }' | python3 -c "
|
|
110
|
+
import json, sys
|
|
111
|
+
r = json.load(sys.stdin)
|
|
112
|
+
result = r.get('result', {})
|
|
113
|
+
# Check for artifacts
|
|
114
|
+
artifacts = result.get('artifacts', [])
|
|
115
|
+
for a in artifacts:
|
|
116
|
+
for p in a.get('parts', []):
|
|
117
|
+
if p.get('kind') == 'text':
|
|
118
|
+
print(p['text'])
|
|
119
|
+
# Check for status message
|
|
120
|
+
status = result.get('status', {})
|
|
121
|
+
msg = status.get('message', {})
|
|
122
|
+
for p in msg.get('parts', []):
|
|
123
|
+
if p.get('kind') == 'text':
|
|
124
|
+
print(p['text'])
|
|
125
|
+
"
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
## Guidelines
|
|
129
|
+
|
|
130
|
+
- **Be specific** about what you need from the other agent
|
|
131
|
+
- **Include context** — the other agent doesn't share your conversation history
|
|
132
|
+
- **Don't loop** — if an agent can't help, handle it yourself instead of retrying
|
|
133
|
+
- You can call multiple agents in sequence to build on each other's output
|
|
134
|
+
`;
|
|
135
|
+
function installCallAgentSkill(agentDir) {
|
|
136
|
+
const skillDir = join(agentDir, '.claude', 'skills', 'call-agent');
|
|
137
|
+
mkdirSync(skillDir, { recursive: true });
|
|
138
|
+
writeFileSync(join(skillDir, 'SKILL.md'), CALL_AGENT_SKILL);
|
|
139
|
+
}
|
|
56
140
|
export const DEFAULT_ARGS = {
|
|
57
141
|
agentsDir: './agents',
|
|
58
142
|
port: 41241,
|
package/dist/event-translator.js
CHANGED
|
@@ -25,6 +25,13 @@ export function createTranslator(ctx) {
|
|
|
25
25
|
},
|
|
26
26
|
final: false,
|
|
27
27
|
});
|
|
28
|
+
// Also emit to monitor bus so the GUI can show speech bubbles
|
|
29
|
+
ctx.monitorBus?.emit({
|
|
30
|
+
type: 'task_status',
|
|
31
|
+
taskId: ctx.taskId,
|
|
32
|
+
state: 'working',
|
|
33
|
+
message: text.length > 80 ? text.slice(0, 77) + '...' : text,
|
|
34
|
+
});
|
|
28
35
|
}
|
|
29
36
|
function publishCompleted() {
|
|
30
37
|
ctx.bus.publish({
|
|
@@ -38,6 +45,7 @@ export function createTranslator(ctx) {
|
|
|
38
45
|
},
|
|
39
46
|
final: true,
|
|
40
47
|
});
|
|
48
|
+
ctx.monitorBus?.emit({ type: 'task_status', taskId: ctx.taskId, state: 'completed' });
|
|
41
49
|
ctx.bus.finished();
|
|
42
50
|
}
|
|
43
51
|
function publishFailed(reason) {
|
|
@@ -60,6 +68,7 @@ export function createTranslator(ctx) {
|
|
|
60
68
|
},
|
|
61
69
|
final: true,
|
|
62
70
|
});
|
|
71
|
+
ctx.monitorBus?.emit({ type: 'task_status', taskId: ctx.taskId, state: 'failed', message: reason });
|
|
63
72
|
ctx.bus.finished();
|
|
64
73
|
}
|
|
65
74
|
function summarizeInput(input) {
|
|
@@ -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/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,11 +22,24 @@ 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
42
|
if (opts.monitorPort) {
|
|
25
|
-
// Bind monitor on a separate server
|
|
26
43
|
const monitorHttp = createHttpServer();
|
|
27
44
|
createMonitorWs({ server: monitorHttp, bus, path: '/monitor', registry });
|
|
28
45
|
await new Promise((r) => monitorHttp.listen(opts.monitorPort, r));
|