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
@@ -12,7 +12,8 @@ from core.config import Settings
12
12
  from models.database import (
13
13
  NodeParameter, Workflow, Execution, APIKey, APIKeyValidation, NodeOutput,
14
14
  ConversationMessage, ToolSchema, UserSkill, ChatMessage, UserSettings,
15
- TokenUsageMetric, CompactionEvent, SessionTokenState, ProviderDefaults
15
+ TokenUsageMetric, CompactionEvent, SessionTokenState, ProviderDefaults,
16
+ AgentTeam, TeamMember, TeamTask, AgentMessage
16
17
  )
17
18
  from models.cache import CacheEntry # SQLite-backed cache for Redis alternative
18
19
  from models.auth import User # Import User model to ensure table creation
@@ -87,6 +88,29 @@ class Database:
87
88
  "ALTER TABLE user_settings ADD COLUMN examples_loaded BOOLEAN DEFAULT 0"
88
89
  ))
89
90
  logger.info("Added examples_loaded column to user_settings")
91
+
92
+ # Migrate token_usage_metrics table - add cost columns
93
+ result = await conn.execute(text("PRAGMA table_info(token_usage_metrics)"))
94
+ columns = {row[1] for row in result.fetchall()}
95
+
96
+ for col in ["input_cost", "output_cost", "cache_cost", "total_cost"]:
97
+ if col not in columns:
98
+ await conn.execute(text(
99
+ f"ALTER TABLE token_usage_metrics ADD COLUMN {col} REAL DEFAULT 0.0"
100
+ ))
101
+ logger.info(f"Added {col} column to token_usage_metrics")
102
+
103
+ # Migrate session_token_states table - add cumulative cost columns
104
+ result = await conn.execute(text("PRAGMA table_info(session_token_states)"))
105
+ columns = {row[1] for row in result.fetchall()}
106
+
107
+ for col in ["cumulative_input_cost", "cumulative_output_cost", "cumulative_total_cost"]:
108
+ if col not in columns:
109
+ await conn.execute(text(
110
+ f"ALTER TABLE session_token_states ADD COLUMN {col} REAL DEFAULT 0.0"
111
+ ))
112
+ logger.info(f"Added {col} column to session_token_states")
113
+
90
114
  except Exception as e:
91
115
  logger.warning(f"Migration check failed (table may not exist yet): {e}")
92
116
 
@@ -1568,7 +1592,12 @@ class Database:
1568
1592
  reasoning_tokens=metric.get("reasoning_tokens", 0),
1569
1593
  iteration=metric.get("iteration", 1),
1570
1594
  execution_id=metric.get("execution_id"),
1571
- created_at=datetime.now(timezone.utc)
1595
+ created_at=datetime.now(timezone.utc),
1596
+ # Cost fields
1597
+ input_cost=metric.get("input_cost", 0.0),
1598
+ output_cost=metric.get("output_cost", 0.0),
1599
+ cache_cost=metric.get("cache_cost", 0.0),
1600
+ total_cost=metric.get("total_cost", 0.0)
1572
1601
  )
1573
1602
  session.add(entry)
1574
1603
  await session.commit()
@@ -1614,6 +1643,97 @@ class Database:
1614
1643
  logger.error("Failed to get token metrics", session_id=session_id, error=str(e))
1615
1644
  return []
1616
1645
 
1646
+ async def get_provider_usage_summary(self) -> List[Dict[str, Any]]:
1647
+ """Get aggregated token usage and cost by provider.
1648
+
1649
+ Returns a list of provider summaries with:
1650
+ - provider: Provider name (openai, anthropic, etc.)
1651
+ - total_input_tokens: Sum of input tokens
1652
+ - total_output_tokens: Sum of output tokens
1653
+ - total_tokens: Sum of all tokens
1654
+ - total_input_cost: Sum of input costs (USD)
1655
+ - total_output_cost: Sum of output costs (USD)
1656
+ - total_cost: Sum of all costs (USD)
1657
+ - execution_count: Number of executions
1658
+ - models: Breakdown by model (list of dicts)
1659
+ """
1660
+ try:
1661
+ async with self.get_session() as session:
1662
+ from sqlalchemy import func
1663
+
1664
+ # First get per-model breakdown
1665
+ model_stmt = (
1666
+ select(
1667
+ TokenUsageMetric.provider,
1668
+ TokenUsageMetric.model,
1669
+ func.sum(TokenUsageMetric.input_tokens).label("input_tokens"),
1670
+ func.sum(TokenUsageMetric.output_tokens).label("output_tokens"),
1671
+ func.sum(TokenUsageMetric.total_tokens).label("total_tokens"),
1672
+ func.sum(TokenUsageMetric.input_cost).label("input_cost"),
1673
+ func.sum(TokenUsageMetric.output_cost).label("output_cost"),
1674
+ func.sum(TokenUsageMetric.cache_cost).label("cache_cost"),
1675
+ func.sum(TokenUsageMetric.total_cost).label("total_cost"),
1676
+ func.count().label("execution_count")
1677
+ )
1678
+ .group_by(TokenUsageMetric.provider, TokenUsageMetric.model)
1679
+ .order_by(TokenUsageMetric.provider, TokenUsageMetric.model)
1680
+ )
1681
+ result = await session.execute(model_stmt)
1682
+ rows = result.all()
1683
+
1684
+ # Aggregate by provider
1685
+ providers: Dict[str, Dict] = {}
1686
+ for row in rows:
1687
+ provider = row.provider or "unknown"
1688
+ if provider not in providers:
1689
+ providers[provider] = {
1690
+ "provider": provider,
1691
+ "total_input_tokens": 0,
1692
+ "total_output_tokens": 0,
1693
+ "total_tokens": 0,
1694
+ "total_input_cost": 0.0,
1695
+ "total_output_cost": 0.0,
1696
+ "total_cache_cost": 0.0,
1697
+ "total_cost": 0.0,
1698
+ "execution_count": 0,
1699
+ "models": []
1700
+ }
1701
+
1702
+ p = providers[provider]
1703
+ p["total_input_tokens"] += row.input_tokens or 0
1704
+ p["total_output_tokens"] += row.output_tokens or 0
1705
+ p["total_tokens"] += row.total_tokens or 0
1706
+ p["total_input_cost"] += float(row.input_cost or 0)
1707
+ p["total_output_cost"] += float(row.output_cost or 0)
1708
+ p["total_cache_cost"] += float(row.cache_cost or 0)
1709
+ p["total_cost"] += float(row.total_cost or 0)
1710
+ p["execution_count"] += row.execution_count or 0
1711
+
1712
+ p["models"].append({
1713
+ "model": row.model,
1714
+ "input_tokens": row.input_tokens or 0,
1715
+ "output_tokens": row.output_tokens or 0,
1716
+ "total_tokens": row.total_tokens or 0,
1717
+ "input_cost": round(float(row.input_cost or 0), 6),
1718
+ "output_cost": round(float(row.output_cost or 0), 6),
1719
+ "cache_cost": round(float(row.cache_cost or 0), 6),
1720
+ "total_cost": round(float(row.total_cost or 0), 6),
1721
+ "execution_count": row.execution_count or 0
1722
+ })
1723
+
1724
+ # Round provider totals
1725
+ for p in providers.values():
1726
+ p["total_input_cost"] = round(p["total_input_cost"], 6)
1727
+ p["total_output_cost"] = round(p["total_output_cost"], 6)
1728
+ p["total_cache_cost"] = round(p["total_cache_cost"], 6)
1729
+ p["total_cost"] = round(p["total_cost"], 6)
1730
+
1731
+ return list(providers.values())
1732
+
1733
+ except Exception as e:
1734
+ logger.error("Failed to get provider usage summary", error=str(e))
1735
+ return []
1736
+
1617
1737
  # ============================================================================
1618
1738
  # Session Token State
1619
1739
  # ============================================================================
@@ -1786,4 +1906,324 @@ class Database:
1786
1906
  ]
1787
1907
  except Exception as e:
1788
1908
  logger.error("Failed to get compaction history", session_id=session_id, error=str(e))
1789
- return []
1909
+ return []
1910
+
1911
+ # ============================================================================
1912
+ # Agent Teams - CRUD Operations
1913
+ # ============================================================================
1914
+
1915
+ async def create_team(self, team_id: str, workflow_id: str, team_lead_node_id: str,
1916
+ config: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
1917
+ """Create a new agent team."""
1918
+ try:
1919
+ async with self.get_session() as session:
1920
+ team = AgentTeam(
1921
+ id=team_id, workflow_id=workflow_id, team_lead_node_id=team_lead_node_id,
1922
+ config=config or {}, created_at=datetime.now(timezone.utc)
1923
+ )
1924
+ session.add(team)
1925
+ await session.commit()
1926
+ logger.info(f"[Teams] Created team {team_id}")
1927
+ return {"id": team.id, "workflow_id": team.workflow_id, "status": team.status}
1928
+ except Exception as e:
1929
+ logger.error(f"Failed to create team: {e}")
1930
+ return None
1931
+
1932
+ async def get_team(self, team_id: str) -> Optional[Dict[str, Any]]:
1933
+ """Get team by ID."""
1934
+ try:
1935
+ async with self.get_session() as session:
1936
+ result = await session.execute(select(AgentTeam).where(AgentTeam.id == team_id))
1937
+ team = result.scalar_one_or_none()
1938
+ if not team:
1939
+ return None
1940
+ return {
1941
+ "id": team.id, "workflow_id": team.workflow_id, "team_lead_node_id": team.team_lead_node_id,
1942
+ "status": team.status, "config": team.config,
1943
+ "created_at": team.created_at.isoformat() if team.created_at else None
1944
+ }
1945
+ except Exception as e:
1946
+ logger.error(f"Failed to get team: {e}")
1947
+ return None
1948
+
1949
+ async def update_team_status(self, team_id: str, status: str) -> bool:
1950
+ """Update team status."""
1951
+ try:
1952
+ async with self.get_session() as session:
1953
+ result = await session.execute(select(AgentTeam).where(AgentTeam.id == team_id))
1954
+ team = result.scalar_one_or_none()
1955
+ if not team:
1956
+ return False
1957
+ team.status = status
1958
+ if status in ("completed", "failed", "dissolved"):
1959
+ team.completed_at = datetime.now(timezone.utc)
1960
+ await session.commit()
1961
+ return True
1962
+ except Exception as e:
1963
+ logger.error(f"Failed to update team status: {e}")
1964
+ return False
1965
+
1966
+ async def add_team_member(self, team_id: str, agent_node_id: str, agent_type: str,
1967
+ role: str = "teammate", agent_label: Optional[str] = None) -> Optional[Dict[str, Any]]:
1968
+ """Add member to team."""
1969
+ try:
1970
+ async with self.get_session() as session:
1971
+ member = TeamMember(
1972
+ team_id=team_id, agent_node_id=agent_node_id, agent_type=agent_type,
1973
+ agent_label=agent_label, role=role, joined_at=datetime.now(timezone.utc)
1974
+ )
1975
+ session.add(member)
1976
+ await session.commit()
1977
+ return {"id": member.id, "agent_node_id": agent_node_id, "role": role}
1978
+ except Exception as e:
1979
+ logger.error(f"Failed to add team member: {e}")
1980
+ return None
1981
+
1982
+ async def get_team_members(self, team_id: str) -> List[Dict[str, Any]]:
1983
+ """Get all team members."""
1984
+ try:
1985
+ async with self.get_session() as session:
1986
+ result = await session.execute(
1987
+ select(TeamMember).where(TeamMember.team_id == team_id)
1988
+ )
1989
+ return [
1990
+ {"id": m.id, "agent_node_id": m.agent_node_id, "agent_type": m.agent_type,
1991
+ "agent_label": m.agent_label, "role": m.role, "status": m.status}
1992
+ for m in result.scalars().all()
1993
+ ]
1994
+ except Exception as e:
1995
+ logger.error(f"Failed to get team members: {e}")
1996
+ return []
1997
+
1998
+ async def update_member_status(self, team_id: str, agent_node_id: str, status: str) -> bool:
1999
+ """Update member status (idle, working, offline)."""
2000
+ try:
2001
+ async with self.get_session() as session:
2002
+ result = await session.execute(
2003
+ select(TeamMember).where(
2004
+ TeamMember.team_id == team_id, TeamMember.agent_node_id == agent_node_id
2005
+ )
2006
+ )
2007
+ member = result.scalar_one_or_none()
2008
+ if not member:
2009
+ return False
2010
+ member.status = status
2011
+ await session.commit()
2012
+ return True
2013
+ except Exception as e:
2014
+ logger.error(f"Failed to update member status: {e}")
2015
+ return False
2016
+
2017
+ async def add_team_task(self, task_id: str, team_id: str, title: str, created_by: str,
2018
+ description: Optional[str] = None, priority: int = 3,
2019
+ depends_on: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
2020
+ """Add task to team's shared list."""
2021
+ try:
2022
+ async with self.get_session() as session:
2023
+ task = TeamTask(
2024
+ id=task_id, team_id=team_id, title=title, description=description,
2025
+ priority=priority, created_by=created_by,
2026
+ depends_on={"task_ids": depends_on} if depends_on else None,
2027
+ created_at=datetime.now(timezone.utc)
2028
+ )
2029
+ session.add(task)
2030
+ await session.commit()
2031
+ return {"id": task.id, "title": title, "status": "pending"}
2032
+ except Exception as e:
2033
+ logger.error(f"Failed to add team task: {e}")
2034
+ return None
2035
+
2036
+ async def get_team_tasks(self, team_id: str, status: Optional[str] = None) -> List[Dict[str, Any]]:
2037
+ """Get team tasks, optionally filtered by status."""
2038
+ try:
2039
+ async with self.get_session() as session:
2040
+ query = select(TeamTask).where(TeamTask.team_id == team_id)
2041
+ if status:
2042
+ query = query.where(TeamTask.status == status)
2043
+ query = query.order_by(TeamTask.priority.asc(), TeamTask.created_at.asc())
2044
+ result = await session.execute(query)
2045
+ return [
2046
+ {"id": t.id, "title": t.title, "description": t.description, "status": t.status,
2047
+ "priority": t.priority, "assigned_to": t.assigned_to, "progress": t.progress,
2048
+ "depends_on": t.depends_on.get("task_ids", []) if t.depends_on else []}
2049
+ for t in result.scalars().all()
2050
+ ]
2051
+ except Exception as e:
2052
+ logger.error(f"Failed to get team tasks: {e}")
2053
+ return []
2054
+
2055
+ async def claim_task(self, task_id: str, agent_node_id: str) -> Optional[Dict[str, Any]]:
2056
+ """Claim a pending task. Returns None if already claimed."""
2057
+ try:
2058
+ async with self.get_session() as session:
2059
+ result = await session.execute(
2060
+ select(TeamTask).where(
2061
+ TeamTask.id == task_id, TeamTask.status == "pending", TeamTask.assigned_to.is_(None)
2062
+ )
2063
+ )
2064
+ task = result.scalar_one_or_none()
2065
+ if not task:
2066
+ return None
2067
+ task.assigned_to = agent_node_id
2068
+ task.status = "in_progress"
2069
+ task.started_at = datetime.now(timezone.utc)
2070
+ await session.commit()
2071
+ return {"id": task.id, "title": task.title, "assigned_to": agent_node_id}
2072
+ except Exception as e:
2073
+ logger.error(f"Failed to claim task: {e}")
2074
+ return None
2075
+
2076
+ async def complete_task(self, task_id: str, result_data: Optional[Dict[str, Any]] = None) -> bool:
2077
+ """Mark task as completed."""
2078
+ try:
2079
+ async with self.get_session() as session:
2080
+ result = await session.execute(select(TeamTask).where(TeamTask.id == task_id))
2081
+ task = result.scalar_one_or_none()
2082
+ if not task:
2083
+ return False
2084
+ task.status = "completed"
2085
+ task.result = result_data
2086
+ task.progress = 100
2087
+ task.completed_at = datetime.now(timezone.utc)
2088
+ await session.commit()
2089
+ return True
2090
+ except Exception as e:
2091
+ logger.error(f"Failed to complete task: {e}")
2092
+ return False
2093
+
2094
+ async def fail_task(self, task_id: str, error: str) -> bool:
2095
+ """Mark task as failed."""
2096
+ try:
2097
+ async with self.get_session() as session:
2098
+ result = await session.execute(select(TeamTask).where(TeamTask.id == task_id))
2099
+ task = result.scalar_one_or_none()
2100
+ if not task:
2101
+ return False
2102
+ task.error = error
2103
+ task.retry_count += 1
2104
+ if task.retry_count < task.max_retries:
2105
+ task.status = "pending"
2106
+ task.assigned_to = None
2107
+ else:
2108
+ task.status = "failed"
2109
+ task.completed_at = datetime.now(timezone.utc)
2110
+ await session.commit()
2111
+ return True
2112
+ except Exception as e:
2113
+ logger.error(f"Failed to fail task: {e}")
2114
+ return False
2115
+
2116
+ async def get_claimable_tasks(self, team_id: str) -> List[Dict[str, Any]]:
2117
+ """Get pending tasks with resolved dependencies."""
2118
+ try:
2119
+ async with self.get_session() as session:
2120
+ result = await session.execute(select(TeamTask).where(TeamTask.team_id == team_id))
2121
+ tasks = result.scalars().all()
2122
+ completed_ids = {t.id for t in tasks if t.status == "completed"}
2123
+ claimable = []
2124
+ for t in tasks:
2125
+ if t.status != "pending" or t.assigned_to:
2126
+ continue
2127
+ deps = t.depends_on.get("task_ids", []) if t.depends_on else []
2128
+ if all(d in completed_ids for d in deps):
2129
+ claimable.append({"id": t.id, "title": t.title, "priority": t.priority})
2130
+ return claimable
2131
+ except Exception as e:
2132
+ logger.error(f"Failed to get claimable tasks: {e}")
2133
+ return []
2134
+
2135
+ async def add_agent_message(self, team_id: str, from_agent: str, content: str,
2136
+ message_type: str = "direct", to_agent: Optional[str] = None) -> Optional[Dict[str, Any]]:
2137
+ """Add message between agents."""
2138
+ try:
2139
+ async with self.get_session() as session:
2140
+ msg = AgentMessage(
2141
+ team_id=team_id, from_agent=from_agent, to_agent=to_agent,
2142
+ message_type=message_type, content=content, created_at=datetime.now(timezone.utc)
2143
+ )
2144
+ session.add(msg)
2145
+ await session.commit()
2146
+ return {"id": msg.id, "from_agent": from_agent, "to_agent": to_agent}
2147
+ except Exception as e:
2148
+ logger.error(f"Failed to add agent message: {e}")
2149
+ return None
2150
+
2151
+ async def get_agent_messages(self, team_id: str, agent_node_id: Optional[str] = None,
2152
+ unread_only: bool = False) -> List[Dict[str, Any]]:
2153
+ """Get messages for team or specific agent."""
2154
+ try:
2155
+ async with self.get_session() as session:
2156
+ from sqlalchemy import or_
2157
+ query = select(AgentMessage).where(AgentMessage.team_id == team_id)
2158
+ if agent_node_id:
2159
+ query = query.where(or_(
2160
+ AgentMessage.to_agent == agent_node_id,
2161
+ AgentMessage.to_agent.is_(None)
2162
+ ))
2163
+ if unread_only:
2164
+ query = query.where(AgentMessage.read == False)
2165
+ query = query.order_by(AgentMessage.created_at.asc()).limit(100)
2166
+ result = await session.execute(query)
2167
+ return [
2168
+ {"id": m.id, "from_agent": m.from_agent, "to_agent": m.to_agent,
2169
+ "message_type": m.message_type, "content": m.content, "read": m.read,
2170
+ "created_at": m.created_at.isoformat() if m.created_at else None}
2171
+ for m in result.scalars().all()
2172
+ ]
2173
+ except Exception as e:
2174
+ logger.error(f"Failed to get agent messages: {e}")
2175
+ return []
2176
+
2177
+ async def mark_messages_read(self, team_id: str, agent_node_id: str) -> int:
2178
+ """Mark all messages as read for an agent."""
2179
+ try:
2180
+ async with self.get_session() as session:
2181
+ from sqlalchemy import or_
2182
+ result = await session.execute(
2183
+ select(AgentMessage).where(
2184
+ AgentMessage.team_id == team_id,
2185
+ AgentMessage.read == False,
2186
+ or_(AgentMessage.to_agent == agent_node_id, AgentMessage.to_agent.is_(None))
2187
+ )
2188
+ )
2189
+ messages = result.scalars().all()
2190
+ for m in messages:
2191
+ m.read = True
2192
+ await session.commit()
2193
+ return len(messages)
2194
+ except Exception as e:
2195
+ logger.error(f"Failed to mark messages read: {e}")
2196
+ return 0
2197
+
2198
+ async def get_team_stats(self, team_id: str) -> Dict[str, Any]:
2199
+ """Get team statistics."""
2200
+ try:
2201
+ async with self.get_session() as session:
2202
+ # Get counts in simple queries
2203
+ team_result = await session.execute(select(AgentTeam).where(AgentTeam.id == team_id))
2204
+ team = team_result.scalar_one_or_none()
2205
+ if not team:
2206
+ return {"error": "Team not found"}
2207
+
2208
+ members_result = await session.execute(select(TeamMember).where(TeamMember.team_id == team_id))
2209
+ members = members_result.scalars().all()
2210
+
2211
+ tasks_result = await session.execute(select(TeamTask).where(TeamTask.team_id == team_id))
2212
+ tasks = tasks_result.scalars().all()
2213
+
2214
+ return {
2215
+ "team_id": team_id,
2216
+ "status": team.status,
2217
+ "member_count": len(members),
2218
+ "task_total": len(tasks),
2219
+ "task_pending": sum(1 for t in tasks if t.status == "pending"),
2220
+ "task_in_progress": sum(1 for t in tasks if t.status == "in_progress"),
2221
+ "task_completed": sum(1 for t in tasks if t.status == "completed"),
2222
+ "task_failed": sum(1 for t in tasks if t.status == "failed"),
2223
+ "members": [{"id": m.agent_node_id, "type": m.agent_type, "status": m.status} for m in members],
2224
+ "active_tasks": [{"id": t.id, "title": t.title, "assigned_to": t.assigned_to, "progress": t.progress}
2225
+ for t in tasks if t.status == "in_progress"]
2226
+ }
2227
+ except Exception as e:
2228
+ logger.error(f"Failed to get team stats: {e}")
2229
+ return {"error": str(e)}
package/server/main.py CHANGED
@@ -27,7 +27,7 @@ from fastapi.responses import ORJSONResponse
27
27
  from core.container import container
28
28
  from core.config import Settings
29
29
  from core.logging import configure_logging, get_logger, setup_websocket_logging, shutdown_websocket_logging
30
- from routers import workflow, database, maps, nodejs_compat, android, websocket, webhook, auth
30
+ from routers import workflow, database, maps, nodejs_compat, android, websocket, webhook, auth, twitter
31
31
 
32
32
  # Initialize settings and logging
33
33
  settings = Settings()
@@ -61,6 +61,7 @@ async def lifespan(app: FastAPI):
61
61
  "routers.websocket",
62
62
  "routers.webhook",
63
63
  "routers.auth",
64
+ "routers.twitter",
64
65
  "middleware.auth"
65
66
  ])
66
67
 
@@ -122,6 +123,12 @@ async def lifespan(app: FastAPI):
122
123
  compaction_svc.set_ai_service(container.ai_service())
123
124
  logger.info("Compaction service initialized")
124
125
 
126
+ # Initialize agent team service
127
+ from services.agent_team import init_agent_team_service
128
+ from services.status_broadcaster import get_status_broadcaster
129
+ init_agent_team_service(container.database(), get_status_broadcaster())
130
+ logger.info("Agent team service initialized")
131
+
125
132
  # Record startup time for health reporting
126
133
  set_startup_time()
127
134
 
@@ -268,6 +275,7 @@ app.include_router(maps.router)
268
275
  app.include_router(android.router)
269
276
  app.include_router(websocket.router)
270
277
  app.include_router(webhook.router)
278
+ app.include_router(twitter.router) # Twitter/X OAuth routes
271
279
 
272
280
 
273
281
  @app.get("/health")
@@ -1,7 +1,7 @@
1
1
  """SQLModel database models and tables."""
2
2
 
3
3
  from datetime import datetime, timezone
4
- from typing import Optional, Dict, Any
4
+ from typing import Optional, Dict, Any, List
5
5
  from sqlmodel import SQLModel, Field, Column, DateTime, JSON
6
6
  from sqlalchemy import func
7
7
 
@@ -315,6 +315,11 @@ class TokenUsageMetric(SQLModel, table=True):
315
315
  iteration: int = Field(default=1)
316
316
  execution_id: Optional[str] = Field(default=None, max_length=255)
317
317
  created_at: Optional[datetime] = Field(default=None)
318
+ # Cost fields (USD)
319
+ input_cost: float = Field(default=0.0)
320
+ output_cost: float = Field(default=0.0)
321
+ cache_cost: float = Field(default=0.0)
322
+ total_cost: float = Field(default=0.0)
318
323
 
319
324
 
320
325
  class CompactionEvent(SQLModel, table=True):
@@ -356,4 +361,109 @@ class SessionTokenState(SQLModel, table=True):
356
361
  compaction_count: int = Field(default=0)
357
362
  custom_threshold: Optional[int] = Field(default=None)
358
363
  compaction_enabled: bool = Field(default=True)
359
- updated_at: Optional[datetime] = Field(default=None)
364
+ updated_at: Optional[datetime] = Field(default=None)
365
+ # Cumulative cost fields (USD)
366
+ cumulative_input_cost: float = Field(default=0.0)
367
+ cumulative_output_cost: float = Field(default=0.0)
368
+ cumulative_total_cost: float = Field(default=0.0)
369
+
370
+
371
+ # =============================================================================
372
+ # Agent Teams - Claude SDK Agent Teams Pattern
373
+ # =============================================================================
374
+
375
+
376
+ class AgentTeam(SQLModel, table=True):
377
+ """Agent team for multi-agent collaboration.
378
+
379
+ Implements Claude SDK Agent Teams pattern where a team lead coordinates
380
+ multiple teammate agents working on shared tasks.
381
+ """
382
+
383
+ __tablename__ = "agent_teams"
384
+
385
+ id: str = Field(primary_key=True, max_length=255)
386
+ workflow_id: str = Field(index=True, max_length=255)
387
+ team_lead_node_id: str = Field(max_length=255)
388
+ status: str = Field(default="active", max_length=20) # active, completed, failed, dissolved
389
+ config: Dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON))
390
+ created_at: datetime = Field(
391
+ default_factory=lambda: datetime.now(timezone.utc),
392
+ sa_column=Column(DateTime(timezone=True), server_default=func.now())
393
+ )
394
+ completed_at: Optional[datetime] = Field(default=None)
395
+
396
+
397
+ class TeamMember(SQLModel, table=True):
398
+ """Agent team membership.
399
+
400
+ Tracks which agents belong to which team and their current status.
401
+ """
402
+
403
+ __tablename__ = "team_members"
404
+
405
+ id: Optional[int] = Field(default=None, primary_key=True)
406
+ team_id: str = Field(index=True, max_length=255)
407
+ agent_node_id: str = Field(index=True, max_length=255)
408
+ agent_type: str = Field(max_length=100) # orchestrator_agent, android_agent, etc.
409
+ agent_label: Optional[str] = Field(default=None, max_length=255) # User-defined label
410
+ role: str = Field(default="teammate", max_length=20) # team_lead, teammate
411
+ status: str = Field(default="idle", max_length=20) # idle, working, offline
412
+ capabilities: Optional[Dict[str, Any]] = Field(default=None, sa_column=Column(JSON))
413
+ joined_at: datetime = Field(
414
+ default_factory=lambda: datetime.now(timezone.utc),
415
+ sa_column=Column(DateTime(timezone=True), server_default=func.now())
416
+ )
417
+
418
+
419
+ class TeamTask(SQLModel, table=True):
420
+ """Shared task in agent team task list.
421
+
422
+ Tasks are created by the team lead and claimed by teammates.
423
+ Supports dependencies between tasks.
424
+ """
425
+
426
+ __tablename__ = "team_tasks"
427
+
428
+ id: str = Field(primary_key=True, max_length=255)
429
+ team_id: str = Field(index=True, max_length=255)
430
+ title: str = Field(max_length=500)
431
+ description: Optional[str] = Field(default=None, max_length=5000)
432
+ status: str = Field(default="pending", max_length=20) # pending, in_progress, completed, failed, skipped
433
+ priority: int = Field(default=3) # 1-5, lower = higher priority
434
+ created_by: str = Field(max_length=255) # agent_node_id
435
+ assigned_to: Optional[str] = Field(default=None, max_length=255)
436
+ depends_on: Optional[Dict[str, Any]] = Field(default=None, sa_column=Column(JSON)) # List of task_ids
437
+ result: Optional[Dict[str, Any]] = Field(default=None, sa_column=Column(JSON))
438
+ error: Optional[str] = Field(default=None, max_length=2000)
439
+ retry_count: int = Field(default=0)
440
+ max_retries: int = Field(default=3)
441
+ progress: int = Field(default=0) # 0-100 percentage
442
+ created_at: datetime = Field(
443
+ default_factory=lambda: datetime.now(timezone.utc),
444
+ sa_column=Column(DateTime(timezone=True), server_default=func.now())
445
+ )
446
+ started_at: Optional[datetime] = Field(default=None)
447
+ completed_at: Optional[datetime] = Field(default=None)
448
+
449
+
450
+ class AgentMessage(SQLModel, table=True):
451
+ """Inter-agent messages within a team.
452
+
453
+ Supports direct messages between agents and broadcasts to all team members.
454
+ """
455
+
456
+ __tablename__ = "agent_messages"
457
+
458
+ id: Optional[int] = Field(default=None, primary_key=True)
459
+ team_id: str = Field(index=True, max_length=255)
460
+ from_agent: str = Field(max_length=255) # node_id
461
+ to_agent: Optional[str] = Field(default=None, max_length=255) # None = broadcast
462
+ message_type: str = Field(max_length=50) # direct, broadcast, task_assignment, task_update, task_complete
463
+ content: str = Field(max_length=10000)
464
+ extra_data: Optional[Dict[str, Any]] = Field(default=None, sa_column=Column(JSON))
465
+ read: bool = Field(default=False)
466
+ created_at: datetime = Field(
467
+ default_factory=lambda: datetime.now(timezone.utc),
468
+ sa_column=Column(DateTime(timezone=True), server_default=func.now())
469
+ )
@@ -54,6 +54,9 @@ dependencies = [
54
54
 
55
55
  # Web Search
56
56
  "ddgs>=9.0.0",
57
+
58
+ # Twitter/X API
59
+ "xdk>=0.1.0",
57
60
  ]
58
61
 
59
62
  [project.optional-dependencies]
@@ -71,6 +71,9 @@ email-validator>=2.0.0
71
71
  # Timezone support
72
72
  pytz>=2024.1
73
73
 
74
+ # Twitter/X API
75
+ xdk>=0.1.0
76
+
74
77
  # Document Processing Nodes
75
78
  beautifulsoup4>=4.12.0
76
79
  langchain-text-splitters>=0.3.0