agents-dojo 0.1.5 → 0.1.7

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.
Files changed (60) hide show
  1. package/dist/a2a-server.d.ts +2 -0
  2. package/dist/a2a-server.js +3 -0
  3. package/dist/agent-executor.d.ts +7 -0
  4. package/dist/agent-executor.js +35 -1
  5. package/dist/claude-bridge.d.ts +4 -0
  6. package/dist/claude-bridge.js +41 -18
  7. package/dist/cli.js +5 -46
  8. package/dist/monitor-ws.d.ts +3 -2
  9. package/dist/monitor-ws.js +5 -49
  10. package/dist/server.js +22 -1
  11. package/monitor/.env.development +1 -0
  12. package/monitor/.env.production +1 -0
  13. package/monitor/ANIMATION_REQUIREMENTS.md +118 -0
  14. package/monitor/index.html +12 -0
  15. package/monitor/package-lock.json +2160 -0
  16. package/monitor/package.json +25 -0
  17. package/monitor/public/bg.png +0 -0
  18. package/monitor/public/bg_clean.png +0 -0
  19. package/monitor/public/positions.json +215 -0
  20. package/monitor/public/sprites/agent_default.png +0 -0
  21. package/monitor/public/sprites/cushion_0.png +0 -0
  22. package/monitor/public/sprites/cushion_1.png +0 -0
  23. package/monitor/public/sprites/cushion_10.png +0 -0
  24. package/monitor/public/sprites/cushion_2.png +0 -0
  25. package/monitor/public/sprites/cushion_3.png +0 -0
  26. package/monitor/public/sprites/cushion_4.png +0 -0
  27. package/monitor/public/sprites/cushion_5.png +0 -0
  28. package/monitor/public/sprites/cushion_6.png +0 -0
  29. package/monitor/public/sprites/cushion_7.png +0 -0
  30. package/monitor/public/sprites/cushion_8.png +0 -0
  31. package/monitor/public/sprites/cushion_9.png +0 -0
  32. package/monitor/public/sprites/master.png +0 -0
  33. package/monitor/public/sprites/stake_0.png +0 -0
  34. package/monitor/public/sprites/stake_1.png +0 -0
  35. package/monitor/public/sprites/stake_10.png +0 -0
  36. package/monitor/public/sprites/stake_2.png +0 -0
  37. package/monitor/public/sprites/stake_3.png +0 -0
  38. package/monitor/public/sprites/stake_4.png +0 -0
  39. package/monitor/public/sprites/stake_5.png +0 -0
  40. package/monitor/public/sprites/stake_6.png +0 -0
  41. package/monitor/public/sprites/stake_7.png +0 -0
  42. package/monitor/public/sprites/stake_8.png +0 -0
  43. package/monitor/public/sprites/stake_9.png +0 -0
  44. package/monitor/scripts/record-gif.py +53 -0
  45. package/monitor/src/App.tsx +22 -0
  46. package/monitor/src/components/AgentMenu.tsx +67 -0
  47. package/monitor/src/components/ChatPanel.tsx +214 -0
  48. package/monitor/src/components/LogPage.tsx +173 -0
  49. package/monitor/src/components/Stage.tsx +39 -0
  50. package/monitor/src/components/StatusBar.tsx +50 -0
  51. package/monitor/src/lib/dojo-app.ts +799 -0
  52. package/monitor/src/lib/interactables.ts +162 -0
  53. package/monitor/src/lib/store.ts +352 -0
  54. package/monitor/src/lib/types.ts +72 -0
  55. package/monitor/src/lib/ws-client.ts +66 -0
  56. package/monitor/src/main.tsx +9 -0
  57. package/monitor/src/vite-env.d.ts +1 -0
  58. package/monitor/tsconfig.json +14 -0
  59. package/monitor/vite.config.ts +13 -0
  60. package/package.json +2 -1
@@ -1,4 +1,5 @@
1
1
  import { type Express } from 'express';
2
+ import { DojoAgentExecutor } from './agent-executor.js';
2
3
  import type { AgentRegistry } from './agent-registry.js';
3
4
  import type { MonitorBus } from './monitor-bus.js';
4
5
  export interface A2AServerOptions {
@@ -9,6 +10,7 @@ export interface A2AServerOptions {
9
10
  }
10
11
  export interface A2AServerHandle {
11
12
  app: Express;
13
+ executors: Map<string, DojoAgentExecutor>;
12
14
  close: () => Promise<void>;
13
15
  }
14
16
  export declare function createA2AServer(opts: A2AServerOptions): A2AServerHandle;
@@ -10,6 +10,7 @@ export function createA2AServer(opts) {
10
10
  const agents = opts.singleAgent
11
11
  ? [opts.registry.get(opts.singleAgent)].filter((a) => a !== undefined)
12
12
  : opts.registry.list().map((id) => opts.registry.get(id)).filter((a) => a !== undefined);
13
+ const executors = new Map();
13
14
  for (const loaded of agents) {
14
15
  const card = {
15
16
  name: loaded.manifest.name,
@@ -28,6 +29,7 @@ export function createA2AServer(opts) {
28
29
  };
29
30
  const taskStore = new InMemoryTaskStore();
30
31
  const executor = new DojoAgentExecutor(loaded, { monitorBus: opts.monitorBus });
32
+ executors.set(loaded.manifest.id, executor);
31
33
  const requestHandler = new DefaultRequestHandler(card, taskStore, executor);
32
34
  const userBuilder = UserBuilder.noAuthentication;
33
35
  app.use(`/a2a/${loaded.manifest.id}`, jsonRpcHandler({ requestHandler, userBuilder }));
@@ -36,6 +38,7 @@ export function createA2AServer(opts) {
36
38
  }
37
39
  return {
38
40
  app,
41
+ executors,
39
42
  close: () => new Promise((resolve) => {
40
43
  // No actual server here; createServer() handles listening
41
44
  resolve();
@@ -9,7 +9,14 @@ export declare class DojoAgentExecutor implements AgentExecutor {
9
9
  private options;
10
10
  private controllers;
11
11
  private contextIds;
12
+ /** Persistent session ID for this agent — enables cross-call memory. */
13
+ private sessionId;
12
14
  constructor(agent: LoadedAgent, options?: AgentExecutorOptions);
13
15
  cancelTask: (taskId: string, eventBus: ExecutionEventBus) => Promise<void>;
14
16
  execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void>;
17
+ /**
18
+ * btw-style one-shot chat: forks the agent's current session, disables all tools,
19
+ * gets a text-only reply, then discards the fork. Does not affect the main session.
20
+ */
21
+ executeBtw(message: string): Promise<string>;
15
22
  }
@@ -29,6 +29,8 @@ export class DojoAgentExecutor {
29
29
  options;
30
30
  controllers = new Map();
31
31
  contextIds = new Map();
32
+ /** Persistent session ID for this agent — enables cross-call memory. */
33
+ sessionId;
32
34
  constructor(agent, options = {}) {
33
35
  this.agent = agent;
34
36
  this.options = options;
@@ -133,11 +135,16 @@ export class DojoAgentExecutor {
133
135
  agent: this.agent,
134
136
  contentBlocks,
135
137
  contextId,
138
+ resume: this.sessionId,
136
139
  onEvent: (m) => {
137
140
  translator.onSdkEvent(m);
138
141
  sdkLogger.onSdkMessage(m);
139
- // Capture final text for summary log
140
142
  const msg = m;
143
+ // Capture session_id for cross-call continuity
144
+ if (msg.session_id && typeof msg.session_id === 'string') {
145
+ this.sessionId = msg.session_id;
146
+ }
147
+ // Capture final text for summary log
141
148
  if (msg.type === 'assistant' && msg.message?.content) {
142
149
  for (const block of msg.message.content) {
143
150
  if (block.type === 'text')
@@ -198,6 +205,33 @@ export class DojoAgentExecutor {
198
205
  });
199
206
  }
200
207
  }
208
+ /**
209
+ * btw-style one-shot chat: forks the agent's current session, disables all tools,
210
+ * gets a text-only reply, then discards the fork. Does not affect the main session.
211
+ */
212
+ async executeBtw(message) {
213
+ // btw is only meaningful when the agent already has a session to fork from.
214
+ // Without a session, there's no context to branch — just run a plain ephemeral query.
215
+ let reply = '';
216
+ for await (const sdkMsg of runClaude({
217
+ agent: this.agent,
218
+ contentBlocks: [{ type: 'text', text: message }],
219
+ contextId: 'btw-' + Date.now(),
220
+ resume: this.sessionId, // undefined if no session yet — SDK creates a fresh one
221
+ btw: this.sessionId !== undefined, // only fork if there's a session to fork from
222
+ onEvent: (m) => {
223
+ const msg = m;
224
+ // Do NOT capture session_id — btw must never affect the main session
225
+ if (msg.type === 'assistant' && msg.message?.content) {
226
+ for (const block of msg.message.content) {
227
+ if (block.type === 'text')
228
+ reply = block.text;
229
+ }
230
+ }
231
+ },
232
+ })) { /* consume */ }
233
+ return reply || '(no response)';
234
+ }
201
235
  }
202
236
  function extractPreview(parts) {
203
237
  if (!Array.isArray(parts))
@@ -7,5 +7,9 @@ export interface RunClaudeParams {
7
7
  contextId: string;
8
8
  onEvent: (event: SDKMessage) => void;
9
9
  abortController?: AbortController;
10
+ /** Resume an existing session (pass the session UUID from a previous query). */
11
+ resume?: string;
12
+ /** btw mode: fork the session, disable all tools, don't persist the fork. */
13
+ btw?: boolean;
10
14
  }
11
15
  export declare function runClaude(params: RunClaudeParams): AsyncGenerator<SDKMessage>;
@@ -1,5 +1,6 @@
1
1
  // src/claude-bridge.ts
2
2
  import { join } from 'path';
3
+ import { existsSync, mkdirSync, symlinkSync } from 'fs';
3
4
  import { query } from '@anthropic-ai/claude-agent-sdk';
4
5
  export async function* runClaude(params) {
5
6
  const { agent, contentBlocks, onEvent, abortController } = params;
@@ -12,23 +13,42 @@ export async function* runClaude(params) {
12
13
  PATH: process.env.PATH,
13
14
  ...(m.env ?? {}),
14
15
  };
15
- // Only override CLAUDE_CONFIG_DIR if the user explicitly specified configDir.
16
- // Otherwise, let the subprocess inherit the parent's config (preserving auth).
17
- if (m.configDir) {
18
- env.CLAUDE_CONFIG_DIR = m.configDir.startsWith('/')
19
- ? m.configDir
20
- : join(agent.agentDir, m.configDir);
16
+ // Each agent stores sessions/config in its own .claude directory by default.
17
+ // This keeps session data co-located with the agent rather than polluting ~/.claude.
18
+ const agentConfigDir = m.configDir
19
+ ? (m.configDir.startsWith('/') ? m.configDir : join(agent.agentDir, m.configDir))
20
+ : join(agent.agentDir, '.claude');
21
+ env.CLAUDE_CONFIG_DIR = agentConfigDir;
22
+ // Ensure the agent's .claude dir exists and symlink global settings (auth, permissions).
23
+ // Symlink keeps all agents in sync with the user's global config automatically.
24
+ mkdirSync(agentConfigDir, { recursive: true });
25
+ const globalSettings = join(process.env.HOME ?? '', '.claude', 'settings.json');
26
+ const localSettings = join(agentConfigDir, 'settings.json');
27
+ if (existsSync(globalSettings) && !existsSync(localSettings)) {
28
+ try {
29
+ symlinkSync(globalSettings, localSettings);
30
+ }
31
+ catch { /* race or permission — ignore */ }
21
32
  }
22
- // Build options. Note: we do NOT pass `resume: contextId` here — the Claude
23
- // SDK treats resume as a previously-issued session ID, and A2A's contextId
24
- // is a fresh UUID with no corresponding SDK session. Treating every send
25
- // as a brand-new Claude session is the safe default. (Future: persist a
26
- // contextId→sessionId mapping if cross-call continuity is required.)
27
33
  const options = {
28
34
  systemPrompt,
29
35
  cwd: agent.agentDir,
30
36
  env,
31
37
  };
38
+ // Session continuity: resume an existing session if provided
39
+ if (params.resume) {
40
+ options.resume = params.resume;
41
+ }
42
+ // btw mode: disable tools, don't persist. Fork only if resuming an existing session.
43
+ if (params.btw) {
44
+ options.tools = [];
45
+ options.allowedTools = [];
46
+ options.disallowedTools = [];
47
+ options.persistSession = false;
48
+ if (params.resume) {
49
+ options.forkSession = true;
50
+ }
51
+ }
32
52
  if (abortController) {
33
53
  options.abortController = abortController;
34
54
  }
@@ -44,12 +64,15 @@ export async function* runClaude(params) {
44
64
  options.pathToClaudeCodeExecutable = m.pathToClaudeCodeExecutable;
45
65
  if (m.extraArgs)
46
66
  options.extraArgs = m.extraArgs;
47
- if (m.tools)
48
- options.tools = m.tools;
49
- if (m.allowedTools)
50
- options.allowedTools = m.allowedTools;
51
- if (m.disallowedTools)
52
- options.disallowedTools = m.disallowedTools;
67
+ // In btw mode, tools are already disabled — don't let manifest override
68
+ if (!params.btw) {
69
+ if (m.tools)
70
+ options.tools = m.tools;
71
+ if (m.allowedTools)
72
+ options.allowedTools = m.allowedTools;
73
+ if (m.disallowedTools)
74
+ options.disallowedTools = m.disallowedTools;
75
+ }
53
76
  if (m.toolAliases)
54
77
  options.toolAliases = m.toolAliases;
55
78
  if (m.permissionMode)
@@ -86,7 +109,7 @@ export async function* runClaude(params) {
86
109
  options.betas = m.betas;
87
110
  if (m.outputFormat)
88
111
  options.outputFormat = m.outputFormat;
89
- if (m.forkSession !== undefined)
112
+ if (m.forkSession !== undefined && !params.btw)
90
113
  options.forkSession = m.forkSession;
91
114
  if (agent.agentsPath)
92
115
  options.agents = agent.agentsPath;
package/dist/cli.js CHANGED
@@ -188,63 +188,22 @@ Options:
188
188
  -h, --help Show this help
189
189
  `);
190
190
  }
191
- // ── chat command ─────────────────────────────────────────
191
+ // ── chat command (btw mode) ───────────────────────────────
192
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}`, {
193
+ const res = await fetch(`http://localhost:${port}/btw/${agentId}`, {
208
194
  method: 'POST',
209
195
  headers: { 'Content-Type': 'application/json' },
210
- body: JSON.stringify(body),
196
+ body: JSON.stringify({ message }),
211
197
  });
212
198
  if (!res.ok) {
213
- console.error(`Error: A2A server returned ${res.status}`);
199
+ console.error(`Error: server returned ${res.status}`);
214
200
  const text = await res.text();
215
201
  if (text)
216
202
  console.error(text);
217
203
  process.exit(1);
218
204
  }
219
205
  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
- }
206
+ console.log(json.reply ?? '(no response)');
248
207
  }
249
208
  // ── monitor GUI launcher ─────────────────────────────────
250
209
  /** Resolve the monitor/ directory shipped alongside the package. */
@@ -1,14 +1,15 @@
1
1
  import { WebSocketServer, WebSocket } from 'ws';
2
2
  import type { Server } from 'http';
3
3
  import type { AgentRegistry } from './agent-registry.js';
4
+ import type { DojoAgentExecutor } from './agent-executor.js';
4
5
  import type { MonitorBus } from './monitor-bus.js';
5
6
  export interface MonitorWsOptions {
6
7
  server: Server;
7
8
  bus: MonitorBus;
8
9
  path: string;
9
10
  registry: AgentRegistry;
10
- /** A2A server port — needed to proxy chat commands. */
11
- a2aPort: number;
11
+ /** Agent executors — needed for btw-style chat. */
12
+ executors: Map<string, DojoAgentExecutor>;
12
13
  }
13
14
  export type MonitorCommand = {
14
15
  type: 'reload';
@@ -1,6 +1,5 @@
1
1
  // src/monitor-ws.ts
2
2
  import { WebSocketServer, WebSocket } from 'ws';
3
- import { v4 as uuidv4 } from 'uuid';
4
3
  export function createMonitorWs(opts) {
5
4
  const clients = new Set();
6
5
  const wss = new WebSocketServer({ server: opts.server, path: opts.path });
@@ -37,56 +36,13 @@ export function createMonitorWs(opts) {
37
36
  });
38
37
  }
39
38
  }
40
- /** Proxy a chat message to the A2A endpoint and stream the reply back. */
39
+ /** btw-style chat: fork agent session, get text-only reply, discard fork. */
41
40
  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
- }
41
+ const executor = opts.executors.get(cmd.agentId);
42
+ if (!executor) {
43
+ throw new Error(`Agent "${cmd.agentId}" not found`);
86
44
  }
87
- if (!replyText)
88
- replyText = '(no response)';
89
- // Send the response only to the requesting client
45
+ const replyText = await executor.executeBtw(cmd.message);
90
46
  const event = {
91
47
  type: 'chat_response',
92
48
  chatId: cmd.chatId,
package/dist/server.js CHANGED
@@ -38,6 +38,27 @@ export async function createServer(opts) {
38
38
  app.get('/logs/dates', (_req, res) => {
39
39
  res.json({ dates: listLogDates(logDir) });
40
40
  });
41
+ // btw-style one-shot chat endpoint (forks session, no tools, ephemeral)
42
+ app.post('/btw/:agentId', async (req, res) => {
43
+ const { agentId } = req.params;
44
+ const { message } = req.body;
45
+ if (!message) {
46
+ res.status(400).json({ error: 'message required' });
47
+ return;
48
+ }
49
+ const executor = a2a.executors.get(agentId);
50
+ if (!executor) {
51
+ res.status(404).json({ error: `agent "${agentId}" not found` });
52
+ return;
53
+ }
54
+ try {
55
+ const reply = await executor.executeBtw(message);
56
+ res.json({ reply });
57
+ }
58
+ catch (err) {
59
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
60
+ }
61
+ });
41
62
  const httpServer = createHttpServer(app);
42
63
  // Listen on main server first so we know the actual port (important when port=0)
43
64
  await new Promise((r) => httpServer.listen(opts.port ?? 41241, r));
@@ -47,7 +68,7 @@ export async function createServer(opts) {
47
68
  if (opts.monitorPort !== undefined) {
48
69
  // Bind monitor on a separate server (port 0 = OS picks a free port)
49
70
  monitorHttp = createHttpServer();
50
- createMonitorWs({ server: monitorHttp, bus, path: '/monitor', registry, a2aPort: actualA2APort });
71
+ createMonitorWs({ server: monitorHttp, bus, path: '/monitor', registry, executors: a2a.executors });
51
72
  await new Promise((r) => monitorHttp.listen(opts.monitorPort, r));
52
73
  actualMonitorPort = monitorHttp.address().port;
53
74
  }
@@ -0,0 +1 @@
1
+ VITE_MONITOR_WS_URL=ws://localhost:41242/monitor
@@ -0,0 +1 @@
1
+ VITE_MONITOR_WS_URL=ws://localhost:41242/monitor
@@ -0,0 +1,118 @@
1
+ # Monitor GUI 动画需求清单
2
+
3
+ 以下是从历史对话中提取的所有前端动画相关需求,按时间顺序排列。
4
+
5
+ ---
6
+
7
+ ## 1. 场景背景
8
+
9
+ - **R1.1** 使用指定的像素风道场背景图 (`bg_clean.png`)
10
+ - **R1.2** 页面缩放时,背景与角色位置保持同步,不能错位
11
+ - **R1.3** 固定设计分辨率 960×600,所有元素在 Pixi.js 内渲染
12
+
13
+ ## 2. 角色基本形象
14
+
15
+ - **R2.1** 角色使用像素风 sprite,风格与道场背景匹配
16
+ - **R2.2** 角色大小与木桩差不多高(约 60-64px)
17
+ - **R2.3** 角色头顶显示名字标签,名字要清晰可辨
18
+ - **R2.4** 角色活动范围限定在地板区域(不能跑到墙壁上)
19
+
20
+ ## 3. 角色动作集
21
+
22
+ - **R3.1** idle(站立发呆):站立,眨眼动画
23
+ - **R3.2** walk(走路):四帧走路动画,移动时自动切换
24
+ - **R3.3** sit(坐在蒲团上):蒲团上的打坐动画
25
+ - **R3.4** attack(打木桩):击打木桩的攻击动画
26
+ - **R3.5** daydream(站着发呆):和 idle 不同的发呆动画
27
+ - **R3.6** chat(聊天):两个 agent 面对面聊天的动画
28
+ - **R3.7** ~~坐地发呆~~ 已移除(效果不佳)
29
+ - **R3.8** ~~躺下~~ 已移除(像素分辨率限制,效果不佳)
30
+
31
+ ## 4. 闲置行为(Idle Behavior)
32
+
33
+ - **R4.1** 无任务时,角色在地图上随机游荡(wander)
34
+ - **R4.2** 随机切换站立发呆(pause)和 daydream
35
+ - **R4.3** 移动时自动切换朝向(面向移动方向)
36
+
37
+ ## 5. 角色与物品交互
38
+
39
+ ### 5.1 蒲团(Cushion)
40
+
41
+ - **R5.1** 每个 agent 有固定的 home 蒲团位置
42
+ - **R5.2** agent 只能坐在自己的蒲团上,不能随地坐
43
+ - **R5.3** agent 坐到蒲团上时,蒲团有被压扁的动画
44
+ - **R5.4** agent 与蒲团的位置要精准对齐,不能浮空
45
+
46
+ ### 5.2 木桩(Stake)
47
+
48
+ - **R5.5** 每个 agent 有固定的木桩位置
49
+ - **R5.6** agent 击打木桩时,木桩有受击晃动动画
50
+ - **R5.7** 木桩晃动必须在 agent 出拳后才触发(不是提前晃)
51
+ - **R5.8** agent 应当和木桩保持平行,站在木桩侧面击打
52
+
53
+ ### 5.3 碰撞与移动
54
+
55
+ - **R5.9** agent 与蒲团、木桩之间有碰撞体积,不能穿过去
56
+ - **R5.10** 碰撞不能卡住 agent 移动(绕行而不是卡死)
57
+ - **R5.11** 物品交互框架化设计,便于后续添加新物品
58
+ - A: agent 设计避让
59
+ - A: 在下面的任务、物品可以挡住上面的,符合人类的观感
60
+
61
+ ## 6. 任务驱动动画
62
+
63
+ - **R6.1** 收到任务(receiving)→ agent 走向 Master 老师傅
64
+ - **R6.2** 到达 Master 后 → 走向自己的木桩
65
+ - **R6.3** 执行任务(working/tool_call)→ 在木桩前做 attack 动画
66
+ - **R6.4** 任务完成(completed/failed)→ 走回蒲团 home 位置
67
+
68
+ ## 7. Agent 间交互(Peer Chat)
69
+
70
+ - **R7.1** agent 之间交流时,两人走到空地面对面
71
+ - **R7.2** 面对面聊天时显示 chat 动画
72
+ - **R7.3** 支持多于两人一起沟通(不限两人)
73
+ - **R7.4** 聊天时头顶显示聊天气泡,显示消息内容
74
+
75
+ ## 8. Master 老师傅
76
+
77
+ - **R8.1** 新增一个 Master 角色,固定在场景中央
78
+ - **R8.2** Master 只有 idle 动画(不移动)
79
+ - **R8.3** 当 client 与 agent 交互时,Master 呼唤 agent 过来
80
+
81
+ ## 9. 其他 UI
82
+
83
+ - **R9.1** StatusBar 显示连接状态、agent 数量
84
+ - **R9.2** Logs 按钮进入聊天记录页面
85
+
86
+ ## 10. 状态机
87
+
88
+ 人物状态机以如下要求为准。
89
+
90
+ ### 10.1 Client → Agent(外部任务)
91
+
92
+ | 状态 | 动画表现 |
93
+ |------|----------|
94
+ | `idle` | 坐在蒲团上发呆(闲置行为:偶尔起身随机游荡、daydream) |
95
+ | `submitted` | 从蒲团起身,走向 Master 老师傅 |
96
+ | `working` | 从 Master 处走向自己的木桩,开始打桩(attack 动画) |
97
+ | `input-required` | 停下打桩,走回 Master 前等待;收到输入后再回木桩继续 |
98
+ | `auth-required` | 同 `input-required`,走回 Master 前等待 |
99
+ | `completed` | 停下打桩,走回蒲团坐下 |
100
+ | `failed` | 打桩手痛 → 甩手动画 → 走回蒲团坐下 |
101
+ | `canceled` | 被 Master 叫回 → 立即停下当前动作,走回蒲团坐下 |
102
+ | `rejected` | 在 Master 前摇头拒绝 → 气泡显示拒绝原因 → 走回蒲团坐下 |
103
+
104
+ ### 10.2 Agent → Agent(内部调用)
105
+
106
+ 当 Agent A(处于 working/打桩中)调用 Agent B 时:
107
+
108
+ | 角色 | 行为 |
109
+ |------|------|
110
+ | Agent A(发起方) | 停下打桩 → 走向空地等 B → 面对面 chat → B 回复后回木桩继续打桩 |
111
+ | Agent B(被调用方) | 从蒲团起身 → 走向 A → 面对面 chat → 回复完毕后走回蒲团 |
112
+
113
+ 关键规则:
114
+ - **判断依据**:看任务的 `from` 是 client 还是 agent,决定 working 状态的动画是打桩还是聊天
115
+ - A 和 B 在对话过程中都处于 `working` 状态,但动画表现为聊天(chat),不是打桩
116
+ - B 回复完成后(B 的任务 completed),B 走回蒲团
117
+ - A 拿到回复后,A 回木桩继续打桩(A 的任务仍在 working)
118
+ - 支持多人同时聊天(参考第 7 节)
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AgentsDojo Monitor</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>