machinaos 0.0.21 → 0.0.23

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/README.md +32 -6
  2. package/bin/cli.js +0 -0
  3. package/client/dist/assets/index-5BWZnM6b.js +703 -0
  4. package/client/dist/index.html +1 -1
  5. package/client/package.json +1 -1
  6. package/client/src/Dashboard.tsx +12 -5
  7. package/client/src/ParameterPanel.tsx +6 -5
  8. package/client/src/components/AIAgentNode.tsx +35 -16
  9. package/client/src/components/CredentialsModal.tsx +450 -5
  10. package/client/src/components/TeamMonitorNode.tsx +269 -0
  11. package/client/src/components/parameterPanel/InputSection.tsx +25 -0
  12. package/client/src/contexts/WebSocketContext.tsx +38 -0
  13. package/client/src/hooks/useApiKeys.ts +44 -0
  14. package/client/src/nodeDefinitions/specializedAgentNodes.ts +59 -3
  15. package/client/src/nodeDefinitions/twitterNodes.ts +441 -0
  16. package/client/src/nodeDefinitions/utilityNodes.ts +45 -1
  17. package/client/src/nodeDefinitions.ts +7 -1
  18. package/client/src/services/executionService.ts +4 -1
  19. package/install.sh +63 -1
  20. package/package.json +5 -2
  21. package/scripts/build.js +0 -0
  22. package/scripts/clean.js +0 -0
  23. package/scripts/daemon.js +0 -0
  24. package/scripts/docker.js +0 -0
  25. package/scripts/install.js +0 -0
  26. package/scripts/postinstall.js +29 -0
  27. package/scripts/preinstall.js +67 -0
  28. package/scripts/serve-client.js +0 -0
  29. package/scripts/start.js +0 -0
  30. package/scripts/stop.js +0 -0
  31. package/scripts/sync-version.js +0 -0
  32. package/server/Dockerfile +10 -15
  33. package/server/constants.py +20 -0
  34. package/server/core/database.py +443 -3
  35. package/server/main.py +9 -1
  36. package/server/models/database.py +112 -2
  37. package/server/pyproject.toml +3 -0
  38. package/server/requirements.txt +3 -0
  39. package/server/routers/twitter.py +390 -0
  40. package/server/routers/websocket.py +320 -0
  41. package/server/services/agent_team.py +266 -0
  42. package/server/services/ai.py +43 -0
  43. package/server/services/compaction.py +39 -4
  44. package/server/services/event_waiter.py +41 -0
  45. package/server/services/handlers/__init__.py +13 -0
  46. package/server/services/handlers/ai.py +66 -2
  47. package/server/services/handlers/tools.py +84 -0
  48. package/server/services/handlers/twitter.py +297 -0
  49. package/server/services/handlers/utility.py +91 -0
  50. package/server/services/node_executor.py +15 -1
  51. package/server/services/pricing.py +270 -0
  52. package/server/services/status_broadcaster.py +79 -0
  53. package/server/services/twitter_oauth.py +410 -0
  54. package/server/skills/social_agent/twitter-search-skill/SKILL.md +146 -0
  55. package/server/skills/social_agent/twitter-send-skill/SKILL.md +142 -0
  56. package/server/skills/social_agent/twitter-user-skill/SKILL.md +165 -0
  57. package/workflows/Zeenie_full.json +459 -0
  58. package/workflows/Zeenie_small.json +459 -0
  59. package/client/dist/assets/index-YVvAiByx.js +0 -703
  60. package/server/requirements-docker.txt +0 -86
@@ -1045,6 +1045,163 @@ async def handle_claude_oauth_status(data: Dict[str, Any], websocket: WebSocket)
1045
1045
  return get_claude_credentials()
1046
1046
 
1047
1047
 
1048
+ # ============================================================================
1049
+ # Twitter OAuth Handlers
1050
+ # ============================================================================
1051
+
1052
+ @ws_handler()
1053
+ async def handle_twitter_oauth_login(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1054
+ """
1055
+ Initiate Twitter OAuth 2.0 with PKCE flow.
1056
+
1057
+ Opens browser to Twitter authorization page. After user authorizes,
1058
+ Twitter redirects to /api/twitter/callback which stores tokens.
1059
+ """
1060
+ import webbrowser
1061
+ from services.twitter_oauth import TwitterOAuth
1062
+
1063
+ auth_service = container.auth_service()
1064
+
1065
+ # Get stored client credentials (configured via Credentials Modal)
1066
+ client_id = await auth_service.get_api_key("twitter_client_id")
1067
+ client_secret = await auth_service.get_api_key("twitter_client_secret")
1068
+
1069
+ if not client_id:
1070
+ return {
1071
+ "success": False,
1072
+ "error": "Twitter Client ID not configured. Add your Twitter API credentials first."
1073
+ }
1074
+
1075
+ # Create OAuth instance and generate authorization URL
1076
+ oauth = TwitterOAuth(
1077
+ client_id=client_id,
1078
+ client_secret=client_secret,
1079
+ redirect_uri="http://localhost:3010/api/twitter/callback",
1080
+ )
1081
+
1082
+ auth_data = oauth.generate_authorization_url()
1083
+
1084
+ # Open browser to authorization URL
1085
+ webbrowser.open(auth_data["url"])
1086
+
1087
+ return {
1088
+ "success": True,
1089
+ "message": "Opening Twitter authorization in browser...",
1090
+ "state": auth_data["state"],
1091
+ }
1092
+
1093
+
1094
+ @ws_handler()
1095
+ async def handle_twitter_oauth_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1096
+ """
1097
+ Check Twitter OAuth connection status.
1098
+
1099
+ Returns connection status and user info if connected.
1100
+ """
1101
+ from services.twitter_oauth import TwitterOAuth
1102
+
1103
+ auth_service = container.auth_service()
1104
+
1105
+ # Check for stored access token
1106
+ access_token = await auth_service.get_api_key("twitter_access_token")
1107
+
1108
+ if not access_token:
1109
+ return {
1110
+ "connected": False,
1111
+ "username": None,
1112
+ "user_id": None,
1113
+ }
1114
+
1115
+ # Get client credentials for API calls
1116
+ client_id = await auth_service.get_api_key("twitter_client_id") or ""
1117
+ client_secret = await auth_service.get_api_key("twitter_client_secret")
1118
+
1119
+ oauth = TwitterOAuth(
1120
+ client_id=client_id,
1121
+ client_secret=client_secret,
1122
+ redirect_uri="http://localhost:3010/api/twitter/callback",
1123
+ )
1124
+
1125
+ # Verify token by getting user info
1126
+ user_info = await oauth.get_user_info(access_token)
1127
+
1128
+ if not user_info.get("success"):
1129
+ # Try to refresh token
1130
+ refresh_token = await auth_service.get_api_key("twitter_refresh_token")
1131
+ if refresh_token:
1132
+ refresh_result = await oauth.refresh_access_token(refresh_token)
1133
+ if refresh_result.get("success"):
1134
+ # Store new tokens
1135
+ await auth_service.store_api_key(
1136
+ provider="twitter_access_token",
1137
+ api_key=refresh_result["access_token"],
1138
+ models=[],
1139
+ session_id="default"
1140
+ )
1141
+ if refresh_result.get("refresh_token"):
1142
+ await auth_service.store_api_key(
1143
+ provider="twitter_refresh_token",
1144
+ api_key=refresh_result["refresh_token"],
1145
+ models=[],
1146
+ session_id="default"
1147
+ )
1148
+ # Retry user info
1149
+ user_info = await oauth.get_user_info(refresh_result["access_token"])
1150
+
1151
+ if not user_info.get("success"):
1152
+ return {
1153
+ "connected": False,
1154
+ "username": None,
1155
+ "user_id": None,
1156
+ "error": user_info.get("error"),
1157
+ }
1158
+
1159
+ return {
1160
+ "connected": True,
1161
+ "username": user_info.get("username"),
1162
+ "user_id": user_info.get("id"),
1163
+ "name": user_info.get("name"),
1164
+ "profile_image_url": user_info.get("profile_image_url"),
1165
+ "verified": user_info.get("verified"),
1166
+ }
1167
+
1168
+
1169
+ @ws_handler()
1170
+ async def handle_twitter_logout(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1171
+ """
1172
+ Disconnect Twitter by revoking tokens and clearing stored credentials.
1173
+ """
1174
+ from services.twitter_oauth import TwitterOAuth
1175
+
1176
+ auth_service = container.auth_service()
1177
+
1178
+ # Get stored tokens
1179
+ access_token = await auth_service.get_api_key("twitter_access_token")
1180
+ refresh_token = await auth_service.get_api_key("twitter_refresh_token")
1181
+ client_id = await auth_service.get_api_key("twitter_client_id") or ""
1182
+ client_secret = await auth_service.get_api_key("twitter_client_secret")
1183
+
1184
+ # Revoke tokens if we have them
1185
+ if access_token or refresh_token:
1186
+ oauth = TwitterOAuth(
1187
+ client_id=client_id,
1188
+ client_secret=client_secret,
1189
+ redirect_uri="http://localhost:3010/api/twitter/callback",
1190
+ )
1191
+
1192
+ if access_token:
1193
+ await oauth.revoke_token(access_token, "access_token")
1194
+ if refresh_token:
1195
+ await oauth.revoke_token(refresh_token, "refresh_token")
1196
+
1197
+ # Clear stored credentials
1198
+ await auth_service.remove_api_key("twitter_access_token")
1199
+ await auth_service.remove_api_key("twitter_refresh_token")
1200
+ await auth_service.remove_api_key("twitter_user_info")
1201
+
1202
+ return {"success": True, "message": "Twitter disconnected"}
1203
+
1204
+
1048
1205
  @ws_handler("url")
1049
1206
  async def handle_test_ai_proxy(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
1050
1207
  """Test connectivity to an AI proxy server."""
@@ -2059,6 +2216,149 @@ async def handle_configure_compaction(data: Dict[str, Any], websocket: WebSocket
2059
2216
  return {"success": success}
2060
2217
 
2061
2218
 
2219
+ @ws_handler()
2220
+ async def handle_get_provider_usage_summary(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2221
+ """Get aggregated token usage and cost by provider for Credentials Modal."""
2222
+ database = container.database()
2223
+ providers = await database.get_provider_usage_summary()
2224
+ return {"success": True, "providers": providers}
2225
+
2226
+
2227
+ # ============================================================================
2228
+ # Agent Team Handlers
2229
+ # ============================================================================
2230
+
2231
+ @ws_handler("workflow_id", "team_lead_node_id")
2232
+ async def handle_create_team(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2233
+ """Create a new agent team."""
2234
+ from services.agent_team import get_agent_team_service
2235
+ service = get_agent_team_service()
2236
+ team = await service.create_team(
2237
+ team_lead_node_id=data["team_lead_node_id"],
2238
+ teammate_node_ids=data.get("teammates", []),
2239
+ workflow_id=data["workflow_id"],
2240
+ config=data.get("config")
2241
+ )
2242
+ return {"team": team} if team else {"success": False, "error": "Failed to create team"}
2243
+
2244
+
2245
+ @ws_handler("team_id")
2246
+ async def handle_get_team(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2247
+ """Get team info."""
2248
+ database = container.database()
2249
+ team = await database.get_team(data["team_id"])
2250
+ return {"team": team} if team else {"success": False, "error": "Team not found"}
2251
+
2252
+
2253
+ @ws_handler()
2254
+ async def handle_get_team_status(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2255
+ """Get team status with stats.
2256
+
2257
+ Can provide team_id directly, or team_lead_node_id to find active team.
2258
+ """
2259
+ from services.agent_team import get_agent_team_service
2260
+
2261
+ try:
2262
+ service = get_agent_team_service()
2263
+ except RuntimeError:
2264
+ # Service not initialized - no active teams
2265
+ return {"status": {"members": [], "task_count": 0, "completed_count": 0, "active_count": 0, "pending_count": 0, "failed_count": 0, "active_tasks": []}}
2266
+
2267
+ team_id = data.get("team_id")
2268
+
2269
+ # If no team_id, try to find by team_lead_node_id
2270
+ if not team_id and data.get("team_lead_node_id"):
2271
+ # Look up active team for this workflow (most recent)
2272
+ # This is a simple approach - check if there's an active team with this lead
2273
+ # For now, return empty status - teams are created when AI Employee runs
2274
+ return {"status": {"members": [], "task_count": 0, "completed_count": 0, "active_count": 0, "pending_count": 0, "failed_count": 0, "active_tasks": [], "message": "No active team yet"}}
2275
+
2276
+ if not team_id:
2277
+ return {"status": {"members": [], "task_count": 0, "completed_count": 0, "active_count": 0, "pending_count": 0, "failed_count": 0, "active_tasks": [], "message": "No team connected"}}
2278
+
2279
+ status = await service.get_team_status(team_id)
2280
+ return {"status": status}
2281
+
2282
+
2283
+ @ws_handler("team_id")
2284
+ async def handle_dissolve_team(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2285
+ """Dissolve a team."""
2286
+ from services.agent_team import get_agent_team_service
2287
+ service = get_agent_team_service()
2288
+ success = await service.dissolve_team(data["team_id"], data.get("workflow_id"))
2289
+ return {"success": success}
2290
+
2291
+
2292
+ @ws_handler("team_id", "title", "created_by")
2293
+ async def handle_add_team_task(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2294
+ """Add task to team."""
2295
+ from services.agent_team import get_agent_team_service
2296
+ service = get_agent_team_service()
2297
+ task = await service.add_task(
2298
+ team_id=data["team_id"],
2299
+ title=data["title"],
2300
+ created_by=data["created_by"],
2301
+ description=data.get("description"),
2302
+ priority=data.get("priority", 3),
2303
+ depends_on=data.get("depends_on")
2304
+ )
2305
+ return {"task": task} if task else {"success": False, "error": "Failed to add task"}
2306
+
2307
+
2308
+ @ws_handler("team_id", "task_id", "agent_node_id")
2309
+ async def handle_claim_team_task(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2310
+ """Claim a task."""
2311
+ from services.agent_team import get_agent_team_service
2312
+ service = get_agent_team_service()
2313
+ task = await service.claim_task(data["team_id"], data["task_id"], data["agent_node_id"])
2314
+ return {"task": task} if task else {"success": False, "error": "Task unavailable"}
2315
+
2316
+
2317
+ @ws_handler("team_id", "task_id")
2318
+ async def handle_complete_team_task(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2319
+ """Complete a task."""
2320
+ from services.agent_team import get_agent_team_service
2321
+ service = get_agent_team_service()
2322
+ success = await service.complete_task(data["team_id"], data["task_id"], data.get("result"))
2323
+ return {"success": success}
2324
+
2325
+
2326
+ @ws_handler("team_id")
2327
+ async def handle_get_team_tasks(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2328
+ """Get team tasks."""
2329
+ database = container.database()
2330
+ tasks = await database.get_team_tasks(data["team_id"], data.get("status"))
2331
+ return {"tasks": tasks}
2332
+
2333
+
2334
+ @ws_handler("team_id", "from_agent", "content")
2335
+ async def handle_send_team_message(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2336
+ """Send message in team."""
2337
+ from services.agent_team import get_agent_team_service
2338
+ service = get_agent_team_service()
2339
+ msg = await service.send_message(
2340
+ team_id=data["team_id"],
2341
+ from_agent=data["from_agent"],
2342
+ content=data["content"],
2343
+ to_agent=data.get("to_agent"),
2344
+ message_type=data.get("message_type", "direct")
2345
+ )
2346
+ return {"message": msg} if msg else {"success": False, "error": "Failed to send message"}
2347
+
2348
+
2349
+ @ws_handler("team_id")
2350
+ async def handle_get_team_messages(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
2351
+ """Get team messages."""
2352
+ from services.agent_team import get_agent_team_service
2353
+ service = get_agent_team_service()
2354
+ messages = await service.get_messages(
2355
+ team_id=data["team_id"],
2356
+ agent_node_id=data.get("agent_node_id"),
2357
+ unread_only=data.get("unread_only", False)
2358
+ )
2359
+ return {"messages": messages}
2360
+
2361
+
2062
2362
  # ============================================================================
2063
2363
  # Message Router
2064
2364
  # ============================================================================
@@ -2123,6 +2423,11 @@ MESSAGE_HANDLERS: Dict[str, MessageHandler] = {
2123
2423
  "claude_oauth_login": handle_claude_oauth_login,
2124
2424
  "claude_oauth_status": handle_claude_oauth_status,
2125
2425
 
2426
+ # Twitter OAuth operations
2427
+ "twitter_oauth_login": handle_twitter_oauth_login,
2428
+ "twitter_oauth_status": handle_twitter_oauth_status,
2429
+ "twitter_logout": handle_twitter_logout,
2430
+
2126
2431
  # Android operations
2127
2432
  "get_android_devices": handle_get_android_devices,
2128
2433
  "execute_android_action": handle_execute_android_action,
@@ -2195,6 +2500,21 @@ MESSAGE_HANDLERS: Dict[str, MessageHandler] = {
2195
2500
  # Compaction
2196
2501
  "get_compaction_stats": handle_get_compaction_stats,
2197
2502
  "configure_compaction": handle_configure_compaction,
2503
+
2504
+ # Provider Usage Summary
2505
+ "get_provider_usage_summary": handle_get_provider_usage_summary,
2506
+
2507
+ # Agent Teams
2508
+ "create_team": handle_create_team,
2509
+ "get_team": handle_get_team,
2510
+ "get_team_status": handle_get_team_status,
2511
+ "dissolve_team": handle_dissolve_team,
2512
+ "add_team_task": handle_add_team_task,
2513
+ "claim_team_task": handle_claim_team_task,
2514
+ "complete_team_task": handle_complete_team_task,
2515
+ "get_team_tasks": handle_get_team_tasks,
2516
+ "send_team_message": handle_send_team_message,
2517
+ "get_team_messages": handle_get_team_messages,
2198
2518
  }
2199
2519
 
2200
2520
 
@@ -0,0 +1,266 @@
1
+ """Agent Team Service - Claude SDK Agent Teams pattern.
2
+
3
+ Coordinates multi-agent teams with shared task lists and messaging.
4
+ Teams are scoped to specific workflow executions.
5
+ """
6
+
7
+ import uuid
8
+ from typing import Dict, Any, List, Optional
9
+ from core.database import Database
10
+ from core.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class AgentTeamService:
16
+ """Service for managing agent teams.
17
+
18
+ Teams are workflow-specific - each team belongs to a workflow execution.
19
+ """
20
+
21
+ def __init__(self, database: Database, broadcaster=None):
22
+ self.database = database
23
+ self.broadcaster = broadcaster
24
+ # Track active teams per workflow
25
+ self._active_teams: Dict[str, str] = {} # workflow_id -> team_id
26
+
27
+ # -------------------------------------------------------------------------
28
+ # Team Lifecycle
29
+ # -------------------------------------------------------------------------
30
+
31
+ async def create_team(
32
+ self,
33
+ team_lead_node_id: str,
34
+ teammate_node_ids: List[Dict[str, Any]],
35
+ workflow_id: str,
36
+ config: Optional[Dict[str, Any]] = None
37
+ ) -> Optional[Dict[str, Any]]:
38
+ """Create a team with lead and teammates.
39
+
40
+ Args:
41
+ team_lead_node_id: Node ID of the team lead agent
42
+ teammate_node_ids: List of {node_id, node_type, label} for teammates
43
+ workflow_id: Workflow containing the team
44
+ config: Team configuration (mode: parallel/sequential/hybrid)
45
+
46
+ Returns:
47
+ Team dict or None on failure
48
+ """
49
+ team_id = f"team_{uuid.uuid4().hex[:12]}"
50
+
51
+ # Create team
52
+ team = await self.database.create_team(
53
+ team_id=team_id,
54
+ workflow_id=workflow_id,
55
+ team_lead_node_id=team_lead_node_id,
56
+ config=config or {"mode": "parallel"}
57
+ )
58
+ if not team:
59
+ return None
60
+
61
+ # Add team lead as member
62
+ await self.database.add_team_member(
63
+ team_id=team_id,
64
+ agent_node_id=team_lead_node_id,
65
+ agent_type="orchestrator_agent",
66
+ role="team_lead"
67
+ )
68
+
69
+ # Add teammates
70
+ for teammate in teammate_node_ids:
71
+ await self.database.add_team_member(
72
+ team_id=team_id,
73
+ agent_node_id=teammate["node_id"],
74
+ agent_type=teammate.get("node_type", "agent"),
75
+ agent_label=teammate.get("label"),
76
+ role="teammate"
77
+ )
78
+
79
+ # Broadcast team creation
80
+ if self.broadcaster:
81
+ await self.broadcaster.broadcast_team_event(team_id, "team_created", {
82
+ "team_id": team_id,
83
+ "workflow_id": workflow_id,
84
+ "member_count": len(teammate_node_ids) + 1
85
+ })
86
+
87
+ # Track active team for this workflow
88
+ self._active_teams[workflow_id] = team_id
89
+
90
+ logger.info(f"[Teams] Created team {team_id} with {len(teammate_node_ids)} teammates")
91
+ return {"team_id": team_id, **team}
92
+
93
+ def get_active_team_for_workflow(self, workflow_id: str) -> Optional[str]:
94
+ """Get the active team ID for a workflow."""
95
+ return self._active_teams.get(workflow_id)
96
+
97
+ async def dissolve_team(self, team_id: str, workflow_id: Optional[str] = None) -> bool:
98
+ """Dissolve a team."""
99
+ success = await self.database.update_team_status(team_id, "dissolved")
100
+ if success:
101
+ # Remove from active teams tracking
102
+ if workflow_id and workflow_id in self._active_teams:
103
+ del self._active_teams[workflow_id]
104
+ if self.broadcaster:
105
+ await self.broadcaster.broadcast_team_event(team_id, "team_dissolved", {"team_id": team_id})
106
+ return success
107
+
108
+ async def get_team_status(self, team_id: str) -> Dict[str, Any]:
109
+ """Get comprehensive team status."""
110
+ return await self.database.get_team_stats(team_id)
111
+
112
+ # -------------------------------------------------------------------------
113
+ # Task Management
114
+ # -------------------------------------------------------------------------
115
+
116
+ async def add_task(
117
+ self,
118
+ team_id: str,
119
+ title: str,
120
+ created_by: str,
121
+ description: Optional[str] = None,
122
+ priority: int = 3,
123
+ depends_on: Optional[List[str]] = None
124
+ ) -> Optional[Dict[str, Any]]:
125
+ """Add a task to the shared task list."""
126
+ task_id = f"task_{uuid.uuid4().hex[:12]}"
127
+
128
+ task = await self.database.add_team_task(
129
+ task_id=task_id,
130
+ team_id=team_id,
131
+ title=title,
132
+ created_by=created_by,
133
+ description=description,
134
+ priority=priority,
135
+ depends_on=depends_on
136
+ )
137
+
138
+ if task and self.broadcaster:
139
+ await self.broadcaster.broadcast_team_event(team_id, "task_added", task)
140
+
141
+ return task
142
+
143
+ async def claim_task(self, team_id: str, task_id: str, agent_node_id: str) -> Optional[Dict[str, Any]]:
144
+ """Claim a task for an agent."""
145
+ # Update member status to working
146
+ await self.database.update_member_status(team_id, agent_node_id, "working")
147
+
148
+ task = await self.database.claim_task(task_id, agent_node_id)
149
+
150
+ if task and self.broadcaster:
151
+ await self.broadcaster.broadcast_team_event(team_id, "task_claimed", {
152
+ **task, "claimed_by": agent_node_id
153
+ })
154
+
155
+ return task
156
+
157
+ async def complete_task(self, team_id: str, task_id: str, result: Optional[Dict[str, Any]] = None) -> bool:
158
+ """Mark a task as completed."""
159
+ # Get task to find assigned agent
160
+ tasks = await self.database.get_team_tasks(team_id)
161
+ task = next((t for t in tasks if t["id"] == task_id), None)
162
+
163
+ success = await self.database.complete_task(task_id, result)
164
+
165
+ if success:
166
+ # Update member status back to idle
167
+ if task and task.get("assigned_to"):
168
+ await self.database.update_member_status(team_id, task["assigned_to"], "idle")
169
+
170
+ if self.broadcaster:
171
+ await self.broadcaster.broadcast_team_event(team_id, "task_completed", {
172
+ "task_id": task_id, "result": result
173
+ })
174
+
175
+ return success
176
+
177
+ async def fail_task(self, team_id: str, task_id: str, error: str) -> bool:
178
+ """Mark a task as failed."""
179
+ tasks = await self.database.get_team_tasks(team_id)
180
+ task = next((t for t in tasks if t["id"] == task_id), None)
181
+
182
+ success = await self.database.fail_task(task_id, error)
183
+
184
+ if success:
185
+ if task and task.get("assigned_to"):
186
+ await self.database.update_member_status(team_id, task["assigned_to"], "idle")
187
+
188
+ if self.broadcaster:
189
+ await self.broadcaster.broadcast_team_event(team_id, "task_failed", {
190
+ "task_id": task_id, "error": error
191
+ })
192
+
193
+ return success
194
+
195
+ async def get_claimable_tasks(self, team_id: str) -> List[Dict[str, Any]]:
196
+ """Get tasks ready to be claimed."""
197
+ return await self.database.get_claimable_tasks(team_id)
198
+
199
+ async def is_team_done(self, team_id: str) -> bool:
200
+ """Check if all tasks are completed/failed."""
201
+ tasks = await self.database.get_team_tasks(team_id)
202
+ if not tasks:
203
+ return True
204
+ return all(t["status"] in ("completed", "failed", "skipped") for t in tasks)
205
+
206
+ # -------------------------------------------------------------------------
207
+ # Messaging
208
+ # -------------------------------------------------------------------------
209
+
210
+ async def send_message(
211
+ self,
212
+ team_id: str,
213
+ from_agent: str,
214
+ content: str,
215
+ to_agent: Optional[str] = None,
216
+ message_type: str = "direct"
217
+ ) -> Optional[Dict[str, Any]]:
218
+ """Send a message to a specific agent or broadcast."""
219
+ msg = await self.database.add_agent_message(
220
+ team_id=team_id,
221
+ from_agent=from_agent,
222
+ content=content,
223
+ message_type=message_type if to_agent else "broadcast",
224
+ to_agent=to_agent
225
+ )
226
+
227
+ if msg and self.broadcaster:
228
+ await self.broadcaster.broadcast_team_event(team_id, "team_message", msg)
229
+
230
+ return msg
231
+
232
+ async def broadcast(self, team_id: str, from_agent: str, content: str) -> Optional[Dict[str, Any]]:
233
+ """Broadcast message to all team members."""
234
+ return await self.send_message(team_id, from_agent, content, to_agent=None)
235
+
236
+ async def get_messages(
237
+ self,
238
+ team_id: str,
239
+ agent_node_id: Optional[str] = None,
240
+ unread_only: bool = False
241
+ ) -> List[Dict[str, Any]]:
242
+ """Get messages for a team or specific agent."""
243
+ return await self.database.get_agent_messages(team_id, agent_node_id, unread_only)
244
+
245
+ async def mark_read(self, team_id: str, agent_node_id: str) -> int:
246
+ """Mark all messages as read for an agent."""
247
+ return await self.database.mark_messages_read(team_id, agent_node_id)
248
+
249
+
250
+ # Singleton instance
251
+ _service: Optional[AgentTeamService] = None
252
+
253
+
254
+ def get_agent_team_service() -> AgentTeamService:
255
+ """Get the singleton AgentTeamService instance."""
256
+ global _service
257
+ if _service is None:
258
+ raise RuntimeError("AgentTeamService not initialized. Call init_agent_team_service first.")
259
+ return _service
260
+
261
+
262
+ def init_agent_team_service(database: Database, broadcaster=None) -> AgentTeamService:
263
+ """Initialize the singleton AgentTeamService."""
264
+ global _service
265
+ _service = AgentTeamService(database, broadcaster)
266
+ return _service
@@ -2917,6 +2917,49 @@ class AIService:
2917
2917
 
2918
2918
  return TaskManagerSchema
2919
2919
 
2920
+ # Twitter Send tool schema (dual-purpose: workflow node + AI tool)
2921
+ if node_type == 'twitterSend':
2922
+ class TwitterSendSchema(BaseModel):
2923
+ """Post tweets, replies, retweets, and likes on Twitter/X."""
2924
+ action: str = Field(
2925
+ default="tweet",
2926
+ description="Action: tweet, reply, retweet, like, unlike, delete"
2927
+ )
2928
+ text: Optional[str] = Field(
2929
+ default=None,
2930
+ description="Tweet text (max 280 chars). Required for tweet/reply."
2931
+ )
2932
+ tweet_id: Optional[str] = Field(
2933
+ default=None,
2934
+ description="Tweet ID for reply/retweet/like/unlike/delete actions."
2935
+ )
2936
+ reply_to_id: Optional[str] = Field(
2937
+ default=None,
2938
+ description="Tweet ID to reply to (alias for tweet_id when action=reply)."
2939
+ )
2940
+
2941
+ return TwitterSendSchema
2942
+
2943
+ # Twitter Search tool schema (dual-purpose: workflow node + AI tool)
2944
+ if node_type == 'twitterSearch':
2945
+ class TwitterSearchSchema(BaseModel):
2946
+ """Search recent tweets on Twitter/X."""
2947
+ query: str = Field(description="Search query (supports X search operators)")
2948
+ max_results: int = Field(default=10, description="Max results (10-100)")
2949
+
2950
+ return TwitterSearchSchema
2951
+
2952
+ # Twitter User tool schema (dual-purpose: workflow node + AI tool)
2953
+ if node_type == 'twitterUser':
2954
+ class TwitterUserSchema(BaseModel):
2955
+ """Look up Twitter/X user profiles and social connections."""
2956
+ operation: str = Field(default="me", description="Operation: me, by_username, by_id, followers, following")
2957
+ username: Optional[str] = Field(default=None, description="Twitter username (without @) for by_username")
2958
+ user_id: Optional[str] = Field(default=None, description="Twitter user ID for by_id/followers/following")
2959
+ max_results: int = Field(default=100, description="Max results for followers/following (1-1000)")
2960
+
2961
+ return TwitterUserSchema
2962
+
2920
2963
  # Check delegated tasks schema (built-in tool for result retrieval)
2921
2964
  if node_type == '_builtin_check_delegated_tasks':
2922
2965
  class CheckDelegatedTasksSchema(BaseModel):