mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[hAIk]: refactor AgentCard and BrowserCard into subdirectory modules, plumb dashboard_id from launch through Agent/AgentSnapshot, add on_done auto-persist callback, fix get_all_sessions to use GET query params, atomic PydanticStore writes, graceful backend shutdown in local.sh, and add mode config JSONs
This commit is contained in:
@@ -48,6 +48,14 @@ AGENT_STORE: PydanticStore[Agent] = PydanticStore[Agent](
|
||||
# NOTE: Essentially the SESSIONS is a cache for active agents.
|
||||
SESSIONS: dict[str, Agent] = {}
|
||||
|
||||
def _persist_agent(agent: Agent) -> None:
|
||||
"""Called when an agent reaches a terminal state (completed/error)."""
|
||||
debug("auto-saving session %s (status=%s)", agent.session_id, agent.status)
|
||||
try:
|
||||
AGENT_STORE.save(agent)
|
||||
except Exception as e:
|
||||
debug("auto-save failed for session %s: %s", agent.session_id, e)
|
||||
|
||||
def get_agent(session_id: str) -> Agent:
|
||||
agent: Optional[Agent] = SESSIONS.get(session_id)
|
||||
if not agent:
|
||||
@@ -64,6 +72,7 @@ async def agents_lifespan():
|
||||
try:
|
||||
stored.status = "stopped"
|
||||
stored.on_event = COMMS_MANAGER.make_session_emitter(stored.session_id)
|
||||
stored.on_done = _persist_agent
|
||||
stored.toolkit = await build_agent_toolkit(
|
||||
agent=stored,
|
||||
sessions=SESSIONS,
|
||||
@@ -73,10 +82,15 @@ async def agents_lifespan():
|
||||
except Exception as e:
|
||||
debug(f"[agents lifespan] Skipping corrupt session {stored.session_id}: {e}")
|
||||
yield
|
||||
debug("agents_lifespan: shutting down — %s sessions to save", len(SESSIONS))
|
||||
for agent in list[Agent](SESSIONS.values()):
|
||||
debug("agents_lifespan: stopping agent %s", agent.session_id)
|
||||
await agent.stop_agent()
|
||||
debug("agents_lifespan: saving agent %s", agent.session_id)
|
||||
AGENT_STORE.save(agent)
|
||||
debug("agents_lifespan: saved agent %s", agent.session_id)
|
||||
SESSIONS.clear()
|
||||
debug("agents_lifespan: shutdown complete")
|
||||
|
||||
|
||||
agents = SubApp("agents", agents_lifespan)
|
||||
@@ -115,10 +129,10 @@ async def websocket_dashboard(websocket: WebSocket):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@agents.router.get("/get_all_sessions")
|
||||
async def get_all_sessions(dashboard_id: str = Body(default="")) -> dict:
|
||||
result: List[Agent] = list[Agent](SESSIONS.values())
|
||||
async def get_all_sessions(dashboard_id: str = "") -> dict:
|
||||
result: List[Agent] = list(SESSIONS.values())
|
||||
if dashboard_id:
|
||||
result: List[Agent] = [a for a in result if getattr(a, "dashboard_id", None) == dashboard_id]
|
||||
result = [a for a in result if a.dashboard_id == dashboard_id]
|
||||
return {"sessions": [a.model_dump(mode="json") for a in result]}
|
||||
|
||||
|
||||
@@ -134,14 +148,17 @@ async def launch_agent(
|
||||
mode: str = Body(),
|
||||
system_prompt: str = Body(),
|
||||
max_turns: int = Body(),
|
||||
dashboard_id: Optional[str] = Body(default=None),
|
||||
) -> dict:
|
||||
agent: Agent = Agent(
|
||||
model=model,
|
||||
mode=mode,
|
||||
status="stopped",
|
||||
dashboard_id=dashboard_id,
|
||||
config=ClaudeAgentOptions(max_turns=max_turns),
|
||||
)
|
||||
agent.on_event = COMMS_MANAGER.make_session_emitter(agent.session_id)
|
||||
agent.on_done = _persist_agent
|
||||
SESSIONS[agent.session_id] = agent
|
||||
|
||||
toolkit: Toolkit = await build_agent_toolkit(
|
||||
@@ -372,6 +389,7 @@ async def resume_session(session_id: str = Body()) -> dict:
|
||||
raise HTTPException(status_code=404, detail="Session not found in history")
|
||||
agent.status = "stopped"
|
||||
agent.on_event = COMMS_MANAGER.make_session_emitter(agent.session_id)
|
||||
agent.on_done = _persist_agent
|
||||
agent.toolkit = await build_agent_toolkit(
|
||||
agent=agent,
|
||||
sessions=SESSIONS,
|
||||
@@ -402,6 +420,7 @@ async def duplicate_session(session_id: str = Body()) -> dict:
|
||||
clone.pending_approvals = []
|
||||
clone.sub_agents = []
|
||||
clone.on_event = COMMS_MANAGER.make_session_emitter(clone.session_id)
|
||||
clone.on_done = _persist_agent
|
||||
clone.toolkit = await build_agent_toolkit(
|
||||
agent=clone,
|
||||
sessions=SESSIONS,
|
||||
|
||||
@@ -32,7 +32,8 @@ class Agent(BaseModel):
|
||||
messages: MessageLog = Field(default_factory=MessageLog)
|
||||
|
||||
session_id: str = Field(default_factory=lambda: uuid4().hex)
|
||||
config: ClaudeAgentOptions
|
||||
dashboard_id: Optional[str] = None
|
||||
config: ClaudeAgentOptions = Field(default_factory=ClaudeAgentOptions, exclude=True)
|
||||
|
||||
branch_id: str = "main"
|
||||
sub_agents: List["Agent"] = Field(default_factory=list)
|
||||
@@ -41,9 +42,10 @@ class Agent(BaseModel):
|
||||
|
||||
toolkit: Optional[Toolkit] = Field(default=None, exclude=True)
|
||||
on_event: Optional[EventCallback] = Field(default=None, exclude=True)
|
||||
on_done: Optional[Any] = Field(default=None, exclude=True)
|
||||
|
||||
task: Optional[InstanceOf[asyncio.Task]] = None
|
||||
lock: InstanceOf[asyncio.Lock] = Field(default_factory=asyncio.Lock)
|
||||
task: Optional[InstanceOf[asyncio.Task]] = Field(default=None, exclude=True)
|
||||
lock: InstanceOf[asyncio.Lock] = Field(default_factory=asyncio.Lock, exclude=True)
|
||||
|
||||
@typechecked
|
||||
def snapshot(self) -> AgentSnapshot:
|
||||
@@ -52,6 +54,7 @@ class Agent(BaseModel):
|
||||
model=self.model,
|
||||
mode=self.mode,
|
||||
status=self.status,
|
||||
dashboard_id=self.dashboard_id,
|
||||
branch_id=self.branch_id,
|
||||
parent_id=self.parent_id,
|
||||
messages=self.messages,
|
||||
@@ -70,6 +73,9 @@ class Agent(BaseModel):
|
||||
self.status = event.status # type: ignore[assignment]
|
||||
if self.on_event:
|
||||
await self.on_event(event)
|
||||
if isinstance(event, AgentStatusEvent) and event.status in ("completed", "error"):
|
||||
if self.on_done:
|
||||
self.on_done(self)
|
||||
|
||||
@typechecked
|
||||
async def request_approval(
|
||||
|
||||
@@ -6,10 +6,12 @@ inside a data directory. This module eliminates that copy-paste.
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Generic, List, Optional, TypeVar
|
||||
|
||||
from fastapi import HTTPException
|
||||
from pydantic import BaseModel
|
||||
from swarm_debug import debug
|
||||
from typeguard import typechecked
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
@@ -39,19 +41,33 @@ class PydanticStore(BaseModel, Generic[T]):
|
||||
def load_all(self) -> list[T]:
|
||||
result: List[T] = []
|
||||
if not os.path.exists(self.data_dir):
|
||||
debug("load_all: data_dir does not exist: %s", self.data_dir)
|
||||
return result
|
||||
for fname in os.listdir(self.data_dir):
|
||||
if fname.endswith(".json"):
|
||||
with open(os.path.join(self.data_dir, fname)) as f:
|
||||
result.append(self.model_cls(**json.load(f)))
|
||||
fnames = [f for f in os.listdir(self.data_dir) if f.endswith(".json")]
|
||||
debug("load_all: scanning %s — found %s json files", self.data_dir, len(fnames), table=False)
|
||||
for fname in fnames:
|
||||
path = os.path.join(self.data_dir, fname)
|
||||
size = os.path.getsize(path)
|
||||
debug("load_all: loading %s (%s bytes)", fname, size, table=False)
|
||||
with open(path) as f:
|
||||
raw = f.read()
|
||||
debug("load_all: raw content length=%s, first 100 chars: %s", len(raw), raw[:100], table=False)
|
||||
result.append(self.model_cls(**json.loads(raw)))
|
||||
return result
|
||||
|
||||
@typechecked
|
||||
def save(self, item: T) -> None:
|
||||
os.makedirs(self.data_dir, exist_ok=True)
|
||||
item_id = getattr(item, self.id_field)
|
||||
with open(self.p_path(item_id), "w") as f:
|
||||
json.dump(self.p_dump(item), f, indent=2)
|
||||
path = self.p_path(item_id)
|
||||
debug("save: writing %s to %s", item_id, path, table=False)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
"w", dir=self.data_dir, suffix=".tmp", delete=False
|
||||
) as tmp:
|
||||
json.dump(self.p_dump(item), tmp, indent=2)
|
||||
tmp_path = tmp.name
|
||||
os.replace(tmp_path, path)
|
||||
debug("save: complete — %s is now %s bytes", path, os.path.getsize(path), table=False)
|
||||
|
||||
@typechecked
|
||||
def load(self, item_id: str) -> T:
|
||||
|
||||
@@ -9,6 +9,7 @@ class AgentSnapshot(BaseModel):
|
||||
model: str
|
||||
mode: str
|
||||
status: str
|
||||
dashboard_id: Optional[str] = None
|
||||
branch_id: str = "main"
|
||||
parent_id: Optional[str] = None
|
||||
messages: MessageLog = Field(default_factory=MessageLog)
|
||||
|
||||
+5
-5
@@ -13,11 +13,11 @@ import { setCardPosition, setCardSize, fadeGlowingAgentCard, clearGlowingAgentCa
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import AgentChat from '@/app/pages/AgentChat/AgentChat';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useOverlayScrollPassthrough } from './useOverlayScrollPassthrough';
|
||||
import { type ResizeDir, DRAG_THRESHOLD, CURSOR_MAP, HANDLE_DEFS } from './cardLayoutConstants';
|
||||
import CardGlowOverlay from './CardGlowOverlay';
|
||||
import AgentCardCollapsed from './AgentCardCollapsed';
|
||||
import { formatDuration, getStatusColors, getPreviewContent } from './agentCardUtils';
|
||||
import { useOverlayScrollPassthrough } from '@/app/pages/Dashboard/useOverlayScrollPassthrough';
|
||||
import { type ResizeDir, DRAG_THRESHOLD, CURSOR_MAP, HANDLE_DEFS } from '@/app/pages/Dashboard/cardLayoutConstants';
|
||||
import CardGlowOverlay from './components/CardGlowOverlay';
|
||||
import AgentCardCollapsed from './components/AgentCardCollapsed';
|
||||
import { formatDuration, getStatusColors, getPreviewContent } from './components/agentCardUtils';
|
||||
|
||||
interface Props {
|
||||
session: AgentSession; expanded: boolean;
|
||||
+8
-7
@@ -6,14 +6,15 @@ import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useBrowserActivity } from '@/shared/useBrowserActivity';
|
||||
import { resolveInput, isGoogleSearch } from '@/shared/resolveUrl';
|
||||
import BrowserAgentOverlay from './BrowserAgentOverlay';
|
||||
import { useOverlayScrollPassthrough } from './useOverlayScrollPassthrough';
|
||||
import BrowserAgentOverlay from './components/BrowserAgentOverlay/BrowserAgentOverlay';
|
||||
import { useOverlayScrollPassthrough } from '@/app/pages/Dashboard/useOverlayScrollPassthrough';
|
||||
import { useElementSelection } from '@/app/components/ElementSelectionContext';
|
||||
import { type ResizeDir, CURSOR_MAP, HANDLE_DEFS, DRAG_THRESHOLD } from './cardLayoutConstants';
|
||||
import { useWebviewLifecycle, isElectron, chromeUserAgent, webviewPreloadPath, type TabLocalState, type WebviewElement } from './hooks/useWebviewLifecycle';
|
||||
import BrowserTabBar from './BrowserTabBar';
|
||||
import BrowserNavBar from './BrowserNavBar';
|
||||
import BrowserActionOverlay from './BrowserActionOverlay';
|
||||
import { type ResizeDir, CURSOR_MAP, HANDLE_DEFS, DRAG_THRESHOLD } from '@/app/pages/Dashboard/cardLayoutConstants';
|
||||
import { useWebviewLifecycle, isElectron, chromeUserAgent, webviewPreloadPath, type WebviewElement } from './hooks/useWebviewLifecycle';
|
||||
import type { TabLocalState } from '@/app/pages/Dashboard/types/types';
|
||||
import BrowserTabBar from './components/BrowserTabBar';
|
||||
import BrowserNavBar from './components/BrowserNavBar';
|
||||
import BrowserActionOverlay from './components/BrowserActionOverlay';
|
||||
|
||||
const MIN_W = 400, MIN_H = 300;
|
||||
|
||||
+41
-2
@@ -9,14 +9,53 @@ import OpenInFullIcon from '@mui/icons-material/OpenInFull';
|
||||
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import { AgentSession } from '@/shared/state/agentsSlice';
|
||||
import { AgentMessage, AgentSession } from '@/shared/state/agentsSlice';
|
||||
import { STOP_AGENT } from '@/shared/backend-bridge/apps/agents';
|
||||
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { summarizeMessage } from './browserAgentOverlayUtils';
|
||||
import { OverlayEntry } from './OverlayEntry';
|
||||
import OverlayActionLog from './OverlayActionLog';
|
||||
|
||||
export function summarizeMessage(msg: AgentMessage): OverlayEntry {
|
||||
if (msg.role === 'assistant' && typeof msg.content === 'string') {
|
||||
const trimmed = msg.content.trim();
|
||||
if (!trimmed) return { type: 'skip', text: '' };
|
||||
return { type: 'thought', text: trimmed };
|
||||
}
|
||||
|
||||
if (msg.role === 'tool_call') {
|
||||
const content = typeof msg.content === 'string'
|
||||
? (() => { try { return JSON.parse(msg.content); } catch { return {}; } })()
|
||||
: msg.content;
|
||||
const tool = content?.tool || content?.name || '?';
|
||||
const input = content?.input || {};
|
||||
let brief = '';
|
||||
switch (tool) {
|
||||
case 'BrowserNavigate': brief = `Navigate → ${input.url || '...'}`; break;
|
||||
case 'BrowserClick': brief = `Click ${input.selector || '...'}`; break;
|
||||
case 'BrowserType':
|
||||
brief = `Type "${(input.text || '').slice(0, 30)}${(input.text || '').length > 30 ? '…' : ''}" into ${input.selector || '...'}`;
|
||||
break;
|
||||
case 'BrowserScreenshot': brief = 'Screenshot'; break;
|
||||
case 'BrowserGetText': brief = 'Read page text'; break;
|
||||
case 'BrowserGetElements':
|
||||
brief = `Inspect elements${input.selector ? ` (${input.selector})` : ''}`;
|
||||
break;
|
||||
case 'BrowserEvaluate': brief = 'Evaluate JS'; break;
|
||||
default: brief = tool;
|
||||
}
|
||||
return { type: 'action', text: brief };
|
||||
}
|
||||
|
||||
if (msg.role === 'tool_result') {
|
||||
return { type: 'result', text: '' };
|
||||
}
|
||||
|
||||
return { type: 'skip', text: '' };
|
||||
}
|
||||
|
||||
|
||||
interface Props {
|
||||
session: AgentSession;
|
||||
browserWidth: number;
|
||||
+1
-1
@@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { OverlayEntry } from './browserAgentOverlayUtils';
|
||||
import { OverlayEntry } from './OverlayEntry';
|
||||
|
||||
interface Props {
|
||||
entries: OverlayEntry[];
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export interface OverlayEntry {
|
||||
type: 'thought' | 'action' | 'result' | 'skip';
|
||||
text: string;
|
||||
}
|
||||
+1
-1
@@ -13,7 +13,7 @@ import {
|
||||
reorderBrowserTab, setActiveBrowserTab, addBrowserTab,
|
||||
removeBrowserTab, removeBrowserCard, type BrowserTab,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { TabLocalState } from './hooks/useWebviewLifecycle';
|
||||
import type { TabLocalState } from '@/app/pages/Dashboard/types/types';
|
||||
|
||||
interface BrowserTabBarProps {
|
||||
tabs: BrowserTab[];
|
||||
+3
-8
@@ -1,8 +1,8 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import {
|
||||
registerWebview,
|
||||
unregisterWebview,
|
||||
registerWebview, // only used in this file, maybe an aNr opportunity? -HD
|
||||
unregisterWebview, // only used in this file, maybe an aNr opportunity? -HD
|
||||
setActiveTab as setRegistryActiveTab,
|
||||
type BrowserWebview,
|
||||
} from '@/shared/browserRegistry';
|
||||
@@ -12,15 +12,10 @@ import {
|
||||
updateBrowserTabFavicon,
|
||||
type BrowserTab,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { TabLocalState } from '@/app/pages/Dashboard/types/types';
|
||||
|
||||
export type WebviewElement = BrowserWebview;
|
||||
|
||||
export interface TabLocalState {
|
||||
loading: boolean;
|
||||
canGoBack: boolean;
|
||||
canGoForward: boolean;
|
||||
}
|
||||
|
||||
export const isElectron = navigator.userAgent.includes('Electron');
|
||||
|
||||
export const chromeUserAgent = navigator.userAgent
|
||||
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import AgentCard from './AgentCard';
|
||||
import AgentCard from './AgentCard/AgentCard';
|
||||
import DashboardViewCard from './DashboardViewCard';
|
||||
import BrowserCard from './BrowserCard';
|
||||
import BrowserCard from './BrowserCard/BrowserCard';
|
||||
import CanvasControls from './CanvasControls';
|
||||
import DashboardToolbar from './DashboardToolbar';
|
||||
import DashboardHeader from './DashboardHeader';
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { AgentMessage } from '@/shared/state/agentsSlice';
|
||||
|
||||
export interface OverlayEntry {
|
||||
type: 'thought' | 'action' | 'result' | 'skip';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function summarizeMessage(msg: AgentMessage): OverlayEntry {
|
||||
if (msg.role === 'assistant' && typeof msg.content === 'string') {
|
||||
const trimmed = msg.content.trim();
|
||||
if (!trimmed) return { type: 'skip', text: '' };
|
||||
return { type: 'thought', text: trimmed };
|
||||
}
|
||||
|
||||
if (msg.role === 'tool_call') {
|
||||
const content = typeof msg.content === 'string'
|
||||
? (() => { try { return JSON.parse(msg.content); } catch { return {}; } })()
|
||||
: msg.content;
|
||||
const tool = content?.tool || content?.name || '?';
|
||||
const input = content?.input || {};
|
||||
let brief = '';
|
||||
switch (tool) {
|
||||
case 'BrowserNavigate': brief = `Navigate → ${input.url || '...'}`; break;
|
||||
case 'BrowserClick': brief = `Click ${input.selector || '...'}`; break;
|
||||
case 'BrowserType':
|
||||
brief = `Type "${(input.text || '').slice(0, 30)}${(input.text || '').length > 30 ? '…' : ''}" into ${input.selector || '...'}`;
|
||||
break;
|
||||
case 'BrowserScreenshot': brief = 'Screenshot'; break;
|
||||
case 'BrowserGetText': brief = 'Read page text'; break;
|
||||
case 'BrowserGetElements':
|
||||
brief = `Inspect elements${input.selector ? ` (${input.selector})` : ''}`;
|
||||
break;
|
||||
case 'BrowserEvaluate': brief = 'Evaluate JS'; break;
|
||||
default: brief = tool;
|
||||
}
|
||||
return { type: 'action', text: brief };
|
||||
}
|
||||
|
||||
if (msg.role === 'tool_result') {
|
||||
return { type: 'result', text: '' };
|
||||
}
|
||||
|
||||
return { type: 'skip', text: '' };
|
||||
}
|
||||
@@ -101,7 +101,7 @@ export function useDashboardInit(deps: InitDeps) {
|
||||
if (!layoutInitialized) return;
|
||||
const dashboardSessionIds = Object.values(sessions)
|
||||
.filter((s: any) => s.dashboard_id === dashboardId && s.mode !== 'browser-agent' && s.mode !== 'invoked-agent' && s.mode !== 'sub-agent')
|
||||
.map((s: any) => s.id);
|
||||
.map((s: any) => s.session_id);
|
||||
const liveIds = dashboardSessionIds.sort().join(',');
|
||||
if (liveIds === prevSessionIdsRef.current) return;
|
||||
prevSessionIdsRef.current = liveIds;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface TabLocalState {
|
||||
loading: boolean;
|
||||
canGoBack: boolean;
|
||||
canGoForward: boolean;
|
||||
}
|
||||
|
||||
@@ -15,13 +15,12 @@ export const AGENTS_WS_API: string = `${API_BASE}/agents/ws`;
|
||||
|
||||
const get_all_sessions_endpoint: string = `${AGENTS_API}/get_all_sessions`;
|
||||
async function get_all_sessions_function(dashboardId?: string): Promise<AgentSession[]> {
|
||||
const res = await fetch(get_all_sessions_endpoint, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ dashboard_id: dashboardId ?? '' }),
|
||||
});
|
||||
const url = dashboardId
|
||||
? `${get_all_sessions_endpoint}?dashboard_id=${encodeURIComponent(dashboardId)}`
|
||||
: get_all_sessions_endpoint;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
return data.SESSIONS as AgentSession[];
|
||||
return data.sessions as AgentSession[];
|
||||
}
|
||||
export const GET_ALL_SESSIONS = createAsyncThunk(
|
||||
get_all_sessions_endpoint,
|
||||
@@ -53,6 +52,7 @@ async function launch_agent_function(config: {
|
||||
mode: string;
|
||||
system_prompt: string;
|
||||
max_turns: number;
|
||||
dashboard_id?: string;
|
||||
}): Promise<{ session_id: string; session: AgentSession }> {
|
||||
const res = await fetch(launch_agent_endpoint, {
|
||||
method: 'POST',
|
||||
@@ -62,6 +62,7 @@ async function launch_agent_function(config: {
|
||||
mode: config.mode,
|
||||
system_prompt: config.system_prompt,
|
||||
max_turns: config.max_turns,
|
||||
dashboard_id: config.dashboard_id,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
@@ -349,6 +350,7 @@ async function meta_launch_and_send_function(
|
||||
mode: payload.mode,
|
||||
system_prompt: payload.config.system_prompt ?? '',
|
||||
max_turns: payload.config.max_turns ?? 100,
|
||||
dashboard_id: payload.config.dashboard_id,
|
||||
});
|
||||
console.log(`[FRONTEND] meta_launch_and_send: launched | draftId=${payload.draftId} → realId=${session.session_id} status=${session.status} dashboard_id=${session.dashboard_id ?? 'NONE'}`);
|
||||
|
||||
|
||||
+5
-2
@@ -40,12 +40,15 @@ cleanup() {
|
||||
echo ""
|
||||
echo -e "${YELLOW}${BOLD}Gracefully shutting down all services...${RESET}"
|
||||
|
||||
for pid in $ELECTRON_PID $BACKEND_PID $FRONTEND_PID; do
|
||||
# Send SIGTERM to top-level PIDs only (not recursively) so uvicorn
|
||||
# can run its lifespan teardown before child processes are killed.
|
||||
for pid in $ELECTRON_PID $FRONTEND_PID; do
|
||||
[[ -n "$pid" ]] && kill_tree "$pid" TERM
|
||||
done
|
||||
[[ -n "$BACKEND_PID" ]] && kill -TERM "$BACKEND_PID" 2>/dev/null
|
||||
|
||||
local elapsed=0
|
||||
while (( elapsed < 5 )); do
|
||||
while (( elapsed < 10 )); do
|
||||
local alive=false
|
||||
for pid in $ELECTRON_PID $BACKEND_PID $FRONTEND_PID; do
|
||||
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null && alive=true
|
||||
|
||||
Reference in New Issue
Block a user