agents-dojo 0.1.9 → 0.1.11
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 +63 -3
- package/dist/agent-loader.js +31 -5
- package/dist/claude-bridge.js +23 -8
- package/dist/cli.js +107 -12
- package/dist/server.js +5 -0
- package/monitor/src/components/LogPage.tsx +1 -1
- package/monitor/src/lib/dojo-app.ts +91 -83
- package/monitor/src/lib/interactables.ts +2 -2
- package/monitor/src/lib/ws-client.ts +28 -9
- package/monitor/vite.config.ts +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,13 +15,55 @@ agents-dojo init
|
|
|
15
15
|
agents-dojo
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## Creating Agents
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
Each agent lives in its own directory under `agents/`. The only required file is `manifest.jsonc`.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
agents/my-agent/
|
|
24
|
+
├── manifest.jsonc # required — agent identity, model, A2A card
|
|
25
|
+
├── context.md # optional — custom system prompt
|
|
26
|
+
└── CLAUDE.md # optional — extra rules and constraints
|
|
27
|
+
```
|
|
22
28
|
|
|
23
29
|
See `agents/_template_echo/` for a complete manifest reference.
|
|
24
30
|
|
|
31
|
+
### context.md — Custom System Prompt
|
|
32
|
+
|
|
33
|
+
`context.md` controls the agent's system prompt:
|
|
34
|
+
|
|
35
|
+
- **Without `context.md`**: The agent uses Claude Code's built-in default system
|
|
36
|
+
prompt. This is ideal for **code-related agents** — they get full programming
|
|
37
|
+
capabilities (file editing, bash, code analysis, etc.) out of the box.
|
|
38
|
+
|
|
39
|
+
- **With `context.md`**: The file content **replaces** the default system prompt
|
|
40
|
+
entirely. Use this for agents whose role has **nothing to do with coding** —
|
|
41
|
+
e.g. a chef, poet, or customer support agent. You define the persona, behavior
|
|
42
|
+
rules, and output format from scratch.
|
|
43
|
+
|
|
44
|
+
### CLAUDE.md — Extra Rules and Constraints
|
|
45
|
+
|
|
46
|
+
`CLAUDE.md` is loaded automatically by the Claude Code SDK (not by AgentsDojo).
|
|
47
|
+
It **appends** to whatever system prompt is active — whether the default or a
|
|
48
|
+
custom `context.md`.
|
|
49
|
+
|
|
50
|
+
Use `CLAUDE.md` for:
|
|
51
|
+
- Project-specific rules ("always use TypeScript", "never delete files")
|
|
52
|
+
- Coding conventions and style guides
|
|
53
|
+
- Build/test commands the agent should know about
|
|
54
|
+
- Safety constraints and guardrails
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
agents/code-reviewer/
|
|
58
|
+
├── manifest.jsonc # identity
|
|
59
|
+
└── CLAUDE.md # "Review for security issues. Run `npm test` after changes."
|
|
60
|
+
|
|
61
|
+
agents/poet/
|
|
62
|
+
├── manifest.jsonc # identity
|
|
63
|
+
├── context.md # "You are Poet Luna, a lyrical poet..."
|
|
64
|
+
└── CLAUDE.md # "Always respond in English. Keep poems under 20 lines."
|
|
65
|
+
```
|
|
66
|
+
|
|
25
67
|
## Monitor GUI
|
|
26
68
|
|
|
27
69
|
The Monitor GUI starts automatically when you run `agents-dojo`. It picks a free
|
|
@@ -34,6 +76,24 @@ agents-dojo --monitor-port=9000 # use a specific WS port
|
|
|
34
76
|
agents-dojo --no-monitor # disable the monitor
|
|
35
77
|
```
|
|
36
78
|
|
|
79
|
+
## Chat with Agents
|
|
80
|
+
|
|
81
|
+
Users can talk to agents via btw-style one-shot chat. This forks the agent's
|
|
82
|
+
current session (preserving context), disables all tools (text-only reply), and
|
|
83
|
+
discards the fork afterward — the main session is never affected.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# CLI
|
|
87
|
+
agents-dojo chat <agentId> <message>
|
|
88
|
+
|
|
89
|
+
# REST API
|
|
90
|
+
curl -X POST http://localhost:41241/btw/<agentId> \
|
|
91
|
+
-H "Content-Type: application/json" \
|
|
92
|
+
-d '{"message": "your question"}'
|
|
93
|
+
|
|
94
|
+
# Monitor GUI: click an agent sprite → Chat
|
|
95
|
+
```
|
|
96
|
+
|
|
37
97
|
## Architecture
|
|
38
98
|
|
|
39
99
|
- Framework (`src/`): A2A server, Claude Code SDK bridge, monitor event bus
|
package/dist/agent-loader.js
CHANGED
|
@@ -135,15 +135,41 @@ Replace AGENT_ID and YOUR MESSAGE with the target agent and your message.
|
|
|
135
135
|
- Be specific about what you need from the other agent
|
|
136
136
|
- Include relevant context — the other agent has no shared memory with you
|
|
137
137
|
- Don't call agents in a loop — if one can't help, handle it yourself
|
|
138
|
+
|
|
139
|
+
## MANDATORY: Post-Task Optimization
|
|
140
|
+
|
|
141
|
+
**After completing ANY task, you MUST call the \`optimizer\` agent.**
|
|
142
|
+
|
|
143
|
+
Send it a message with:
|
|
144
|
+
- Your agent ID
|
|
145
|
+
- A brief summary of what the task was and how it went
|
|
146
|
+
- Any difficulties or mistakes you encountered
|
|
147
|
+
|
|
148
|
+
Example (replace YOUR_AGENT_ID and summary):
|
|
149
|
+
|
|
150
|
+
\`\`\`bash
|
|
151
|
+
python3 -c "
|
|
152
|
+
import urllib.request, json, uuid, sys
|
|
153
|
+
url = 'http://localhost:41241/a2a/optimizer'
|
|
154
|
+
msg = 'Agent: YOUR_AGENT_ID. Task summary: [what you did]. Issues: [any problems or inefficiencies].'
|
|
155
|
+
body = json.dumps({'jsonrpc':'2.0','id':1,'method':'message/send','params':{'message':{
|
|
156
|
+
'messageId':str(uuid.uuid4()),'kind':'message','role':'user',
|
|
157
|
+
'parts':[{'kind':'text','text':msg}]}}}).encode()
|
|
158
|
+
try:
|
|
159
|
+
req = urllib.request.Request(url, data=body, headers={'Content-Type':'application/json'})
|
|
160
|
+
urllib.request.urlopen(req, timeout=120)
|
|
161
|
+
except: pass
|
|
162
|
+
"
|
|
163
|
+
\`\`\`
|
|
164
|
+
|
|
165
|
+
This is NOT optional. Every task must end with an optimizer notification.
|
|
138
166
|
`;
|
|
139
167
|
function installBuiltinSkills(agentDir) {
|
|
140
168
|
const skillDir = join(agentDir, '.claude', 'skills', 'call-agent');
|
|
141
169
|
const skillFile = join(skillDir, 'SKILL.md');
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
writeFileSync(skillFile, CALL_AGENT_SKILL_MD);
|
|
146
|
-
}
|
|
170
|
+
// Always overwrite to keep skill up to date with framework version
|
|
171
|
+
mkdirSync(skillDir, { recursive: true });
|
|
172
|
+
writeFileSync(skillFile, CALL_AGENT_SKILL_MD);
|
|
147
173
|
}
|
|
148
174
|
export function loadAgents(agentsDir) {
|
|
149
175
|
const result = new Map();
|
package/dist/claude-bridge.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/claude-bridge.ts
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { existsSync, mkdirSync, symlinkSync } from 'fs';
|
|
3
|
+
import { existsSync, mkdirSync, symlinkSync, lstatSync, unlinkSync } from 'fs';
|
|
4
4
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
5
5
|
export async function* runClaude(params) {
|
|
6
6
|
const { agent, contentBlocks, onEvent, abortController } = params;
|
|
@@ -22,16 +22,31 @@ export async function* runClaude(params) {
|
|
|
22
22
|
? (m.configDir.startsWith('/') ? m.configDir : join(agent.agentDir, m.configDir))
|
|
23
23
|
: join(agent.agentDir, '.claude');
|
|
24
24
|
env.CLAUDE_CONFIG_DIR = agentConfigDir;
|
|
25
|
-
// Ensure the agent's .claude dir exists and symlink global
|
|
26
|
-
//
|
|
25
|
+
// Ensure the agent's .claude dir exists and symlink global config files.
|
|
26
|
+
// Symlinks keep all agents in sync with the user's global config automatically.
|
|
27
27
|
mkdirSync(agentConfigDir, { recursive: true });
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const globalDir = join(process.env.HOME ?? '', '.claude');
|
|
29
|
+
// Symlink settings.json, .credentials.json (auth), and .claude.json (user identity)
|
|
30
|
+
for (const file of ['settings.json', '.credentials.json', '.claude.json']) {
|
|
31
|
+
const globalFile = join(globalDir, file);
|
|
32
|
+
const localFile = join(agentConfigDir, file);
|
|
33
|
+
if (!existsSync(globalFile))
|
|
34
|
+
continue;
|
|
35
|
+
// Replace regular files with symlinks (but keep existing symlinks)
|
|
31
36
|
try {
|
|
32
|
-
|
|
37
|
+
const stat = lstatSync(localFile);
|
|
38
|
+
if (!stat.isSymbolicLink()) {
|
|
39
|
+
unlinkSync(localFile);
|
|
40
|
+
symlinkSync(globalFile, localFile);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// File doesn't exist — create symlink
|
|
45
|
+
try {
|
|
46
|
+
symlinkSync(globalFile, localFile);
|
|
47
|
+
}
|
|
48
|
+
catch { /* ignore */ }
|
|
33
49
|
}
|
|
34
|
-
catch { /* race or permission — ignore */ }
|
|
35
50
|
}
|
|
36
51
|
const options = {
|
|
37
52
|
cwd: agent.agentDir,
|
package/dist/cli.js
CHANGED
|
@@ -36,19 +36,113 @@ You are **Echo Agent**, a minimal example agent.
|
|
|
36
36
|
Repeat back the user's input, prefixed with "ECHO:".
|
|
37
37
|
Do not add any other commentary.
|
|
38
38
|
`;
|
|
39
|
+
const OPTIMIZER_MANIFEST = `{
|
|
40
|
+
"id": "optimizer",
|
|
41
|
+
"name": "Optimizer",
|
|
42
|
+
"description": "Analyzes agent session logs and generates optimization suggestions for other agents.",
|
|
43
|
+
"version": "0.1.0",
|
|
44
|
+
"fixedContext": "context.md",
|
|
45
|
+
"a2aCard": {
|
|
46
|
+
"skills": [
|
|
47
|
+
{
|
|
48
|
+
"id": "optimize",
|
|
49
|
+
"name": "Agent Optimization",
|
|
50
|
+
"description": "Reviews agent conversation logs and produces actionable improvement suggestions",
|
|
51
|
+
"tags": ["optimization", "meta"]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"monitor": {
|
|
56
|
+
"position": { "x": 480, "y": 550 },
|
|
57
|
+
"sprite": "default"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
const OPTIMIZER_CONTEXT = `# Role
|
|
62
|
+
|
|
63
|
+
You are the **Optimizer Agent** for AgentsDojo. Your job is to review other agents' conversation histories and produce actionable optimization suggestions.
|
|
64
|
+
|
|
65
|
+
## When You Are Called
|
|
66
|
+
|
|
67
|
+
Other agents call you after completing a task. They send you:
|
|
68
|
+
- Their agent ID
|
|
69
|
+
- A summary of what the task was
|
|
70
|
+
- Any difficulties or mistakes they encountered
|
|
71
|
+
|
|
72
|
+
## What You Do
|
|
73
|
+
|
|
74
|
+
1. Read the calling agent's session logs to understand the full conversation history:
|
|
75
|
+
\`\`\`bash
|
|
76
|
+
# Find and read the agent's session logs
|
|
77
|
+
find ../AGENT_ID/.claude/projects/ -name "*.jsonl" -exec cat {} \\;
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
2. Analyze the conversation for:
|
|
81
|
+
- Repeated mistakes or inefficiencies
|
|
82
|
+
- Misunderstandings of user intent
|
|
83
|
+
- Missed opportunities to use tools or call other agents
|
|
84
|
+
- Response quality issues (too verbose, too terse, off-topic)
|
|
85
|
+
- Patterns that could be improved with explicit rules
|
|
86
|
+
|
|
87
|
+
3. Write optimization suggestions to your reviews directory:
|
|
88
|
+
\`\`\`bash
|
|
89
|
+
mkdir -p reviews
|
|
90
|
+
# Write or update the review file for this agent
|
|
91
|
+
cat > reviews/AGENT_ID.md << 'REVIEW_EOF'
|
|
92
|
+
# Optimization Review: AGENT_ID
|
|
93
|
+
|
|
94
|
+
Date: [today's date]
|
|
95
|
+
Sessions analyzed: [count]
|
|
96
|
+
|
|
97
|
+
## Suggested CLAUDE.md Rules
|
|
98
|
+
|
|
99
|
+
Add these rules to agents/AGENT_ID/CLAUDE.md:
|
|
100
|
+
|
|
101
|
+
[specific, actionable rules based on your analysis]
|
|
102
|
+
|
|
103
|
+
## Observations
|
|
104
|
+
|
|
105
|
+
[detailed analysis of patterns you found]
|
|
106
|
+
REVIEW_EOF
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
## Output Format
|
|
110
|
+
|
|
111
|
+
Your review file must include:
|
|
112
|
+
- **Suggested CLAUDE.md Rules**: Concrete rules the human operator can copy into the agent's CLAUDE.md
|
|
113
|
+
- **Observations**: Evidence-based analysis explaining why each rule is recommended
|
|
114
|
+
|
|
115
|
+
## Important Rules
|
|
116
|
+
|
|
117
|
+
- Be specific and actionable — vague suggestions like "be better" are useless
|
|
118
|
+
- Base every suggestion on evidence from the logs — cite specific examples
|
|
119
|
+
- Focus on patterns, not one-off incidents
|
|
120
|
+
- Keep suggested rules concise — each rule should be 1-2 sentences
|
|
121
|
+
- Do NOT modify other agents' files directly — only write to your own reviews/ directory
|
|
122
|
+
- If the agent is performing well, say so — not every review needs changes
|
|
123
|
+
`;
|
|
39
124
|
function runInit(targetDir) {
|
|
40
125
|
const agentsDir = join(targetDir, 'agents');
|
|
41
126
|
const echoDir = join(agentsDir, 'echo');
|
|
127
|
+
const optimizerDir = join(agentsDir, 'optimizer');
|
|
42
128
|
if (existsSync(echoDir)) {
|
|
43
|
-
console.log(`agents/echo/ already exists — skipping.`);
|
|
44
|
-
|
|
129
|
+
console.log(`agents/echo/ already exists — skipping echo.`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
mkdirSync(echoDir, { recursive: true });
|
|
133
|
+
writeFileSync(join(echoDir, 'manifest.jsonc'), ECHO_MANIFEST);
|
|
134
|
+
writeFileSync(join(echoDir, 'context.md'), ECHO_CONTEXT);
|
|
135
|
+
installCallAgentSkill(echoDir);
|
|
136
|
+
}
|
|
137
|
+
if (existsSync(optimizerDir)) {
|
|
138
|
+
console.log(`agents/_optimizer/ already exists — skipping optimizer.`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
mkdirSync(optimizerDir, { recursive: true });
|
|
142
|
+
writeFileSync(join(optimizerDir, 'manifest.jsonc'), OPTIMIZER_MANIFEST);
|
|
143
|
+
writeFileSync(join(optimizerDir, 'context.md'), OPTIMIZER_CONTEXT);
|
|
45
144
|
}
|
|
46
|
-
|
|
47
|
-
writeFileSync(join(echoDir, 'manifest.jsonc'), ECHO_MANIFEST);
|
|
48
|
-
writeFileSync(join(echoDir, 'context.md'), ECHO_CONTEXT);
|
|
49
|
-
// Install built-in call-agent skill
|
|
50
|
-
installCallAgentSkill(echoDir);
|
|
51
|
-
console.log(`Created agents/echo/ with manifest.jsonc and context.md.
|
|
145
|
+
console.log(`Created agents/echo/ and agents/optimizer/.
|
|
52
146
|
|
|
53
147
|
Next steps:
|
|
54
148
|
agents-dojo # start the server
|
|
@@ -56,7 +150,7 @@ Next steps:
|
|
|
56
150
|
|
|
57
151
|
To add your own agent:
|
|
58
152
|
mkdir agents/my-agent
|
|
59
|
-
# create manifest.jsonc and context.md
|
|
153
|
+
# create manifest.jsonc (and optionally context.md)
|
|
60
154
|
`);
|
|
61
155
|
}
|
|
62
156
|
// ── Built-in skill: call-agent ────────────────────────────
|
|
@@ -144,7 +238,7 @@ function installCallAgentSkill(agentDir) {
|
|
|
144
238
|
export const DEFAULT_ARGS = {
|
|
145
239
|
agentsDir: './agents',
|
|
146
240
|
port: 41241,
|
|
147
|
-
monitorPort: 0, // 0 = OS picks a free port
|
|
241
|
+
monitorPort: 0, // 0 = OS picks a free port; frontend discovers via /monitor/info
|
|
148
242
|
singleAgent: null,
|
|
149
243
|
help: false,
|
|
150
244
|
};
|
|
@@ -211,7 +305,7 @@ function resolveMonitorDir() {
|
|
|
211
305
|
// __dirname is dist/ after build, so monitor/ is one level up
|
|
212
306
|
return resolve(__dirname, '..', 'monitor');
|
|
213
307
|
}
|
|
214
|
-
function startMonitorGui(monitorWsPort) {
|
|
308
|
+
function startMonitorGui(monitorWsPort, a2aPort = 41241) {
|
|
215
309
|
const monitorDir = resolveMonitorDir();
|
|
216
310
|
if (!existsSync(join(monitorDir, 'package.json'))) {
|
|
217
311
|
console.warn('[agents-dojo] Monitor GUI not found — skipping.');
|
|
@@ -235,6 +329,7 @@ function startMonitorGui(monitorWsPort) {
|
|
|
235
329
|
env: {
|
|
236
330
|
...process.env,
|
|
237
331
|
VITE_MONITOR_WS_URL: wsUrl,
|
|
332
|
+
VITE_A2A_PORT: String(a2aPort),
|
|
238
333
|
},
|
|
239
334
|
});
|
|
240
335
|
child.stdout?.on('data', (data) => {
|
|
@@ -323,7 +418,7 @@ async function main() {
|
|
|
323
418
|
// Auto-start monitor GUI
|
|
324
419
|
if (server.monitorPort) {
|
|
325
420
|
console.log(`[agents-dojo] Monitor WS port: ${server.monitorPort}`);
|
|
326
|
-
const monitorChild = startMonitorGui(server.monitorPort);
|
|
421
|
+
const monitorChild = startMonitorGui(server.monitorPort, args.port);
|
|
327
422
|
if (monitorChild) {
|
|
328
423
|
registerCleanup(monitorChild);
|
|
329
424
|
}
|
package/dist/server.js
CHANGED
|
@@ -71,6 +71,11 @@ export async function createServer(opts) {
|
|
|
71
71
|
createMonitorWs({ server: monitorHttp, bus, path: '/monitor', registry, executors: a2a.executors });
|
|
72
72
|
await new Promise((r) => monitorHttp.listen(opts.monitorPort, r));
|
|
73
73
|
actualMonitorPort = monitorHttp.address().port;
|
|
74
|
+
// Discovery endpoint: frontend fetches this to find the WS port
|
|
75
|
+
app.get('/monitor/info', (_req, res) => {
|
|
76
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
77
|
+
res.json({ wsPort: actualMonitorPort, wsUrl: `ws://localhost:${actualMonitorPort}/monitor` });
|
|
78
|
+
});
|
|
74
79
|
}
|
|
75
80
|
return {
|
|
76
81
|
registry,
|
|
@@ -126,6 +126,14 @@ export class DojoApp {
|
|
|
126
126
|
private agents = new Map<string, AgentState>();
|
|
127
127
|
private interactables: InteractableState[] = [];
|
|
128
128
|
private ready = false;
|
|
129
|
+
// Master state
|
|
130
|
+
private masterContainer!: Container;
|
|
131
|
+
private masterSprite!: Sprite;
|
|
132
|
+
private masterX = MASTER_POS.x;
|
|
133
|
+
private masterY = MASTER_POS.y;
|
|
134
|
+
private masterTargetX = MASTER_POS.x;
|
|
135
|
+
private masterTargetY = MASTER_POS.y;
|
|
136
|
+
private masterFacingLeft = false;
|
|
129
137
|
// Master bubble state
|
|
130
138
|
private masterBubble!: Container;
|
|
131
139
|
private masterBubbleBg!: Graphics;
|
|
@@ -247,13 +255,13 @@ export class DojoApp {
|
|
|
247
255
|
}
|
|
248
256
|
|
|
249
257
|
private createMaster() {
|
|
250
|
-
|
|
251
|
-
masterContainer.x = MASTER_POS.x;
|
|
252
|
-
masterContainer.y = MASTER_POS.y;
|
|
258
|
+
this.masterContainer = new Container();
|
|
259
|
+
this.masterContainer.x = MASTER_POS.x;
|
|
260
|
+
this.masterContainer.y = MASTER_POS.y;
|
|
253
261
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
262
|
+
this.masterSprite = new Sprite(this.masterFrames[0]);
|
|
263
|
+
this.masterSprite.anchor.set(0.5, 1);
|
|
264
|
+
this.masterSprite.scale.set(SPRITE_SCALE);
|
|
257
265
|
|
|
258
266
|
const nameBg = new Graphics();
|
|
259
267
|
nameBg.roundRect(-28, -FRAME_SIZE * SPRITE_SCALE - 14, 56, 14, 3);
|
|
@@ -271,8 +279,8 @@ export class DojoApp {
|
|
|
271
279
|
this.masterBubbleText.anchor.set(0.5, 0);
|
|
272
280
|
this.masterBubble.addChild(this.masterBubbleBg, this.masterBubbleText);
|
|
273
281
|
|
|
274
|
-
masterContainer.addChild(
|
|
275
|
-
this.scene.addChild(masterContainer);
|
|
282
|
+
this.masterContainer.addChild(this.masterSprite, nameBg, label, this.masterBubble);
|
|
283
|
+
this.scene.addChild(this.masterContainer);
|
|
276
284
|
}
|
|
277
285
|
|
|
278
286
|
private createAgent(id: string, agent: Agent) {
|
|
@@ -428,27 +436,29 @@ export class DojoApp {
|
|
|
428
436
|
}
|
|
429
437
|
// Priority 3: Client task states
|
|
430
438
|
else if (state === 'submitted') {
|
|
431
|
-
//
|
|
432
|
-
s.animKey = '
|
|
433
|
-
s.targetX =
|
|
439
|
+
// Agent stays on cushion (sit), Master walks to agent
|
|
440
|
+
s.animKey = 'sit';
|
|
441
|
+
s.targetX = agent.homePosition.x; s.targetY = agent.homePosition.y;
|
|
442
|
+
// Tell Master to walk to this agent
|
|
443
|
+
this.masterTargetX = agent.homePosition.x + 30;
|
|
444
|
+
this.masterTargetY = agent.homePosition.y;
|
|
434
445
|
s.visitedMaster = false;
|
|
435
446
|
}
|
|
436
447
|
else if (state === 'working' || state === 'tool_call') {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
s.targetX = agent.stakePosition.x + STAKE_ATTACK_OFFSET_X;
|
|
445
|
-
s.targetY = agent.stakePosition.y;
|
|
446
|
-
}
|
|
448
|
+
// Agent walks to stake and attacks
|
|
449
|
+
s.animKey = s.arrived ? 'attack' : 'walk';
|
|
450
|
+
s.targetX = agent.stakePosition.x + STAKE_ATTACK_OFFSET_X;
|
|
451
|
+
s.targetY = agent.stakePosition.y;
|
|
452
|
+
// Master returns home
|
|
453
|
+
this.masterTargetX = MASTER_POS.x;
|
|
454
|
+
this.masterTargetY = MASTER_POS.y;
|
|
447
455
|
}
|
|
448
456
|
else if (state === 'input-required' || state === 'auth-required') {
|
|
449
|
-
//
|
|
457
|
+
// Agent waits at stake, Master comes back to agent
|
|
450
458
|
s.animKey = s.arrived ? 'idle' : 'walk';
|
|
451
|
-
s.targetX =
|
|
459
|
+
s.targetX = agent.homePosition.x; s.targetY = agent.homePosition.y;
|
|
460
|
+
this.masterTargetX = agent.homePosition.x + 30;
|
|
461
|
+
this.masterTargetY = agent.homePosition.y;
|
|
452
462
|
}
|
|
453
463
|
else if (state === 'completed' || state === 'canceled') {
|
|
454
464
|
// Walk home
|
|
@@ -477,19 +487,20 @@ export class DojoApp {
|
|
|
477
487
|
}
|
|
478
488
|
}
|
|
479
489
|
else if (state === 'rejected') {
|
|
480
|
-
//
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
490
|
+
// Agent stays at cushion, shakes head when Master arrives
|
|
491
|
+
s.targetX = agent.homePosition.x; s.targetY = agent.homePosition.y;
|
|
492
|
+
this.masterTargetX = agent.homePosition.x + 30;
|
|
493
|
+
this.masterTargetY = agent.homePosition.y;
|
|
494
|
+
if (s.rejectTimer === undefined) {
|
|
485
495
|
s.animKey = 'head_shake';
|
|
486
496
|
s.rejectTimer = 0;
|
|
487
497
|
} else if (s.rejectTimer < 1500) {
|
|
488
498
|
s.animKey = 'head_shake';
|
|
489
499
|
} else {
|
|
490
|
-
|
|
491
|
-
s.
|
|
492
|
-
|
|
500
|
+
// Done shaking, go back to sit
|
|
501
|
+
s.animKey = 'sit';
|
|
502
|
+
this.masterTargetX = MASTER_POS.x;
|
|
503
|
+
this.masterTargetY = MASTER_POS.y;
|
|
493
504
|
}
|
|
494
505
|
}
|
|
495
506
|
// else: idle — handled by auto-behavior in tick
|
|
@@ -546,36 +557,10 @@ export class DojoApp {
|
|
|
546
557
|
}
|
|
547
558
|
if (s.idleGrace > 0) s.idleGrace -= dt;
|
|
548
559
|
|
|
549
|
-
// Auto-behavior when idle
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (action.kind === 'sit') {
|
|
554
|
-
// Sit on cushion (home position)
|
|
555
|
-
s.animKey = 'sit';
|
|
556
|
-
s.targetX = agent.homePosition.x; s.targetY = agent.homePosition.y;
|
|
557
|
-
action.timer += dt;
|
|
558
|
-
if (action.timer >= (action.duration ?? 5000)) {
|
|
559
|
-
s.autoAction = this.pickAutoAction();
|
|
560
|
-
s.arrived = false;
|
|
561
|
-
}
|
|
562
|
-
} else if (action.kind === 'wander') {
|
|
563
|
-
s.animKey = 'walk';
|
|
564
|
-
s.targetX = action.target!.x;
|
|
565
|
-
s.targetY = action.target!.y;
|
|
566
|
-
if (s.arrived) {
|
|
567
|
-
s.autoAction = this.pickAutoAction();
|
|
568
|
-
s.arrived = false;
|
|
569
|
-
}
|
|
570
|
-
} else if (action.kind === 'daydream' || action.kind === 'pause') {
|
|
571
|
-
s.animKey = action.kind === 'daydream' ? 'daydream' : 'idle';
|
|
572
|
-
s.targetX = s.x; s.targetY = s.y;
|
|
573
|
-
action.timer += dt;
|
|
574
|
-
if (action.timer >= (action.duration ?? 3000)) {
|
|
575
|
-
s.autoAction = this.pickAutoAction();
|
|
576
|
-
s.arrived = false;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
560
|
+
// Auto-behavior when idle: always sit on cushion
|
|
561
|
+
if (isIdle && !isInPeerChat) {
|
|
562
|
+
s.animKey = s.arrived ? 'sit' : 'walk';
|
|
563
|
+
s.targetX = agent.homePosition.x; s.targetY = agent.homePosition.y;
|
|
579
564
|
}
|
|
580
565
|
|
|
581
566
|
// Advance fail/reject timers
|
|
@@ -595,28 +580,17 @@ export class DojoApp {
|
|
|
595
580
|
if (Math.abs(dx) > 0.5) s.facingLeft = dx < 0;
|
|
596
581
|
let nx = s.x + (dx / dist) * MOVE_SPEED;
|
|
597
582
|
let ny = s.y + (dy / dist) * MOVE_SPEED;
|
|
598
|
-
// Obstacle avoidance when
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const od = Math.hypot(nx - o.x, ny - o.y);
|
|
602
|
-
if (od < o.r + 8 && od > 0.1) {
|
|
603
|
-
nx = o.x + ((nx - o.x) / od) * (o.r + 8);
|
|
604
|
-
ny = o.y + ((ny - o.y) / od) * (o.r + 8);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
583
|
+
// Obstacle avoidance: only when not heading to a specific target
|
|
584
|
+
// (idle agents heading home, or task agents heading to stake/master, skip avoidance)
|
|
585
|
+
// Avoidance is only useful during peer-chat or other free movement
|
|
608
586
|
s.x = nx; s.y = ny;
|
|
609
587
|
} else {
|
|
610
588
|
if (!s.arrived) { s.x = s.targetX; s.y = s.targetY; }
|
|
611
589
|
s.arrived = true;
|
|
612
590
|
}
|
|
613
591
|
|
|
614
|
-
//
|
|
615
|
-
|
|
616
|
-
&& Math.hypot(s.x - MASTER_POS.x - 30, s.y - MASTER_POS.y) < MOVE_SPEED * 3) {
|
|
617
|
-
s.visitedMaster = true;
|
|
618
|
-
s.arrived = false;
|
|
619
|
-
}
|
|
592
|
+
// Note: visitedMaster is now set by the Master movement block when Master
|
|
593
|
+
// arrives at the agent's cushion position.
|
|
620
594
|
|
|
621
595
|
// Peer chat: face toward partner when arrived
|
|
622
596
|
if (isInPeerChat && s.arrived) {
|
|
@@ -687,6 +661,44 @@ export class DojoApp {
|
|
|
687
661
|
}
|
|
688
662
|
}
|
|
689
663
|
|
|
664
|
+
// ── Master movement ──────────────────────────────────────
|
|
665
|
+
{
|
|
666
|
+
const dx = this.masterTargetX - this.masterX;
|
|
667
|
+
const dy = this.masterTargetY - this.masterY;
|
|
668
|
+
const dist = Math.hypot(dx, dy);
|
|
669
|
+
if (dist > MOVE_SPEED * 2) {
|
|
670
|
+
if (Math.abs(dx) > 0.5) this.masterFacingLeft = dx < 0;
|
|
671
|
+
this.masterX += (dx / dist) * MOVE_SPEED;
|
|
672
|
+
this.masterY += (dy / dist) * MOVE_SPEED;
|
|
673
|
+
// Use walk frame for master (alternate between frame 0 and 1)
|
|
674
|
+
const walkFrame = Math.floor(performance.now() / 200) % 2;
|
|
675
|
+
this.masterSprite.texture = this.masterFrames[walkFrame] ?? this.masterFrames[0];
|
|
676
|
+
} else {
|
|
677
|
+
this.masterX = this.masterTargetX;
|
|
678
|
+
this.masterY = this.masterTargetY;
|
|
679
|
+
this.masterSprite.texture = this.masterFrames[0];
|
|
680
|
+
|
|
681
|
+
// When Master arrives at an agent in submitted state → transition agent to working
|
|
682
|
+
// (The store will handle this via the normal task_status flow from backend,
|
|
683
|
+
// but visually we detect Master arrival to trigger agent getting up)
|
|
684
|
+
for (const [, s] of this.agents) {
|
|
685
|
+
if (!s.visitedMaster && s.animKey === 'sit') {
|
|
686
|
+
const agentStore = [...useMonitorStore.getState().agents.values()].find(
|
|
687
|
+
a => a.state === 'submitted' &&
|
|
688
|
+
Math.abs(a.homePosition.x + 30 - this.masterX) < 5 &&
|
|
689
|
+
Math.abs(a.homePosition.y - this.masterY) < 5
|
|
690
|
+
);
|
|
691
|
+
if (agentStore) {
|
|
692
|
+
s.visitedMaster = true;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
this.masterSprite.scale.x = (this.masterFacingLeft ? -1 : 1) * SPRITE_SCALE;
|
|
698
|
+
this.masterContainer.x = this.masterX;
|
|
699
|
+
this.masterContainer.y = this.masterY;
|
|
700
|
+
}
|
|
701
|
+
|
|
690
702
|
// Master bubble: show client's message with chunked display
|
|
691
703
|
const masterMsg = useMonitorStore.getState().masterMessage;
|
|
692
704
|
if (masterMsg) {
|
|
@@ -768,12 +780,8 @@ export class DojoApp {
|
|
|
768
780
|
}
|
|
769
781
|
|
|
770
782
|
private pickAutoAction() {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
if (r < 0.45) return { kind: 'sit', duration: 5000 + Math.random() * 8000, timer: 0 };
|
|
774
|
-
if (r < 0.70) return { kind: 'wander', target: randomFloorPos(), timer: 0 };
|
|
775
|
-
if (r < 0.85) return { kind: 'daydream', duration: 3000 + Math.random() * 5000, timer: 0 };
|
|
776
|
-
return { kind: 'pause', duration: 1000 + Math.random() * 2000, timer: 0 };
|
|
783
|
+
// Idle agents always sit on cushion
|
|
784
|
+
return { kind: 'sit', timer: 0 };
|
|
777
785
|
}
|
|
778
786
|
|
|
779
787
|
private animToStoreAnim(key: string): AgentAnim {
|
|
@@ -66,8 +66,8 @@ const STAKE_HALF_W = 21;
|
|
|
66
66
|
const STAKE_AGENT_GAP = 4; // gap between agent and stake edge
|
|
67
67
|
const STAKE_AGENT_OFFSET_X = STAKE_HALF_W + STAKE_AGENT_GAP; // 25px
|
|
68
68
|
|
|
69
|
-
// Cushion: agent sits
|
|
70
|
-
const CUSHION_SIT_OFFSET_Y =
|
|
69
|
+
// Cushion: agent sits on top of cushion — offset positions feet below cushion center
|
|
70
|
+
const CUSHION_SIT_OFFSET_Y = 7;
|
|
71
71
|
|
|
72
72
|
// ── Build interactable list from positions data ────────────
|
|
73
73
|
|
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
import { useMonitorStore } from './store.js';
|
|
2
2
|
import type { MonitorEvent } from './types.js';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/** Discover WS URL from the server's /monitor/info endpoint (proxied by Vite). */
|
|
5
|
+
async function discoverWsUrl(): Promise<string> {
|
|
6
|
+
try {
|
|
7
|
+
const resp = await fetch('/monitor/info');
|
|
8
|
+
const data = await resp.json();
|
|
9
|
+
if (data.wsUrl) return data.wsUrl;
|
|
10
|
+
} catch { /* fallback */ }
|
|
11
|
+
return import.meta.env.VITE_MONITOR_WS_URL || 'ws://localhost:41242/monitor';
|
|
12
|
+
}
|
|
5
13
|
|
|
6
14
|
/** Shared persistent connection for both events and commands. */
|
|
7
15
|
let sharedWs: WebSocket | null = null;
|
|
8
16
|
|
|
9
17
|
export function connectWs(): () => void {
|
|
10
|
-
|
|
18
|
+
let cancelled = false;
|
|
19
|
+
|
|
20
|
+
discoverWsUrl().then((wsUrl) => {
|
|
21
|
+
if (cancelled) return;
|
|
22
|
+
const ws = new WebSocket(wsUrl);
|
|
23
|
+
setupWs(ws);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return () => { cancelled = true; if (sharedWs) sharedWs.close(); };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setupWs(ws: WebSocket): void {
|
|
11
30
|
sharedWs = ws;
|
|
12
31
|
|
|
13
32
|
ws.addEventListener('open', () => {
|
|
@@ -32,8 +51,6 @@ export function connectWs(): () => void {
|
|
|
32
51
|
console.error('[ws-client] bad message:', err);
|
|
33
52
|
}
|
|
34
53
|
});
|
|
35
|
-
|
|
36
|
-
return () => ws.close();
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
export function sendCommand(cmd: object): void {
|
|
@@ -41,11 +58,13 @@ export function sendCommand(cmd: object): void {
|
|
|
41
58
|
sharedWs.send(JSON.stringify(cmd));
|
|
42
59
|
return;
|
|
43
60
|
}
|
|
44
|
-
// Fallback: open a one-shot connection
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
ws.
|
|
48
|
-
|
|
61
|
+
// Fallback: discover URL and open a one-shot connection
|
|
62
|
+
discoverWsUrl().then((url) => {
|
|
63
|
+
const ws = new WebSocket(url);
|
|
64
|
+
ws.addEventListener('open', () => {
|
|
65
|
+
ws.send(JSON.stringify(cmd));
|
|
66
|
+
ws.close();
|
|
67
|
+
});
|
|
49
68
|
});
|
|
50
69
|
}
|
|
51
70
|
|
package/monitor/vite.config.ts
CHANGED
|
@@ -9,8 +9,14 @@ export default defineConfig({
|
|
|
9
9
|
server: {
|
|
10
10
|
port: 5173,
|
|
11
11
|
proxy: {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
'/monitor/info': {
|
|
13
|
+
target: `http://localhost:${process.env.VITE_A2A_PORT || '41241'}`,
|
|
14
|
+
changeOrigin: true,
|
|
15
|
+
},
|
|
16
|
+
'/logs': {
|
|
17
|
+
target: `http://localhost:${process.env.VITE_A2A_PORT || '41241'}`,
|
|
18
|
+
changeOrigin: true,
|
|
19
|
+
},
|
|
14
20
|
},
|
|
15
21
|
},
|
|
16
22
|
});
|