agents-dojo 0.1.6 → 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.
- package/monitor/.env.development +1 -0
- package/monitor/.env.production +1 -0
- package/monitor/ANIMATION_REQUIREMENTS.md +118 -0
- package/monitor/index.html +12 -0
- package/monitor/package-lock.json +2160 -0
- package/monitor/package.json +25 -0
- package/monitor/public/bg.png +0 -0
- package/monitor/public/bg_clean.png +0 -0
- package/monitor/public/positions.json +215 -0
- package/monitor/public/sprites/agent_default.png +0 -0
- package/monitor/public/sprites/cushion_0.png +0 -0
- package/monitor/public/sprites/cushion_1.png +0 -0
- package/monitor/public/sprites/cushion_10.png +0 -0
- package/monitor/public/sprites/cushion_2.png +0 -0
- package/monitor/public/sprites/cushion_3.png +0 -0
- package/monitor/public/sprites/cushion_4.png +0 -0
- package/monitor/public/sprites/cushion_5.png +0 -0
- package/monitor/public/sprites/cushion_6.png +0 -0
- package/monitor/public/sprites/cushion_7.png +0 -0
- package/monitor/public/sprites/cushion_8.png +0 -0
- package/monitor/public/sprites/cushion_9.png +0 -0
- package/monitor/public/sprites/master.png +0 -0
- package/monitor/public/sprites/stake_0.png +0 -0
- package/monitor/public/sprites/stake_1.png +0 -0
- package/monitor/public/sprites/stake_10.png +0 -0
- package/monitor/public/sprites/stake_2.png +0 -0
- package/monitor/public/sprites/stake_3.png +0 -0
- package/monitor/public/sprites/stake_4.png +0 -0
- package/monitor/public/sprites/stake_5.png +0 -0
- package/monitor/public/sprites/stake_6.png +0 -0
- package/monitor/public/sprites/stake_7.png +0 -0
- package/monitor/public/sprites/stake_8.png +0 -0
- package/monitor/public/sprites/stake_9.png +0 -0
- package/monitor/scripts/record-gif.py +53 -0
- package/monitor/src/App.tsx +22 -0
- package/monitor/src/components/AgentMenu.tsx +67 -0
- package/monitor/src/components/ChatPanel.tsx +214 -0
- package/monitor/src/components/LogPage.tsx +173 -0
- package/monitor/src/components/Stage.tsx +39 -0
- package/monitor/src/components/StatusBar.tsx +50 -0
- package/monitor/src/lib/dojo-app.ts +799 -0
- package/monitor/src/lib/interactables.ts +162 -0
- package/monitor/src/lib/store.ts +352 -0
- package/monitor/src/lib/types.ts +72 -0
- package/monitor/src/lib/ws-client.ts +66 -0
- package/monitor/src/main.tsx +9 -0
- package/monitor/src/vite-env.d.ts +1 -0
- package/monitor/tsconfig.json +14 -0
- package/monitor/vite.config.ts +13 -0
- package/package.json +2 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactable framework — unified system for objects that agents interact with.
|
|
3
|
+
*
|
|
4
|
+
* Each interactable defines:
|
|
5
|
+
* - Its position and collision shape
|
|
6
|
+
* - Where the agent should stand/sit to interact
|
|
7
|
+
* - What animation the agent plays during interaction
|
|
8
|
+
* - What animation the object plays when interacted with
|
|
9
|
+
*
|
|
10
|
+
* To add a new interactable type:
|
|
11
|
+
* 1. Add an entry to INTERACTABLE_DEFS below
|
|
12
|
+
* 2. Create a sprite component using the InteractableSprite helper
|
|
13
|
+
* 3. Register it in Stage.tsx
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import positionsData from '../../public/positions.json';
|
|
17
|
+
|
|
18
|
+
// ── Types ──────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export type InteractableType = 'stake' | 'cushion';
|
|
21
|
+
|
|
22
|
+
export interface InteractableDef {
|
|
23
|
+
type: InteractableType;
|
|
24
|
+
|
|
25
|
+
/** Anchor position in design coords (where the object sits) */
|
|
26
|
+
anchorX: number;
|
|
27
|
+
anchorY: number;
|
|
28
|
+
|
|
29
|
+
/** Where the agent should move to interact (feet position) */
|
|
30
|
+
agentX: number;
|
|
31
|
+
agentY: number;
|
|
32
|
+
|
|
33
|
+
/** Which direction the agent faces during interaction (true = face left) */
|
|
34
|
+
agentFaceLeft: boolean;
|
|
35
|
+
|
|
36
|
+
/** Agent animation during interaction */
|
|
37
|
+
agentAnim: 'sit' | 'attack' | 'idle';
|
|
38
|
+
|
|
39
|
+
/** Collision radius for wander avoidance */
|
|
40
|
+
collisionRadius: number;
|
|
41
|
+
|
|
42
|
+
/** Sprite file path */
|
|
43
|
+
spriteFile: string;
|
|
44
|
+
|
|
45
|
+
/** Original pixel dimensions (for scaling to design space) */
|
|
46
|
+
origW: number;
|
|
47
|
+
origH: number;
|
|
48
|
+
|
|
49
|
+
/** Rendering: anchor point of the sprite */
|
|
50
|
+
spriteAnchorX: number;
|
|
51
|
+
spriteAnchorY: number;
|
|
52
|
+
|
|
53
|
+
/** Rendering: y position for the sprite */
|
|
54
|
+
spriteY: number;
|
|
55
|
+
|
|
56
|
+
/** Index for matching to agent assignments */
|
|
57
|
+
index: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Constants ──────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
const DS = 960 / 2400; // design scale factor
|
|
63
|
+
|
|
64
|
+
// Stake: 104px wide in original → 41.6px design → half = ~21px
|
|
65
|
+
const STAKE_HALF_W = 21;
|
|
66
|
+
const STAKE_AGENT_GAP = 4; // gap between agent and stake edge
|
|
67
|
+
const STAKE_AGENT_OFFSET_X = STAKE_HALF_W + STAKE_AGENT_GAP; // 25px
|
|
68
|
+
|
|
69
|
+
// Cushion: agent sits slightly above cushion center so body overlaps the cushion
|
|
70
|
+
const CUSHION_SIT_OFFSET_Y = 2;
|
|
71
|
+
|
|
72
|
+
// ── Build interactable list from positions data ────────────
|
|
73
|
+
|
|
74
|
+
export const INTERACTABLES: InteractableDef[] = [];
|
|
75
|
+
|
|
76
|
+
// Stakes
|
|
77
|
+
for (let i = 0; i < positionsData.stakes.length; i++) {
|
|
78
|
+
const s = positionsData.stakes[i];
|
|
79
|
+
INTERACTABLES.push({
|
|
80
|
+
type: 'stake',
|
|
81
|
+
anchorX: s.x,
|
|
82
|
+
anchorY: (s.y_top + s.y_bot) / 2,
|
|
83
|
+
// Agent stands beside stake, feet at stake bottom y
|
|
84
|
+
agentX: s.x + STAKE_AGENT_OFFSET_X,
|
|
85
|
+
agentY: s.y_bot,
|
|
86
|
+
agentFaceLeft: true,
|
|
87
|
+
agentAnim: 'attack',
|
|
88
|
+
collisionRadius: 20,
|
|
89
|
+
spriteFile: `/sprites/stake_${i}.png`,
|
|
90
|
+
origW: s.w,
|
|
91
|
+
origH: s.h,
|
|
92
|
+
spriteAnchorX: 0.5,
|
|
93
|
+
spriteAnchorY: 0,
|
|
94
|
+
spriteY: s.y_top,
|
|
95
|
+
index: i,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Cushions (skip the 11th lone cushion if > 10 agents)
|
|
100
|
+
const cushionCount = Math.min(positionsData.cushions.length, 10);
|
|
101
|
+
for (let i = 0; i < cushionCount; i++) {
|
|
102
|
+
const c = positionsData.cushions[i];
|
|
103
|
+
INTERACTABLES.push({
|
|
104
|
+
type: 'cushion',
|
|
105
|
+
anchorX: c.x,
|
|
106
|
+
anchorY: c.y,
|
|
107
|
+
// Agent sits at cushion center x, shifted down for overlap
|
|
108
|
+
agentX: c.x,
|
|
109
|
+
agentY: c.y + CUSHION_SIT_OFFSET_Y,
|
|
110
|
+
agentFaceLeft: false,
|
|
111
|
+
agentAnim: 'sit',
|
|
112
|
+
collisionRadius: 18,
|
|
113
|
+
spriteFile: `/sprites/cushion_${i}.png`,
|
|
114
|
+
origW: c.w,
|
|
115
|
+
origH: c.h,
|
|
116
|
+
spriteAnchorX: 0.5,
|
|
117
|
+
spriteAnchorY: 0.5,
|
|
118
|
+
spriteY: c.y,
|
|
119
|
+
index: i,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Helpers for agent assignment ───────────────────────────
|
|
124
|
+
|
|
125
|
+
/** Get all interactables of a given type, sorted by index */
|
|
126
|
+
export function getByType(type: InteractableType): InteractableDef[] {
|
|
127
|
+
return INTERACTABLES.filter((i) => i.type === type).sort((a, b) => a.index - b.index);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Get obstacles for wander collision avoidance */
|
|
131
|
+
export function getObstacles(): Array<{ x: number; y: number; r: number }> {
|
|
132
|
+
return INTERACTABLES.map((i) => ({ x: i.anchorX, y: i.anchorY, r: i.collisionRadius }));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Pair cushions and stakes 1:1 by proximity (returns paired arrays) */
|
|
136
|
+
export function getPairedPositions(): Array<{
|
|
137
|
+
cushion: InteractableDef;
|
|
138
|
+
stake: InteractableDef;
|
|
139
|
+
}> {
|
|
140
|
+
const cushions = getByType('cushion');
|
|
141
|
+
const stakes = getByType('stake');
|
|
142
|
+
const usedStakes = new Set<number>();
|
|
143
|
+
const pairs: Array<{ cushion: InteractableDef; stake: InteractableDef }> = [];
|
|
144
|
+
|
|
145
|
+
for (const c of cushions) {
|
|
146
|
+
let bestIdx = -1;
|
|
147
|
+
let bestDist = Infinity;
|
|
148
|
+
for (let j = 0; j < stakes.length; j++) {
|
|
149
|
+
if (usedStakes.has(j)) continue;
|
|
150
|
+
const dist = Math.abs(stakes[j].anchorX - c.anchorX);
|
|
151
|
+
if (dist < bestDist) {
|
|
152
|
+
bestDist = dist;
|
|
153
|
+
bestIdx = j;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (bestIdx >= 0) {
|
|
157
|
+
usedStakes.add(bestIdx);
|
|
158
|
+
pairs.push({ cushion: c, stake: stakes[bestIdx] });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return pairs;
|
|
162
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { Agent, AgentState, AgentAnim, ChatSession, MessageLink, MonitorEvent, UserChat, ChatMessage } from './types.js';
|
|
3
|
+
import { getPairedPositions } from './interactables.js';
|
|
4
|
+
|
|
5
|
+
const PAIRED = getPairedPositions();
|
|
6
|
+
|
|
7
|
+
// ── localStorage persistence (position + state) ────────────
|
|
8
|
+
|
|
9
|
+
const STORAGE_KEY = 'dojo-monitor-agents';
|
|
10
|
+
|
|
11
|
+
interface SavedAgent {
|
|
12
|
+
position: { x: number; y: number };
|
|
13
|
+
state: AgentState;
|
|
14
|
+
taskFrom?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadSavedAgents(): Map<string, SavedAgent> {
|
|
18
|
+
try {
|
|
19
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
20
|
+
if (!raw) return new Map();
|
|
21
|
+
const obj = JSON.parse(raw) as Record<string, SavedAgent>;
|
|
22
|
+
return new Map(Object.entries(obj));
|
|
23
|
+
} catch { return new Map(); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
27
|
+
function scheduleSave(agents: Map<string, Agent>) {
|
|
28
|
+
if (saveTimer) return; // already scheduled
|
|
29
|
+
saveTimer = setTimeout(() => {
|
|
30
|
+
saveTimer = null;
|
|
31
|
+
try {
|
|
32
|
+
const obj: Record<string, SavedAgent> = {};
|
|
33
|
+
for (const [id, a] of agents) {
|
|
34
|
+
obj[id] = { position: a.position, state: a.state, taskFrom: a.taskFrom };
|
|
35
|
+
}
|
|
36
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
|
|
37
|
+
} catch { /* ignore quota errors */ }
|
|
38
|
+
}, 500);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface MonitorStore {
|
|
42
|
+
agents: Map<string, Agent>;
|
|
43
|
+
links: Map<string, MessageLink>;
|
|
44
|
+
chatSessions: Map<string, ChatSession>;
|
|
45
|
+
taskToAgent: Map<string, string>;
|
|
46
|
+
connected: boolean;
|
|
47
|
+
agentIndex: number;
|
|
48
|
+
|
|
49
|
+
// User chat (monitor GUI → agent)
|
|
50
|
+
userChat: UserChat | null;
|
|
51
|
+
// Master bubble message (client's message shown on master sprite)
|
|
52
|
+
masterMessage: string | null;
|
|
53
|
+
// Context menu
|
|
54
|
+
menuAgentId: string | null;
|
|
55
|
+
menuPosition: { x: number; y: number } | null;
|
|
56
|
+
|
|
57
|
+
applyEvent: (event: MonitorEvent) => void;
|
|
58
|
+
setConnected: (b: boolean) => void;
|
|
59
|
+
setPosition: (id: string, x: number, y: number) => void;
|
|
60
|
+
setAnim: (id: string, anim: AgentAnim) => void;
|
|
61
|
+
|
|
62
|
+
// Chat session API
|
|
63
|
+
createChatSession: (session: ChatSession) => void;
|
|
64
|
+
endChatSession: (sessionId: string) => void;
|
|
65
|
+
getAgentSession: (agentId: string) => ChatSession | undefined;
|
|
66
|
+
|
|
67
|
+
// User chat actions
|
|
68
|
+
openChat: (agentId: string) => void;
|
|
69
|
+
closeChat: () => void;
|
|
70
|
+
addChatMessage: (msg: ChatMessage) => void;
|
|
71
|
+
setChatLoading: (loading: boolean) => void;
|
|
72
|
+
|
|
73
|
+
// Context menu actions
|
|
74
|
+
openMenu: (agentId: string, x: number, y: number) => void;
|
|
75
|
+
closeMenu: () => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compute where each member stands around the meetPoint.
|
|
80
|
+
* Members form a circle/line around the center point.
|
|
81
|
+
*/
|
|
82
|
+
export function getChatPosition(meetPoint: { x: number; y: number }, memberIndex: number, totalMembers: number): { x: number; y: number } {
|
|
83
|
+
const SPACING = 30;
|
|
84
|
+
if (totalMembers <= 2) {
|
|
85
|
+
// Two people: side by side
|
|
86
|
+
const offset = memberIndex === 0 ? -SPACING / 2 : SPACING / 2;
|
|
87
|
+
return { x: meetPoint.x + offset, y: meetPoint.y };
|
|
88
|
+
}
|
|
89
|
+
// N people: arrange in a circle
|
|
90
|
+
const angle = (2 * Math.PI * memberIndex) / totalMembers - Math.PI / 2;
|
|
91
|
+
const radius = SPACING;
|
|
92
|
+
return {
|
|
93
|
+
x: meetPoint.x + Math.cos(angle) * radius,
|
|
94
|
+
y: meetPoint.y + Math.sin(angle) * radius * 0.6, // squash y for pseudo-isometric
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const useMonitorStore = create<MonitorStore>((set, get) => {
|
|
99
|
+
if (typeof window !== 'undefined') {
|
|
100
|
+
(window as any).__DOJO_STORE__ = { getState: () => get() };
|
|
101
|
+
}
|
|
102
|
+
return ({
|
|
103
|
+
agents: new Map(),
|
|
104
|
+
links: new Map(),
|
|
105
|
+
chatSessions: new Map(),
|
|
106
|
+
taskToAgent: new Map(),
|
|
107
|
+
connected: false,
|
|
108
|
+
agentIndex: 0,
|
|
109
|
+
userChat: null,
|
|
110
|
+
masterMessage: null,
|
|
111
|
+
menuAgentId: null,
|
|
112
|
+
menuPosition: null,
|
|
113
|
+
|
|
114
|
+
setConnected: (b) => set({ connected: b }),
|
|
115
|
+
|
|
116
|
+
setPosition: (id, x, y) => {
|
|
117
|
+
const a = get().agents.get(id);
|
|
118
|
+
// Skip if position hasn't actually changed (avoids unnecessary re-renders)
|
|
119
|
+
if (!a || (Math.abs(a.position.x - x) < 0.5 && Math.abs(a.position.y - y) < 0.5)) return;
|
|
120
|
+
const agents = new Map(get().agents);
|
|
121
|
+
agents.set(id, { ...a, position: { x, y } });
|
|
122
|
+
set({ agents });
|
|
123
|
+
scheduleSave(agents);
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
setAnim: (id, anim) => {
|
|
127
|
+
const agents = new Map(get().agents);
|
|
128
|
+
const a = agents.get(id);
|
|
129
|
+
if (a && a.currentAnim !== anim) {
|
|
130
|
+
agents.set(id, { ...a, currentAnim: anim });
|
|
131
|
+
set({ agents });
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
createChatSession: (session) => {
|
|
136
|
+
const sessions = new Map(get().chatSessions);
|
|
137
|
+
const agents = new Map(get().agents);
|
|
138
|
+
sessions.set(session.id, session);
|
|
139
|
+
// Tag each member with the session ID
|
|
140
|
+
for (const mid of session.memberIds) {
|
|
141
|
+
const a = agents.get(mid);
|
|
142
|
+
if (a) agents.set(mid, { ...a, chatSessionId: session.id });
|
|
143
|
+
}
|
|
144
|
+
set({ chatSessions: sessions, agents });
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
endChatSession: (sessionId) => {
|
|
148
|
+
const sessions = new Map(get().chatSessions);
|
|
149
|
+
const agents = new Map(get().agents);
|
|
150
|
+
const session = sessions.get(sessionId);
|
|
151
|
+
if (session) {
|
|
152
|
+
for (const mid of session.memberIds) {
|
|
153
|
+
const a = agents.get(mid);
|
|
154
|
+
if (a) agents.set(mid, { ...a, chatSessionId: undefined });
|
|
155
|
+
}
|
|
156
|
+
sessions.delete(sessionId);
|
|
157
|
+
}
|
|
158
|
+
set({ chatSessions: sessions, agents });
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
getAgentSession: (agentId) => {
|
|
162
|
+
const a = get().agents.get(agentId);
|
|
163
|
+
if (!a?.chatSessionId) return undefined;
|
|
164
|
+
return get().chatSessions.get(a.chatSessionId);
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// User chat actions
|
|
168
|
+
openChat: (agentId) => set({ userChat: { agentId, messages: [], loading: false }, menuAgentId: null, menuPosition: null }),
|
|
169
|
+
closeChat: () => set({ userChat: null }),
|
|
170
|
+
addChatMessage: (msg) => {
|
|
171
|
+
const chat = get().userChat;
|
|
172
|
+
if (!chat) return;
|
|
173
|
+
set({ userChat: { ...chat, messages: [...chat.messages, msg] } });
|
|
174
|
+
},
|
|
175
|
+
setChatLoading: (loading) => {
|
|
176
|
+
const chat = get().userChat;
|
|
177
|
+
if (!chat) return;
|
|
178
|
+
set({ userChat: { ...chat, loading } });
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// Context menu actions
|
|
182
|
+
openMenu: (agentId, x, y) => set({ menuAgentId: agentId, menuPosition: { x, y } }),
|
|
183
|
+
closeMenu: () => set({ menuAgentId: null, menuPosition: null }),
|
|
184
|
+
|
|
185
|
+
applyEvent: (event) => {
|
|
186
|
+
const agents = new Map(get().agents);
|
|
187
|
+
const links = new Map(get().links);
|
|
188
|
+
const taskToAgent = new Map(get().taskToAgent);
|
|
189
|
+
|
|
190
|
+
switch (event.type) {
|
|
191
|
+
case 'agent_loaded': {
|
|
192
|
+
if (!agents.has(event.agentId)) {
|
|
193
|
+
const idx = get().agentIndex;
|
|
194
|
+
const pair = PAIRED[idx % PAIRED.length];
|
|
195
|
+
const home = { x: pair.cushion.agentX, y: pair.cushion.agentY };
|
|
196
|
+
const saved = loadSavedAgents().get(event.agentId);
|
|
197
|
+
agents.set(event.agentId, {
|
|
198
|
+
id: event.agentId,
|
|
199
|
+
position: saved?.position ?? { ...home },
|
|
200
|
+
homePosition: { ...home },
|
|
201
|
+
stakePosition: { x: pair.stake.anchorX, y: pair.stake.agentY },
|
|
202
|
+
sprite: 'default',
|
|
203
|
+
state: saved?.state ?? 'idle',
|
|
204
|
+
currentAnim: 'idle',
|
|
205
|
+
taskFrom: saved?.taskFrom,
|
|
206
|
+
});
|
|
207
|
+
set({ agentIndex: idx + 1 });
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case 'task_created': {
|
|
212
|
+
taskToAgent.set(event.taskId, event.to);
|
|
213
|
+
|
|
214
|
+
// Detect agent-to-agent call: if another agent is currently working/tool_call,
|
|
215
|
+
// it likely called this agent via the call-agent skill.
|
|
216
|
+
let callerAgentId: string | undefined;
|
|
217
|
+
for (const [id, a] of agents) {
|
|
218
|
+
if (id !== event.to && (a.state === 'working' || a.state === 'tool_call')) {
|
|
219
|
+
callerAgentId = id;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const fromId = callerAgentId ?? event.from;
|
|
225
|
+
const link: MessageLink = {
|
|
226
|
+
id: event.taskId,
|
|
227
|
+
from: fromId,
|
|
228
|
+
to: event.to,
|
|
229
|
+
preview: event.preview, startTime: Date.now(), taskId: event.taskId,
|
|
230
|
+
};
|
|
231
|
+
links.set(link.id, link);
|
|
232
|
+
const to = agents.get(event.to);
|
|
233
|
+
// Client's message is shown on master, not on the agent
|
|
234
|
+
if (!callerAgentId) {
|
|
235
|
+
set({ masterMessage: event.preview });
|
|
236
|
+
}
|
|
237
|
+
if (to) agents.set(to.id, { ...to, state: 'submitted', taskFrom: fromId });
|
|
238
|
+
if (callerAgentId) {
|
|
239
|
+
// Agent-to-agent call — create a chat session
|
|
240
|
+
const sessionId = `peer-${event.taskId}`;
|
|
241
|
+
const meetPoint = {
|
|
242
|
+
x: ((agents.get(callerAgentId)?.position.x ?? 480) + (to?.position.x ?? 480)) / 2,
|
|
243
|
+
y: Math.max(agents.get(callerAgentId)?.position.y ?? 400, to?.position.y ?? 400),
|
|
244
|
+
};
|
|
245
|
+
const chatSessions = new Map(get().chatSessions);
|
|
246
|
+
chatSessions.set(sessionId, {
|
|
247
|
+
id: sessionId,
|
|
248
|
+
initiatorId: callerAgentId,
|
|
249
|
+
memberIds: [callerAgentId, event.to],
|
|
250
|
+
meetPoint,
|
|
251
|
+
duration: 30000,
|
|
252
|
+
startTime: Date.now(),
|
|
253
|
+
});
|
|
254
|
+
agents.set(callerAgentId, { ...agents.get(callerAgentId)!, chatSessionId: sessionId });
|
|
255
|
+
if (to) agents.set(to.id, { ...to, state: 'submitted', taskFrom: fromId, chatSessionId: sessionId });
|
|
256
|
+
set({ chatSessions });
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case 'task_status': {
|
|
261
|
+
const targetId = taskToAgent.get(event.taskId);
|
|
262
|
+
if (targetId) {
|
|
263
|
+
const agent = agents.get(targetId);
|
|
264
|
+
if (agent) {
|
|
265
|
+
let newState: AgentState;
|
|
266
|
+
switch (event.state) {
|
|
267
|
+
case 'submitted': newState = 'submitted'; break;
|
|
268
|
+
case 'working': newState = 'working'; break;
|
|
269
|
+
case 'completed': newState = 'completed'; break;
|
|
270
|
+
case 'failed': newState = 'failed'; break;
|
|
271
|
+
case 'canceled': newState = 'canceled'; break;
|
|
272
|
+
case 'rejected': newState = 'rejected'; break;
|
|
273
|
+
case 'input-required': newState = 'input-required'; break;
|
|
274
|
+
case 'auth-required': newState = 'auth-required'; break;
|
|
275
|
+
default: newState = 'idle';
|
|
276
|
+
}
|
|
277
|
+
const msg = event.message ?? agent.lastMessage;
|
|
278
|
+
agents.set(targetId, { ...agent, state: newState, lastMessage: msg });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const terminalStates = ['completed', 'failed', 'canceled', 'rejected'];
|
|
282
|
+
if (terminalStates.includes(event.state)) {
|
|
283
|
+
// Clear master bubble on task end
|
|
284
|
+
set({ masterMessage: null });
|
|
285
|
+
|
|
286
|
+
// End any peer chat session associated with this task
|
|
287
|
+
const peerSessionId = `peer-${event.taskId}`;
|
|
288
|
+
if (get().chatSessions.has(peerSessionId)) {
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
useMonitorStore.getState().endChatSession(peerSessionId);
|
|
291
|
+
}, 1500);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const [id, link] of links) {
|
|
295
|
+
if (link.taskId === event.taskId) {
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
const s = useMonitorStore.getState();
|
|
298
|
+
const newLinks = new Map(s.links);
|
|
299
|
+
newLinks.delete(id);
|
|
300
|
+
const newAgents = new Map(s.agents);
|
|
301
|
+
if (targetId) {
|
|
302
|
+
const a = newAgents.get(targetId);
|
|
303
|
+
if (a && (a.state === 'completed' || a.state === 'failed' || a.state === 'canceled' || a.state === 'rejected')) {
|
|
304
|
+
newAgents.set(targetId, { ...a, state: 'idle', lastToolName: undefined, chatSessionId: undefined, lastMessage: undefined, taskFrom: undefined });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
useMonitorStore.setState({ links: newLinks, agents: newAgents });
|
|
308
|
+
}, 3000);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
setTimeout(() => {
|
|
312
|
+
const newMap = new Map(useMonitorStore.getState().taskToAgent);
|
|
313
|
+
newMap.delete(event.taskId);
|
|
314
|
+
useMonitorStore.setState({ taskToAgent: newMap });
|
|
315
|
+
}, 5000);
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case 'tool_call_start': {
|
|
320
|
+
const targetId = taskToAgent.get(event.taskId);
|
|
321
|
+
if (targetId) {
|
|
322
|
+
const agent = agents.get(targetId);
|
|
323
|
+
if (agent) agents.set(targetId, { ...agent, state: 'tool_call', lastToolName: event.toolName });
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
case 'tool_call_end': {
|
|
328
|
+
const targetId = taskToAgent.get(event.taskId);
|
|
329
|
+
if (targetId) {
|
|
330
|
+
const agent = agents.get(targetId);
|
|
331
|
+
if (agent && agent.state === 'tool_call') agents.set(targetId, { ...agent, state: 'working', lastToolName: undefined });
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
case 'chat_response': {
|
|
336
|
+
const chat = get().userChat;
|
|
337
|
+
if (chat && chat.agentId === event.agentId) {
|
|
338
|
+
const newChat = {
|
|
339
|
+
...chat,
|
|
340
|
+
messages: [...chat.messages, { role: 'agent' as const, text: event.text }],
|
|
341
|
+
loading: event.final ? false : chat.loading,
|
|
342
|
+
};
|
|
343
|
+
set({ userChat: newChat });
|
|
344
|
+
}
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
set({ agents, links, taskToAgent });
|
|
350
|
+
scheduleSave(agents);
|
|
351
|
+
},
|
|
352
|
+
})});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type MonitorEvent =
|
|
2
|
+
| { type: 'agent_loaded'; agentId: string; position?: { x: number; y: number } }
|
|
3
|
+
| { type: 'agent_reloaded'; agentId: string }
|
|
4
|
+
| { type: 'task_created'; taskId: string; contextId: string; from: string; to: string; preview: string }
|
|
5
|
+
| { type: 'task_status'; taskId: string; state: string; message?: string }
|
|
6
|
+
| { type: 'tool_call_start'; taskId: string; toolName: string; inputSummary: string }
|
|
7
|
+
| { type: 'tool_call_end'; taskId: string; toolName: string; success: boolean }
|
|
8
|
+
| { type: 'chat_response'; chatId: string; agentId: string; text: string; final: boolean };
|
|
9
|
+
|
|
10
|
+
export type AgentState =
|
|
11
|
+
| 'idle'
|
|
12
|
+
| 'submitted' // task just received, walking to master
|
|
13
|
+
| 'working' // executing task (attack or chat depending on context)
|
|
14
|
+
| 'tool_call' // working sub-state: calling a tool
|
|
15
|
+
| 'input-required' // paused, waiting for client input (walk back to master)
|
|
16
|
+
| 'auth-required' // paused, waiting for auth (same as input-required)
|
|
17
|
+
| 'completed' // task done, walking home
|
|
18
|
+
| 'failed' // task failed, shake-hand then walk home
|
|
19
|
+
| 'canceled' // task canceled by client, walk home
|
|
20
|
+
| 'rejected'; // agent refused the task, shake head at master
|
|
21
|
+
|
|
22
|
+
export type AgentAnim = 'idle' | 'walk' | 'sit' | 'attack' | 'chat' | 'daydream' | 'shake_hand' | 'head_shake';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A chat session between N agents.
|
|
26
|
+
* - initiatorId creates the session and invites others
|
|
27
|
+
* - memberIds includes all participants (initiator + invitees)
|
|
28
|
+
* - meetPoint is where everyone walks to
|
|
29
|
+
* - Each member gets a position offset around the meetPoint (computed from index)
|
|
30
|
+
*/
|
|
31
|
+
export interface ChatSession {
|
|
32
|
+
id: string;
|
|
33
|
+
initiatorId: string;
|
|
34
|
+
memberIds: string[];
|
|
35
|
+
meetPoint: { x: number; y: number };
|
|
36
|
+
duration: number;
|
|
37
|
+
startTime: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Agent {
|
|
41
|
+
id: string;
|
|
42
|
+
position: { x: number; y: number };
|
|
43
|
+
homePosition: { x: number; y: number };
|
|
44
|
+
stakePosition: { x: number; y: number };
|
|
45
|
+
sprite: string;
|
|
46
|
+
state: AgentState;
|
|
47
|
+
currentAnim: AgentAnim;
|
|
48
|
+
chatSessionId?: string; // ID of the chat session this agent is in
|
|
49
|
+
taskFrom?: string; // 'user' for client calls, agent-id for agent-to-agent calls
|
|
50
|
+
lastToolName?: string;
|
|
51
|
+
lastMessage?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface MessageLink {
|
|
55
|
+
id: string;
|
|
56
|
+
from: string;
|
|
57
|
+
to: string;
|
|
58
|
+
preview: string;
|
|
59
|
+
startTime: number;
|
|
60
|
+
taskId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ChatMessage {
|
|
64
|
+
role: 'user' | 'agent';
|
|
65
|
+
text: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface UserChat {
|
|
69
|
+
agentId: string;
|
|
70
|
+
messages: ChatMessage[];
|
|
71
|
+
loading: boolean;
|
|
72
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useMonitorStore } from './store.js';
|
|
2
|
+
import type { MonitorEvent } from './types.js';
|
|
3
|
+
|
|
4
|
+
const WS_URL = import.meta.env.VITE_MONITOR_WS_URL || 'ws://localhost:41242/monitor';
|
|
5
|
+
|
|
6
|
+
/** Shared persistent connection for both events and commands. */
|
|
7
|
+
let sharedWs: WebSocket | null = null;
|
|
8
|
+
|
|
9
|
+
export function connectWs(): () => void {
|
|
10
|
+
const ws = new WebSocket(WS_URL);
|
|
11
|
+
sharedWs = ws;
|
|
12
|
+
|
|
13
|
+
ws.addEventListener('open', () => {
|
|
14
|
+
useMonitorStore.setState({ connected: true });
|
|
15
|
+
});
|
|
16
|
+
ws.addEventListener('close', () => {
|
|
17
|
+
// Only clear sharedWs if it's still pointing to this instance
|
|
18
|
+
// (avoids race with StrictMode double-mount)
|
|
19
|
+
if (sharedWs === ws) {
|
|
20
|
+
sharedWs = null;
|
|
21
|
+
}
|
|
22
|
+
useMonitorStore.setState({ connected: false });
|
|
23
|
+
});
|
|
24
|
+
ws.addEventListener('error', () => {
|
|
25
|
+
useMonitorStore.setState({ connected: false });
|
|
26
|
+
});
|
|
27
|
+
ws.addEventListener('message', (e) => {
|
|
28
|
+
try {
|
|
29
|
+
const event = JSON.parse(e.data) as MonitorEvent;
|
|
30
|
+
useMonitorStore.getState().applyEvent(event);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.error('[ws-client] bad message:', err);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return () => ws.close();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function sendCommand(cmd: object): void {
|
|
40
|
+
if (sharedWs && sharedWs.readyState === WebSocket.OPEN) {
|
|
41
|
+
sharedWs.send(JSON.stringify(cmd));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
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();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let chatIdCounter = 0;
|
|
53
|
+
|
|
54
|
+
/** Send a chat message to an agent and receive the response via store events. */
|
|
55
|
+
export function sendChatMessage(agentId: string, message: string): string {
|
|
56
|
+
const chatId = `chat-${Date.now()}-${++chatIdCounter}`;
|
|
57
|
+
useMonitorStore.getState().addChatMessage({ role: 'user', text: message });
|
|
58
|
+
useMonitorStore.getState().setChatLoading(true);
|
|
59
|
+
sendCommand({ type: 'chat', chatId, agentId, message });
|
|
60
|
+
return chatId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Expose for E2E testing
|
|
64
|
+
if (typeof window !== 'undefined') {
|
|
65
|
+
(window as any).__DOJO_SEND_CHAT__ = sendChatMessage;
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
11
|
+
"resolveJsonModule": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
server: {
|
|
7
|
+
port: 5173,
|
|
8
|
+
proxy: {
|
|
9
|
+
// Optional: proxy A2A HTTP calls to framework (if needed)
|
|
10
|
+
// '/a2a': 'http://localhost:41241',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|