agents-dojo 0.1.10 → 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.
@@ -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 settings (auth, permissions).
26
- // Symlink keeps all agents in sync with the user's global config automatically.
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 globalSettings = join(process.env.HOME ?? '', '.claude', 'settings.json');
29
- const localSettings = join(agentConfigDir, 'settings.json');
30
- if (existsSync(globalSettings) && !existsSync(localSettings)) {
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
- symlinkSync(globalSettings, localSettings);
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
@@ -238,7 +238,7 @@ function installCallAgentSkill(agentDir) {
238
238
  export const DEFAULT_ARGS = {
239
239
  agentsDir: './agents',
240
240
  port: 41241,
241
- monitorPort: 0, // 0 = OS picks a free port
241
+ monitorPort: 0, // 0 = OS picks a free port; frontend discovers via /monitor/info
242
242
  singleAgent: null,
243
243
  help: false,
244
244
  };
@@ -305,7 +305,7 @@ function resolveMonitorDir() {
305
305
  // __dirname is dist/ after build, so monitor/ is one level up
306
306
  return resolve(__dirname, '..', 'monitor');
307
307
  }
308
- function startMonitorGui(monitorWsPort) {
308
+ function startMonitorGui(monitorWsPort, a2aPort = 41241) {
309
309
  const monitorDir = resolveMonitorDir();
310
310
  if (!existsSync(join(monitorDir, 'package.json'))) {
311
311
  console.warn('[agents-dojo] Monitor GUI not found — skipping.');
@@ -329,6 +329,7 @@ function startMonitorGui(monitorWsPort) {
329
329
  env: {
330
330
  ...process.env,
331
331
  VITE_MONITOR_WS_URL: wsUrl,
332
+ VITE_A2A_PORT: String(a2aPort),
332
333
  },
333
334
  });
334
335
  child.stdout?.on('data', (data) => {
@@ -417,7 +418,7 @@ async function main() {
417
418
  // Auto-start monitor GUI
418
419
  if (server.monitorPort) {
419
420
  console.log(`[agents-dojo] Monitor WS port: ${server.monitorPort}`);
420
- const monitorChild = startMonitorGui(server.monitorPort);
421
+ const monitorChild = startMonitorGui(server.monitorPort, args.port);
421
422
  if (monitorChild) {
422
423
  registerCleanup(monitorChild);
423
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,
@@ -1,6 +1,6 @@
1
1
  import React, { useEffect, useState, useRef } from 'react';
2
2
 
3
- const API_BASE = 'http://localhost:41241';
3
+ const API_BASE = ''; // relative path, proxied by Vite
4
4
 
5
5
  interface ChatMessage {
6
6
  timestamp: string;
@@ -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
- const masterContainer = new Container();
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
- const spr = new Sprite(this.masterFrames[0]);
255
- spr.anchor.set(0.5, 1);
256
- spr.scale.set(SPRITE_SCALE);
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(spr, nameBg, label, this.masterBubble);
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
- // Walk to Master
432
- s.animKey = 'walk';
433
- s.targetX = MASTER_POS.x + 30; s.targetY = MASTER_POS.y;
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
- if (!s.visitedMaster) {
438
- // First visit master, then go to stake
439
- s.animKey = 'walk';
440
- s.targetX = MASTER_POS.x + 30; s.targetY = MASTER_POS.y;
441
- } else {
442
- // At stake, attack
443
- s.animKey = s.arrived ? 'attack' : 'walk';
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
- // Walk back to Master and wait
457
+ // Agent waits at stake, Master comes back to agent
450
458
  s.animKey = s.arrived ? 'idle' : 'walk';
451
- s.targetX = MASTER_POS.x + 30; s.targetY = MASTER_POS.y;
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
- // Head shake at Master, then walk home
481
- if (!s.arrived && Math.hypot(s.x - MASTER_POS.x - 30, s.y - MASTER_POS.y) > MOVE_SPEED * 3) {
482
- s.animKey = 'walk';
483
- s.targetX = MASTER_POS.x + 30; s.targetY = MASTER_POS.y;
484
- } else if (s.rejectTimer === undefined) {
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
- s.animKey = 'walk';
491
- s.targetX = agent.homePosition.x; s.targetY = agent.homePosition.y;
492
- s.arrived = false;
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 (sit on cushion, occasionally wander)
550
- // Skip during grace period after task completion
551
- if (isIdle && !isInPeerChat && s.idleGrace <= 0) {
552
- const action = s.autoAction;
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 idle wandering
599
- if (isIdle && !isInPeerChat) {
600
- for (const o of OBSTACLES) {
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
- // Master visit: when agent arrives at Master during working state
615
- if ((state === 'working' || state === 'tool_call') && !s.visitedMaster && s.arrived
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
- const r = Math.random();
772
- // Prefer sitting on cushion (idle default per R10: "坐在蒲团上发呆")
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 slightly above cushion center so body overlaps the cushion
70
- const CUSHION_SIT_OFFSET_Y = 2;
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
- const WS_URL = import.meta.env.VITE_MONITOR_WS_URL || 'ws://localhost:41242/monitor';
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
- const ws = new WebSocket(WS_URL);
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
- const ws = new WebSocket(WS_URL);
46
- ws.addEventListener('open', () => {
47
- ws.send(JSON.stringify(cmd));
48
- ws.close();
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
 
@@ -9,8 +9,14 @@ export default defineConfig({
9
9
  server: {
10
10
  port: 5173,
11
11
  proxy: {
12
- // Optional: proxy A2A HTTP calls to framework (if needed)
13
- // '/a2a': 'http://localhost:41241',
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-dojo",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "A2A-compatible Agent framework built on Claude Code SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",