diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index 43a06031..d0aa654b 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -14,7 +14,6 @@ from backend.apps.agents.models import ( ) from backend.apps.agents.ws_manager import ws_manager from backend.apps.modes.modes import load_mode -from backend.apps.outputs.outputs import _load_all as load_all_outputs from backend.apps.settings.settings import load_settings from backend.apps.tools_lib.tools_lib import ( _load_all as load_all_tools, @@ -188,7 +187,6 @@ FULL_TOOLS = [ "EnterPlanMode", "ExitPlanMode", "EnterWorktree", "TaskOutput", "TaskStop", "CronCreate", "CronList", "CronDelete", - "RenderOutput", "InvokeAgent", "Agent", # ToolSearch is the loader the CLI uses to expose deferred tool schemas @@ -530,55 +528,6 @@ class AgentManager: + "\n" ) - def _build_outputs_context(self, active_outputs: list[str] | None = None) -> str | None: - """Outputs context for the system prompt. - - Two-mode emission gated by session.active_outputs: - - Cheap one-line index for ALL Outputs (name + id + description) - so the model can OutputSearch / OutputActivate against them. - - FULL input_schema only for the ids in active_outputs. Defaults - to empty: nothing ships full-schema until the model has - explicitly activated the Output. - - This drops typical 30-Output context from ~30KB to ~2KB at - steady state; an active Output adds ~1KB back per id. - """ - import json as _json - all_outputs = load_all_outputs() - if not all_outputs: - return None - - active_set = set(active_outputs or []) - index_lines = [] - full_schemas: list[str] = [] - for out in all_outputs: - desc = f" — {out.description}" if out.description else "" - marker = " [active]" if out.id in active_set else "" - index_lines.append(f"- `{out.id}` **{out.name}**{desc}{marker}") - if out.id in active_set: - schema_str = _json.dumps(out.input_schema, indent=2) - full_schemas.append( - f"### `{out.id}` ({out.name})\n```json\n{schema_str}\n```" - ) - - sections = [""] - sections.append( - "The following reusable View artifacts are available. The model " - "must call OutputActivate(output_id) before RenderOutput so that " - "the schema is in context — otherwise RenderOutput input_data may " - "be malformed. Activated Outputs appear under " - "below." - ) - sections.append("") - sections.extend(index_lines) - sections.append("") - if full_schemas: - sections.append("") - sections.append("") - sections.extend(full_schemas) - sections.append("") - return "\n".join(sections) - def _build_browser_context(self, dashboard_id: str | None, selected_browser_ids: list[str] | None = None) -> str | None: """Build a context block listing browser cards and delegation instructions. @@ -736,8 +685,8 @@ class AgentManager: sections.append("") return "\n".join(sections) - def _compose_system_prompt(self, default_prompt: str | None, mode_prompt: str | None, session_prompt: str | None, connected_tools_ctx: str | None = None, outputs_ctx: str | None = None, browser_ctx: str | None = None, mcp_registry_ctx: str | None = None) -> str | None: - parts = [p for p in (default_prompt, mode_prompt, session_prompt, connected_tools_ctx, mcp_registry_ctx, outputs_ctx, browser_ctx) if p] + def _compose_system_prompt(self, default_prompt: str | None, mode_prompt: str | None, session_prompt: str | None, connected_tools_ctx: str | None = None, browser_ctx: str | None = None, mcp_registry_ctx: str | None = None) -> str | None: + parts = [p for p in (default_prompt, mode_prompt, session_prompt, connected_tools_ctx, mcp_registry_ctx, browser_ctx) if p] return "\n\n".join(parts) if parts else None async def launch_agent(self, config: AgentConfig) -> AgentSession: @@ -963,7 +912,7 @@ class AgentManager: # - compact_threshold_pct (default 0.65): summarize stale tool_results # and old user/assistant pairs before the next query() call # - context_soft_cap_pct (default 0.90): pre-send hard guard. After - # compaction, if still over, LRU-trim active_outputs/active_mcps + # compaction, if still over, LRU-trim active_mcps # - >= 1.0 hits the proxy/Anthropic 200K ceiling — friendly card # surfaces from the catch-all # ------------------------------------------------------------------ @@ -1411,9 +1360,9 @@ class AgentManager: sub_session_id = uuid4().hex sub_name = agent_prompt[:50] if agent_prompt else "Sub-agent" # Subagent context isolation invariant (Phase 3, Layer P): - # children DO NOT inherit the parent's active_mcps, - # active_outputs, or compaction state. They start with the - # AgentSession defaults (empty lists). Reasoning: + # children DO NOT inherit the parent's active_mcps or + # compaction state. They start with the AgentSession + # defaults (empty lists). Reasoning: # - Security: a parent that activated Gmail shouldn't # leak Gmail tools to a subagent doing an unrelated # task. The user only approved Gmail for the parent. @@ -1441,11 +1390,10 @@ class AgentManager: ], dashboard_id=session.dashboard_id, parent_session_id=session_id, - # Explicit empty lists (matches the model defaults) so + # Explicit empty list (matches the model default) so # the invariant is visible at the spawn site rather # than relying on the field's default_factory. active_mcps=[], - active_outputs=[], ) self.sessions[sub_session_id] = sub_session await ws_manager.broadcast_global("agent:status", { @@ -1496,7 +1444,6 @@ class AgentManager: # tool-call layer instead — prompt rules are not a security # boundary. connected_tools_ctx = None - outputs_ctx = self._build_outputs_context(session.active_outputs) browser_ctx = self._build_browser_context(session.dashboard_id, selected_browser_ids=selected_browser_ids) # Reconcile active_mcps against currently-enabled tools (Phase 3). @@ -1530,7 +1477,6 @@ class AgentManager: mode_sys_prompt, session.system_prompt, connected_tools_ctx, - outputs_ctx, browser_ctx, mcp_registry_ctx, ) @@ -1630,24 +1576,6 @@ class AgentManager: "type": "stdio", } - # Outputs/Views activation gate (Phase 2). Same shape as the - # MCP meta-server but for Outputs. The model only sees a - # one-line index of available Outputs in the system prompt - # (see _build_outputs_context); to load any specific - # Output's full input_schema, it must call OutputActivate. - outputs_meta_server_path = os.path.join( - os.path.dirname(__file__), "outputs_meta_server.py" - ) - mcp_servers["openswarm-outputs-meta"] = { - "command": sys.executable, - "args": [outputs_meta_server_path], - "env": { - "OPENSWARM_PORT": os.environ.get("OPENSWARM_PORT", "8324"), - "OPENSWARM_AUTH_TOKEN": _get_auth_token3(), - "OPENSWARM_PARENT_SESSION_ID": session.id, - }, - "type": "stdio", - } # The CLI's built-in WebSearch/WebFetch wraps Anthropic's # web_search_20250305. For non-Claude primaries the CLI @@ -2260,8 +2188,8 @@ class AgentManager: # Pre-send hard guard (Phase 2). After compaction, if the # session is still over context_soft_cap_pct of the window, - # LRU-trim oldest active_outputs then active_mcps. Stops the - # 429 from ever firing on predictable overflow paths. + # LRU-trim oldest active_mcps. Stops the 429 from ever + # firing on predictable overflow paths. try: # Use the most recent measurement (the prior turn's # input_tokens) as the estimate. Conservative because the @@ -2272,9 +2200,6 @@ class AgentManager: _hard_cap = int(session.context_window * session.context_soft_cap_pct) if _est_tokens >= _hard_cap: trimmed: list[str] = [] - while _est_tokens >= _hard_cap and session.active_outputs: - trimmed.append(f"output:{session.active_outputs.pop(0)}") - _est_tokens -= 5_000 # rough per-Output schema cost while _est_tokens >= _hard_cap and len(session.active_mcps) > 1: # Keep at least one MCP active so the model can # finish whatever it was doing; trim from oldest diff --git a/backend/apps/agents/models.py b/backend/apps/agents/models.py index 8aee6b84..3c6790b8 100644 --- a/backend/apps/agents/models.py +++ b/backend/apps/agents/models.py @@ -155,12 +155,6 @@ class AgentSession(BaseModel): # filter lives at the dispatch layer (mcp_servers passed to the SDK), # not the prompt layer. active_mcps: list[str] = Field(default_factory=list) - # Output ids the model has activated this session via the - # OutputActivate meta-tool. Empty by default — _build_outputs_context - # only emits the cheap one-line index for unactivated outputs; full - # input_schema is shipped only for the ids in this list. Same gate - # pattern as active_mcps but for the Outputs/Views surface. - active_outputs: list[str] = Field(default_factory=list) # Estimated framework preamble tokens (preset + tool defs + MCP descs + # composed prompt). Subtracted from displayed input for honest "this turn" # numbers. Heuristic; clamped >= 0. @@ -177,9 +171,8 @@ class AgentSession(BaseModel): # Pre-send hard guard. Fires later than the compaction threshold — # 0.90 of 200K = 180K — to give the auto-compact path a chance to # bring the request back under the ceiling. If still over after - # compaction, LRU-trim the oldest active_outputs / active_mcps. Past - # this we surface the friendly context-overflow card instead of - # letting a 429 hit. + # compaction, LRU-trim the oldest active_mcps. Past this we surface + # the friendly context-overflow card instead of letting a 429 hit. context_soft_cap_pct: float = 0.90 context_window: int = 200_000 # How much the model should "think" before answering. Provider-agnostic diff --git a/backend/apps/agents/outputs_meta_server.py b/backend/apps/agents/outputs_meta_server.py deleted file mode 100644 index a256721d..00000000 --- a/backend/apps/agents/outputs_meta_server.py +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/env python3 -"""Stdio MCP server exposing the Outputs (Views) activation gate. - -Same shape as mcp_meta_server.py but for the Outputs surface. The model -sees a one-line index of all available Outputs in the system prompt; to -get the full input_schema for a specific Output (so it can call -RenderOutput correctly), it must call OutputActivate first. The full -schema is then injected on the next turn. - -Tools: - - OutputList: enumerate all Outputs (active + available). - - OutputSearch(query): rank by name/description match + use_count. - - OutputActivate(output_id): pin the Output's schema into context. - -Same security/anti-hallucination guarantees as mcp_meta_server: input -validated against the canonical store, unknown ids return the valid -options instead of activating. -""" - -import json -import os -import sys -import urllib.error -import urllib.request - -BACKEND_PORT = os.environ.get("OPENSWARM_PORT", "8324") -BACKEND_AUTH = os.environ.get("OPENSWARM_AUTH_TOKEN", "") -BACKEND_URL = f"http://127.0.0.1:{BACKEND_PORT}/api/outputs-meta" -PARENT_SESSION_ID = os.environ.get("OPENSWARM_PARENT_SESSION_ID", "") - - -TOOLS = [ - { - "name": "OutputList", - "description": ( - "List all reusable View artifacts (Outputs) installed on this " - "machine. Returns one entry per Output with id, name, " - "description, and activation status. The full input_schema " - "is NOT included — call OutputActivate to load it. Use this " - "for a broad survey before picking one." - ), - "inputSchema": {"type": "object", "properties": {}, "additionalProperties": False}, - }, - { - "name": "OutputSearch", - "description": ( - "Find Outputs relevant to a query. Ranks by name/description " - "match plus recent-use frequency. Returns the top matches " - "without their schemas. Call OutputActivate after picking one." - ), - "inputSchema": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Free-form description of what you want to render (e.g. 'inbox dashboard', 'sales chart').", - }, - }, - "required": ["query"], - "additionalProperties": False, - }, - }, - { - "name": "OutputActivate", - "description": ( - "Activate an Output for this session — pins its full input_schema " - "into context starting next turn so RenderOutput can validate " - "the input_data shape. Validate the id by calling OutputList or " - "OutputSearch first; invalid ids return the valid options " - "instead of activating." - ), - "inputSchema": { - "type": "object", - "properties": { - "output_id": { - "type": "string", - "description": "Output id as returned by OutputList/OutputSearch.", - }, - "reason": { - "type": "string", - "description": "One-sentence explanation of why this Output is needed for the user's task.", - }, - }, - "required": ["output_id"], - "additionalProperties": False, - }, - }, -] - - -def send_response(id_, result=None, error=None): - msg = {"jsonrpc": "2.0", "id": id_} - if error is not None: - msg["error"] = error - else: - msg["result"] = result - sys.stdout.write(json.dumps(msg) + "\n") - sys.stdout.flush() - - -def call_backend(action: str, payload: dict) -> dict: - full = {**payload, "parent_session_id": PARENT_SESSION_ID} - body = json.dumps(full).encode() - headers = {"Content-Type": "application/json"} - if BACKEND_AUTH: - headers["Authorization"] = f"Bearer {BACKEND_AUTH}" - req = urllib.request.Request( - f"{BACKEND_URL}/{action}", - data=body, - headers=headers, - method="POST", - ) - try: - with urllib.request.urlopen(req, timeout=60) as resp: - return json.loads(resp.read().decode()) - except urllib.error.HTTPError as e: - body = e.read().decode() if e.fp else str(e) - return {"error": f"HTTP {e.code}: {body}"} - except Exception as e: - return {"error": str(e)} - - -def format_outputs(outputs: list[dict], heading: str = "") -> str: - if not outputs: - return "" - lines = [] - if heading: - lines.append(heading) - for o in outputs: - oid = o.get("id", "") - name = o.get("name", "") - desc = o.get("description", "") or "no description" - status = o.get("status", "available") - used = o.get("use_count", 0) - used_hint = f" (used {used}×)" if used else "" - lines.append(f"- `{oid}` **{name}** [{status}]{used_hint} — {desc}") - return "\n".join(lines) - - -def handle_tool_call(tool_name: str, arguments: dict) -> dict: - if tool_name == "OutputList": - result = call_backend("list", {}) - if "error" in result: - return {"content": [{"type": "text", "text": f"Error: {result['error']}"}], "isError": True} - active = result.get("active", []) - available = result.get("available", []) - if not active and not available: - return {"content": [{"type": "text", "text": "No Outputs / Views are defined yet. Use the App Builder mode to create one."}]} - parts = [] - if active: - parts.append(format_outputs(active, "Active (full schema in context, RenderOutput can use these now):")) - if available: - parts.append(format_outputs(available, "Available (call OutputActivate to load schema):")) - return {"content": [{"type": "text", "text": "\n\n".join(parts)}]} - - if tool_name == "OutputSearch": - query = arguments.get("query", "") - if not query: - return {"content": [{"type": "text", "text": "Error: query is required"}], "isError": True} - result = call_backend("search", {"query": query}) - if "error" in result: - return {"content": [{"type": "text", "text": f"Error: {result['error']}"}], "isError": True} - matches = result.get("matches", []) - if not matches: - return {"content": [{"type": "text", "text": f"No Outputs matched '{query}'. Try OutputList to see everything available."}]} - body = format_outputs(matches, f"Top matches for '{query}':") - body += "\n\nNext step: call OutputActivate(output_id) to pin the schema." - return {"content": [{"type": "text", "text": body}]} - - if tool_name == "OutputActivate": - output_id = arguments.get("output_id", "") - reason = arguments.get("reason", "") - if not output_id: - return {"content": [{"type": "text", "text": "Error: output_id is required"}], "isError": True} - result = call_backend("activate", {"output_id": output_id, "reason": reason}) - if "error" in result: - return {"content": [{"type": "text", "text": f"Error: {result['error']}"}], "isError": True} - if result.get("status") == "unknown_output": - available = result.get("available", []) - return { - "content": [{ - "type": "text", - "text": ( - f"Unknown Output id '{output_id}'. Valid options: " - + ", ".join(f"`{o}`" for o in available) - + ". Call OutputList for full descriptions." - ), - }], - "isError": True, - } - if result.get("status") == "already_active": - return {"content": [{"type": "text", "text": f"`{output_id}` is already active for this session — its schema is in context now, RenderOutput can use it."}]} - if result.get("status") == "activated": - return { - "content": [{ - "type": "text", - "text": ( - f"Activated Output `{output_id}`. Its full input_schema " - f"will be in context on the NEXT turn. End this turn now " - f"and call RenderOutput with the activated id." - ), - }], - } - return {"content": [{"type": "text", "text": f"Unexpected response: {json.dumps(result)}"}], "isError": True} - - return {"content": [{"type": "text", "text": f"Unknown tool: {tool_name}"}], "isError": True} - - -def main(): - for line in sys.stdin: - line = line.strip() - if not line: - continue - try: - msg = json.loads(line) - except json.JSONDecodeError: - continue - - method = msg.get("method") - id_ = msg.get("id") - params = msg.get("params", {}) - - if method == "initialize": - send_response(id_, { - "protocolVersion": "2024-11-05", - "capabilities": {"tools": {}}, - "serverInfo": {"name": "openswarm-outputs-meta", "version": "1.0.0"}, - }) - elif method == "notifications/initialized": - pass - elif method == "tools/list": - send_response(id_, {"tools": TOOLS}) - elif method == "tools/call": - tool_name = params.get("name", "") - arguments = params.get("arguments", {}) - try: - result = handle_tool_call(tool_name, arguments) - send_response(id_, result) - except Exception as e: - send_response(id_, error={"code": -32000, "message": str(e)}) - elif method == "resources/list": - send_response(id_, {"resources": []}) - elif method == "prompts/list": - send_response(id_, {"prompts": []}) - elif id_ is not None: - send_response(id_, error={"code": -32601, "message": f"Method not found: {method}"}) - - -if __name__ == "__main__": - main() diff --git a/backend/apps/outputs/models.py b/backend/apps/outputs/models.py index c54d8d56..540a3553 100644 --- a/backend/apps/outputs/models.py +++ b/backend/apps/outputs/models.py @@ -4,15 +4,6 @@ from uuid import uuid4 from datetime import datetime -class AutoRunConfig(BaseModel): - enabled: bool = False - prompt: str = "" - context_paths: list[dict[str, str]] = Field(default_factory=list) - forced_tools: list[dict[str, Any]] = Field(default_factory=list) - mode: str = "agent" - model: str = "sonnet" - - class Output(BaseModel): id: str = Field(default_factory=lambda: uuid4().hex) name: str @@ -25,7 +16,6 @@ class Output(BaseModel): }) files: dict[str, str] = Field(default_factory=dict) permission: str = "ask" - auto_run_config: Optional[AutoRunConfig] = None thumbnail: Optional[str] = None # Linkage so reopening the App Builder reattaches to the in-progress session # and reuses the same on-disk workspace folder instead of seeding a fresh one @@ -34,11 +24,6 @@ class Output(BaseModel): workspace_id: Optional[str] = None created_at: str = Field(default_factory=lambda: datetime.now().isoformat()) updated_at: str = Field(default_factory=lambda: datetime.now().isoformat()) - # Usage stats: bumped by RenderOutput dispatch + OutputActivate. Drives - # ranking in OutputSearch so frequently-used Outputs surface first. - # Both default to absent for backward compat with old on-disk records. - last_used_at: Optional[str] = None - use_count: int = 0 @model_validator(mode="before") @classmethod @@ -79,7 +64,6 @@ class OutputCreate(BaseModel): "required": [], }) files: dict[str, str] = Field(default_factory=dict) - auto_run_config: Optional[dict[str, Any]] = None thumbnail: Optional[str] = None session_id: Optional[str] = None workspace_id: Optional[str] = None @@ -111,7 +95,6 @@ class OutputUpdate(BaseModel): input_schema: Optional[dict[str, Any]] = None files: Optional[dict[str, str]] = None permission: Optional[str] = None - auto_run_config: Optional[dict[str, Any]] = None thumbnail: Optional[str] = None session_id: Optional[str] = None workspace_id: Optional[str] = None @@ -153,29 +136,6 @@ class OutputExecuteResult(BaseModel): error: Optional[str] = None -class AutoRunRequest(BaseModel): - # extra="ignore" so callers that historically sent `backend_code` (the - # field was removed for security — see auto_run_output endpoint) don't - # 422 on the way in. The endpoint silently drops it now; backend code - # only runs via the persisted-Output flow at /api/outputs/execute. - model_config = {"extra": "ignore"} - - prompt: str - input_schema: dict[str, Any] = Field(default_factory=dict) - context_paths: list[dict[str, str]] = Field(default_factory=list) - forced_tools: list[str] = Field(default_factory=list) - model: str = "sonnet" - - -class AutoRunAgentRequest(BaseModel): - prompt: str - input_schema: dict[str, Any] = Field(default_factory=dict) - output_id: str - model: str = "sonnet" - forced_tools: list[str] = Field(default_factory=list) - context_paths: list[dict[str, str]] = Field(default_factory=list) - - class WorkspaceSeedRequest(BaseModel): workspace_id: str files: Optional[dict[str, str]] = None diff --git a/backend/apps/outputs/outputs.py b/backend/apps/outputs/outputs.py index a72878fe..640da094 100644 --- a/backend/apps/outputs/outputs.py +++ b/backend/apps/outputs/outputs.py @@ -13,8 +13,7 @@ from jsonschema import validate as schema_validate, ValidationError as SchemaVal from backend.config.Apps import SubApp from backend.apps.outputs.models import ( Output, OutputCreate, OutputUpdate, OutputExecute, OutputExecuteResult, - VibeCodeRequest, AutoRunRequest, AutoRunConfig, AutoRunAgentRequest, - WorkspaceSeedRequest, + VibeCodeRequest, WorkspaceSeedRequest, ) from backend.apps.outputs.executor import execute_backend_code from backend.apps.outputs.view_builder_templates import VIEW_BUILDER_SKILL, VIEW_TEMPLATE_FILES @@ -367,7 +366,6 @@ async def create_output(body: OutputCreate): icon=body.icon, input_schema=body.input_schema, files=body.files, - auto_run_config=body.auto_run_config, thumbnail=body.thumbnail, created_at=now, updated_at=now, @@ -381,8 +379,6 @@ async def create_output(body: OutputCreate): async def update_output(output_id: str, body: OutputUpdate): output = _load(output_id) for k, v in body.model_dump(exclude_none=True).items(): - if k == "auto_run_config" and isinstance(v, dict): - v = AutoRunConfig(**v) setattr(output, k, v) output.updated_at = datetime.now().isoformat() _save(output) @@ -503,88 +499,6 @@ async def vibe_code(body: VibeCodeRequest): } -AUTO_RUN_SYSTEM_PROMPT = """\ -You generate structured JSON data matching a given schema. -The user provides a prompt describing what data to generate and a JSON Schema. -Return ONLY valid JSON that conforms to the schema. No markdown fences, no extra text, no explanation. -Every required field must be present. Use realistic, meaningful data.\ -""" - - -@outputs.router.post("/auto-run") -async def auto_run_output(body: AutoRunRequest): - """Use an LLM to generate input data matching the schema, then optionally execute backend code.""" - try: - import anthropic - except ImportError: - return {"error": "anthropic SDK not installed", "input_data": None, "backend_result": None} - - schema_str = json.dumps(body.input_schema, indent=2) - user_message = f"Schema:\n```json\n{schema_str}\n```\n\nGenerate data for: {body.prompt}" - - # Resolve body.model via the registry so non-Anthropic selections are - # routed through 9Router with the correct prefix (cx/, gc/). - # If body.model is unset or unknown, fall back to whichever aux model - # is available (prefers Claude, else any connected subscription). - from backend.apps.agents.providers.registry import ( - _find_builtin_model, - resolve_model_id_for_sdk, - resolve_aux_model, - ) - settings = load_settings() - if body.model and _find_builtin_model(body.model) is not None: - api_model = resolve_model_id_for_sdk(body.model, settings) - else: - try: - api_model, _ = await resolve_aux_model(settings, preferred_tier="haiku") - except ValueError as e: - return {"error": str(e), "input_data": None, "backend_result": None} - - client = _get_anthropic_client(api_model) - try: - resp = await client.messages.create( - model=api_model, - max_tokens=4000, - system=AUTO_RUN_SYSTEM_PROMPT, - messages=[{"role": "user", "content": user_message}], - ) - from backend.apps.agents.agent_manager import _safe_resp_text - raw = _safe_resp_text(resp).strip() - if not raw: - return {"error": "Aux model returned no content.", "input_data": None, "backend_result": None} - if raw.startswith("```"): - raw = raw.split("\n", 1)[1] if "\n" in raw else raw[3:] - if raw.endswith("```"): - raw = raw[:-3] - - input_data = json.loads(raw) - - validation_err = _validate_against_schema(input_data, body.input_schema) - if validation_err: - return {"input_data": input_data, "backend_result": None, "error": validation_err} - - # SECURITY: this endpoint used to accept arbitrary `backend_code` in - # the request body and pass it straight to execute_backend_code — - # which is an unsandboxed `python -c` subprocess. That gave anyone - # holding the install token (which is readable by every process - # running as the same OS user, and is also handed to every agent - # subprocess via OPENSWARM_AUTH_TOKEN) a one-shot RCE primitive. - # The field is now ignored at the model layer; backend code can - # only run via /api/outputs/execute against a persisted Output. - return { - "input_data": input_data, - "backend_result": None, - "stdout": None, - "stderr": None, - "error": None, - } - except json.JSONDecodeError: - return {"error": "Failed to parse generated data as JSON", "input_data": None, "backend_result": None} - except Exception as e: - logger.exception("Auto-run failed") - return {"error": str(e), "input_data": None, "backend_result": None} - - @outputs.router.post("/execute") async def execute_output(body: OutputExecute): output = _load(body.output_id) @@ -627,72 +541,3 @@ async def execute_output(body: OutputExecute): ).model_dump() -AUTO_RUN_AGENT_SYSTEM_PROMPT = """\ -You are a data-gathering agent. Your job is to use the available tools to collect \ -real data, then render it into a structured View. - -You have access to MCP tools (e.g. Gmail, calendar, etc.) that let you fetch live data. \ -Use them as needed to fulfil the user's request. - -When you have gathered enough data, call the **RenderOutput** tool with: -- `output_id`: `{output_id}` -- `input_data`: a JSON object conforming to this schema: -```json -{schema} -``` - -Do NOT fabricate data. Use the tools to get real information, then structure it to match \ -the schema above. If a tool call fails, report the error clearly.\ -""" - - -@outputs.router.post("/auto-run-agent") -async def auto_run_agent(body: AutoRunAgentRequest): - """Launch a temporary agent session that uses MCP tools to gather data for a view.""" - from backend.apps.agents.agent_manager import agent_manager, FULL_TOOLS - from backend.apps.agents.models import AgentConfig - - output = _load(body.output_id) - schema_str = json.dumps(body.input_schema or output.input_schema, indent=2) - - system_prompt = AUTO_RUN_AGENT_SYSTEM_PROMPT.format( - output_id=body.output_id, - schema=schema_str, - ) - - allowed_tools = list(FULL_TOOLS) - for tool_name in body.forced_tools: - if tool_name not in allowed_tools: - allowed_tools.append(tool_name) - - config = AgentConfig( - name=f"AutoRun: {output.name}", - model=body.model, - mode="agent", - system_prompt=system_prompt, - allowed_tools=allowed_tools, - max_turns=20, - ) - - session = await agent_manager.launch_agent(config) - - await agent_manager.send_message( - session.id, - body.prompt, - context_paths=body.context_paths if body.context_paths else None, - forced_tools=body.forced_tools if body.forced_tools else None, - ) - - return {"session_id": session.id} - - -@outputs.router.delete("/auto-run-agent/{session_id}") -async def cleanup_auto_run_agent(session_id: str): - """Delete a temporary auto-run agent session.""" - from backend.apps.agents.agent_manager import agent_manager - - try: - await agent_manager.delete_session(session_id) - except Exception as e: - logger.warning(f"Auto-run agent cleanup failed for {session_id}: {e}") - return {"ok": True} diff --git a/backend/apps/tools_lib/models.py b/backend/apps/tools_lib/models.py index 0f2f66ad..f4118e65 100644 --- a/backend/apps/tools_lib/models.py +++ b/backend/apps/tools_lib/models.py @@ -33,7 +33,6 @@ BUILTIN_TOOLS: list[BuiltinTool] = [ BuiltinTool(name="CronCreate", description="Create a scheduled or recurring task", category="scheduling", deferred=True), BuiltinTool(name="CronList", description="List all scheduled tasks", category="scheduling", deferred=True), BuiltinTool(name="CronDelete", description="Delete a scheduled task", category="scheduling", deferred=True), - BuiltinTool(name="RenderOutput", description="Render a reusable View artifact with structured input data", category="views", deferred=True), # Agent tools BuiltinTool(name="Agent", display_name="CreateAgent", description="Spawn a sub-agent to handle a complex subtask", category="agents"), BuiltinTool(name="InvokeAgent", description="Invoke a copy of an existing agent with a new message, preserving full conversation context", category="agents"), diff --git a/backend/main.py b/backend/main.py index bc536a8f..3f508455 100644 --- a/backend/main.py +++ b/backend/main.py @@ -608,7 +608,7 @@ async def session_clear(session_id: str): Preserves session.messages (so the chat UI keeps the visible history) but clears the SDK-side conversation by minting a new sdk_session_id. - Also drops active_mcps/active_outputs so the user starts fresh. + Also drops active_mcps so the user starts fresh. """ from backend.apps.agents.agent_manager import agent_manager from backend.apps.agents.ws_manager import ws_manager as _ws @@ -617,7 +617,6 @@ async def session_clear(session_id: str): return JSONResponse({"error": "session not found"}, status_code=404) session.sdk_session_id = None session.active_mcps = [] - session.active_outputs = [] session.compacted_through_msg_id = None session.tokens = {"input": 0, "output": 0} session.cost_usd = 0.0 @@ -634,115 +633,6 @@ async def session_clear(session_id: str): return JSONResponse({"cleared": True}) -@app.post("/api/outputs-meta/{action}") -async def outputs_meta(action: str, request: Request): - """Back the openswarm-outputs-meta stdio MCP server. - - Mirrors mcp_meta but for Outputs/Views: list/search/activate against - the canonical Output store. Anti-hallucination: unknown ids return - the valid options instead of activating. Schema spilled into context - on next turn via session.active_outputs (consumed by - _build_outputs_context in agent_manager). - """ - from datetime import datetime as _dt - from backend.apps.agents.agent_manager import agent_manager - from backend.apps.outputs.outputs import _load_all as load_all_outputs, _save as save_output - - body = await request.json() - parent_session_id = body.get("parent_session_id", "") - - def _all_outputs_ranked() -> list[dict]: - items = [] - for o in load_all_outputs(): - items.append({ - "id": o.id, - "name": o.name, - "description": (o.description or "").strip(), - "use_count": int(getattr(o, "use_count", 0) or 0), - "last_used_at": getattr(o, "last_used_at", None), - }) - # Sort: most-used first, then most-recent, then alphabetical. - items.sort(key=lambda i: (-(i["use_count"] or 0), -(len(i["last_used_at"] or "")), i["name"].lower())) - return items - - if action == "list": - outputs = _all_outputs_ranked() - session = agent_manager.sessions.get(parent_session_id) if parent_session_id else None - active_set = set(session.active_outputs) if session else set() - active = [{**o, "status": "active"} for o in outputs if o["id"] in active_set] - available = [{**o, "status": "available"} for o in outputs if o["id"] not in active_set] - return JSONResponse({"active": active, "available": available}) - - if action == "search": - query = (body.get("query") or "").strip().lower() - if not query: - return JSONResponse({"matches": []}) - outputs = _all_outputs_ranked() - session = agent_manager.sessions.get(parent_session_id) if parent_session_id else None - active_set = set(session.active_outputs) if session else set() - scored: list[tuple[int, dict]] = [] - for o in outputs: - hay = f"{o['name']} {o['description']}".lower() - score = 0 - for tok in query.split(): - if tok and tok in hay: - score += 2 if tok in o["name"].lower() else 1 - # Use-count bonus so frequently-rendered Outputs surface for - # generic queries like "dashboard" / "chart". - if score: - score += min(3, (o["use_count"] or 0) // 5) - annotated = {**o, "status": "active" if o["id"] in active_set else "available"} - scored.append((score, annotated)) - scored.sort(key=lambda t: (-t[0], 0 if t[1]["status"] == "active" else 1, t[1]["name"])) - matches = [s for _, s in scored[:5]] - return JSONResponse({"matches": matches}) - - if action == "activate": - output_id = (body.get("output_id") or "").strip() - reason = body.get("reason") or "" - if not output_id: - return JSONResponse({"error": "output_id is required"}, status_code=400) - if not parent_session_id: - return JSONResponse({"error": "parent_session_id is required"}, status_code=400) - session = agent_manager.sessions.get(parent_session_id) - if not session: - return JSONResponse({"error": "session not found"}, status_code=404) - - all_outputs = load_all_outputs() - match = next((o for o in all_outputs if o.id == output_id), None) - if not match: - return JSONResponse({"status": "unknown_output", "available": [o.id for o in all_outputs]}) - - if output_id in session.active_outputs: - return JSONResponse({"status": "already_active", "output_id": output_id}) - - session.active_outputs.append(output_id) - session.needs_fork = True - # Bump usage stats. Activation is a pretty strong "model intends - # to use this" signal — even if RenderOutput isn't called, the - # Output is meaningfully in scope. last_used_at lets ranking - # surface "stuff I touched recently" to the model later. - try: - match.use_count = int(getattr(match, "use_count", 0) or 0) + 1 - match.last_used_at = _dt.now().isoformat() - save_output(match) - except Exception: - logger.exception("Failed to bump Output usage stats") - try: - from backend.apps.agents.ws_manager import ws_manager as _ws - await _ws.send_to_session(parent_session_id, "agent:status", { - "session_id": parent_session_id, - "status": session.status, - "session": session.model_dump(mode="json"), - }) - except Exception: - logger.exception("Failed to broadcast post-activate session status") - pass # Output activation captured via session dump on close - return JSONResponse({"status": "activated", "output_id": output_id}) - - return JSONResponse({"error": f"unknown action: {action}"}, status_code=400) - - @app.post("/api/invoke-agent/run") async def invoke_agent_run(request: Request): """Fork an existing agent session and send it a new message. diff --git a/backend/tests/test_v2_invariants.py b/backend/tests/test_v2_invariants.py index 8038ecea..6a0f9282 100644 --- a/backend/tests/test_v2_invariants.py +++ b/backend/tests/test_v2_invariants.py @@ -653,11 +653,9 @@ def test_active_mcps_persistence_on_session(): from backend.apps.agents.models import AgentSession s = AgentSession(id="x", name="t", model="sonnet", mode="agent") s.active_mcps = ["gmail", "slack"] - s.active_outputs = ["view-1"] dumped = json.dumps(s.model_dump(mode="json")) rehydrated = AgentSession.model_validate(json.loads(dumped)) assert rehydrated.active_mcps == ["gmail", "slack"] - assert rehydrated.active_outputs == ["view-1"] # =========================================================================== diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index e4c81bee..1442801a 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -12,7 +12,8 @@ React 18 + TypeScript + webpack 5 + Redux. Entry: `src/app/Main.tsx`. Dev server - **Spatial dashboard** — agents are draggable nodes on a canvas; layout + selection state lives in Redux. - **Settings draft persistence** — `AppSettings.dismissed_mcp_suggestions` is a map of MCP id → ISO timestamp; preserve this shape when modifying settings serialization. -- **Onboarding wizard** (`src/app/pages/Onboarding/`) — 8-step agentic cursor walkthrough. Cursor offsets, fit-to-view, AC popup timing, and group-meta dedup were each delicate to land; verify visually after touching this code. +- **Onboarding wizard** (`src/app/components/Onboarding/`) — 8-step agentic cursor walkthrough. Cursor offsets, fit-to-view, AC popup timing, and group-meta dedup were each delicate to land; verify visually after touching this code. Note: steps 3/5/6 launch real agent sessions that hit the cloud's analytics ingest — don't treat them as visual-only. +- **SignInGate** (`src/app/components/SignInGate.tsx`, mounted in `Main.tsx`) — first-launch gate that captures `user_id` + email via Google OAuth or email magic link, hitting the cloud's `/api/auth/{google,email}/*`. Auto-dismisses for users with a valid bearer. - **Custom providers** — `AppSettings.custom_providers: CustomProvider[]` supports any OpenAI-compatible endpoint (e.g. LM Studio). ## Conventions diff --git a/frontend/src/app/components/CommandPicker.tsx b/frontend/src/app/components/CommandPicker.tsx index d381db2b..04213763 100644 --- a/frontend/src/app/components/CommandPicker.tsx +++ b/frontend/src/app/components/CommandPicker.tsx @@ -16,7 +16,6 @@ import ViewQuiltOutlinedIcon from '@mui/icons-material/ViewQuiltOutlined'; import { useAppSelector, useAppDispatch } from '@/shared/hooks'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice'; -import { fetchOutputs } from '@/shared/state/outputsSlice'; import { fetchSkills } from '@/shared/state/skillsSlice'; const GoogleIcon: React.FC<{ sx?: object }> = ({ sx }) => ( @@ -95,21 +94,18 @@ const CommandPicker: React.FC = ({ trigger, filter, onSelect, onClose, vi const modesMap = useAppSelector((s) => s.modes.items); const builtinTools = useAppSelector((s) => s.tools.builtinTools); const customTools = useAppSelector((s) => s.tools.items); - const outputItems = useAppSelector((s) => s.outputs.items); const [selectedIndex, setSelectedIndex] = useState(0); const containerRef = useRef(null); const toolsLoaded = useAppSelector((s) => s.tools.loaded); const builtinLoaded = useAppSelector((s) => s.tools.builtinLoaded); - const outputsLoaded = useAppSelector((s) => s.outputs.loaded); const skillsLoaded = useAppSelector((s) => s.skills.loaded); useEffect(() => { if (!builtinLoaded) dispatch(fetchBuiltinTools()); if (!toolsLoaded) dispatch(fetchTools()); - if (!outputsLoaded) dispatch(fetchOutputs()); if (!skillsLoaded) dispatch(fetchSkills()); - }, [dispatch, builtinLoaded, toolsLoaded, outputsLoaded, skillsLoaded]); + }, [dispatch, builtinLoaded, toolsLoaded, skillsLoaded]); const items: CommandPickerItem[] = useMemo(() => { let all: CommandPickerItem[] = []; @@ -252,22 +248,6 @@ const CommandPicker: React.FC = ({ trigger, filter, onSelect, onClose, vi } } - for (const out of Object.values(outputItems)) { - if (out.permission === 'deny') continue; - const cmd = out.name.toLowerCase().replace(/\s+/g, '-'); - atItems.push({ - id: `view-${out.id}`, - type: 'context' as const, - category: 'Apps', - name: out.name, - description: out.description || `Render ${out.name} view`, - command: cmd, - icon: , - toolNames: ['RenderOutput'], - iconKey: 'View', - }); - } - all = atItems; } @@ -279,7 +259,7 @@ const CommandPicker: React.FC = ({ trigger, filter, onSelect, onClose, vi item.command.toLowerCase().includes(lower) || item.description.toLowerCase().includes(lower), ); - }, [trigger, skills, modesMap, builtinTools, customTools, outputItems, filter]); + }, [trigger, skills, modesMap, builtinTools, customTools, filter]); const flatItems = useMemo(() => { const result: { item: CommandPickerItem; isGroupStart: boolean; category: string }[] = []; diff --git a/frontend/src/app/components/Onboarding/selectors.ts b/frontend/src/app/components/Onboarding/selectors.ts index 659668d9..5aac73f7 100644 --- a/frontend/src/app/components/Onboarding/selectors.ts +++ b/frontend/src/app/components/Onboarding/selectors.ts @@ -71,7 +71,6 @@ export const S = { // new — apps / views page appsNewButton: 'apps-new-button', - appBuilderInput: 'app-builder-input', appBuilderSubmit: 'app-builder-submit', appCardLatest: 'app-card-latest', diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index d12b6d80..5b794940 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -618,16 +618,6 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose const sessionRunning = session?.status === 'running' || session?.status === 'waiting_approval'; const renderItems: RenderItem[] = useMemo(() => { - const isOutputCall = (m: AgentMessage) => - m.role === 'tool_call' && typeof m.content === 'object' && m.content.tool === 'RenderOutput'; - const isOutputResult = (m: AgentMessage) => { - if (m.role !== 'tool_result') return false; - try { - const parsed = typeof m.content === 'string' ? JSON.parse(m.content) : m.content; - return !!(parsed?.output_id && parsed?.frontend_code); - } catch { return false; } - }; - const items: RenderItem[] = []; let i = 0; while (i < activeBranchMessages.length) { @@ -643,15 +633,8 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose i++; } - const regular: typeof activeBranchMessages = []; - const outputItems: typeof activeBranchMessages = []; - for (const m of group) { - if (isOutputCall(m) || isOutputResult(m)) { outputItems.push(m); continue; } - regular.push(m); - } - - const calls = regular.filter((m) => m.role === 'tool_call'); - const results = regular.filter((m) => m.role === 'tool_result'); + const calls = group.filter((m) => m.role === 'tool_call'); + const results = group.filter((m) => m.role === 'tool_result'); const pairs: ToolPair[] = calls.map((call, idx) => ({ type: 'tool_pair' as const, id: `pair-${call.id}`, @@ -699,8 +682,6 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose callCount: calls.length, } satisfies ToolGroup); } - - items.push(...outputItems); } else { if (!msg.hidden) { items.push(msg); diff --git a/frontend/src/app/pages/AgentChat/ContextDrawer.tsx b/frontend/src/app/pages/AgentChat/ContextDrawer.tsx index 993d4ff0..dec2cae3 100644 --- a/frontend/src/app/pages/AgentChat/ContextDrawer.tsx +++ b/frontend/src/app/pages/AgentChat/ContextDrawer.tsx @@ -83,12 +83,6 @@ export default function ContextDrawer() { ))} -
- {((session as any).active_outputs || []).map((o: string) => ( - - ))} -
-
{session.compacted_through_msg_id diff --git a/frontend/src/app/pages/AgentChat/MessageBubble.tsx b/frontend/src/app/pages/AgentChat/MessageBubble.tsx index 50303124..47b49ce0 100644 --- a/frontend/src/app/pages/AgentChat/MessageBubble.tsx +++ b/frontend/src/app/pages/AgentChat/MessageBubble.tsx @@ -23,7 +23,6 @@ import { openSettingsModal } from '@/shared/state/settingsSlice'; import { useAppDispatch } from '@/shared/hooks'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { SKILL_COLOR } from '@/app/components/richEditorUtils'; -import ViewBubble from './ViewBubble'; import PlanPicker from '@/app/components/PlanPicker'; import { ErrorSlime } from '@/app/components/ErrorSlime'; @@ -842,26 +841,7 @@ const MessageBubble: React.FC = React.memo(({ message, editing = false, o ); } - if (role === 'tool_call') { - const toolData = typeof content === 'object' ? content : {}; - const toolInput = toolData.input || {}; - if (toolData.tool === 'RenderOutput') { - return ; - } - return null; - } - - if (role === 'tool_result') { - let parsedContent: any = null; - try { parsedContent = typeof content === 'string' ? JSON.parse(content) : content; } catch {} - if (parsedContent?.output_id && parsedContent?.frontend_code) { - return ( - - ); - } + if (role === 'tool_call' || role === 'tool_result') { return null; } diff --git a/frontend/src/app/pages/AgentChat/ViewBubble.tsx b/frontend/src/app/pages/AgentChat/ViewBubble.tsx deleted file mode 100644 index ec7b2062..00000000 --- a/frontend/src/app/pages/AgentChat/ViewBubble.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import IconButton from '@mui/material/IconButton'; -import Collapse from '@mui/material/Collapse'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import Icon from '@mui/material/Icon'; -import OpenInFullIcon from '@mui/icons-material/OpenInFull'; -import CloseIcon from '@mui/icons-material/Close'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useAppSelector } from '@/shared/hooks'; -import { SERVE_BASE } from '@/shared/state/outputsSlice'; -import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -import ViewPreview from '../Views/ViewPreview'; - -interface Props { - toolInput: Record; - toolResult?: string | Record; - isStreaming?: boolean; -} - -const ViewBubble: React.FC = ({ toolInput, toolResult, isStreaming }) => { - const c = useClaudeTokens(); - const [expanded, setExpanded] = useState(false); - const [showInputs, setShowInputs] = useState(false); - - const outputId = toolInput?.output_id; - const inputData = toolInput?.input_data || {}; - const outputsMap = useAppSelector((state) => state.outputs.items); - const output = outputId ? outputsMap[outputId] : null; - - const parsedResult = useMemo(() => { - if (!toolResult) return null; - if (typeof toolResult === 'object') return toolResult; - try { return JSON.parse(toolResult as string); } catch { return null; } - }, [toolResult]); - - const frontendCode = parsedResult?.frontend_code || (output?.files?.['index.html'] ?? '') || ''; - const backendResult = parsedResult?.backend_result || null; - const outputName = parsedResult?.output_name || output?.name || 'App'; - const outputColor = c.accent.primary; - const outputIcon = output?.icon || 'view_quilt'; - const hasPreview = !!frontendCode.trim(); - const serveUrl = outputId ? `${SERVE_BASE}/${outputId}/serve/index.html` : undefined; - const inputEntries = Object.entries(inputData); - - if (isStreaming && !hasPreview) { - return ( - - - {outputIcon} - - {outputName} - - - - Rendering… - - - - ); - } - - return ( - <> - - - {/* Header */} - - {outputIcon} - - {outputName} - - {inputEntries.length > 0 && ( - setShowInputs(!showInputs)} - sx={{ - color: c.text.tertiary, - p: 0.5, - transform: showInputs ? 'rotate(180deg)' : 'rotate(0deg)', - transition: 'transform 0.2s ease', - }} - > - - - )} - {hasPreview && ( - setExpanded(true)} - sx={{ color: c.text.tertiary, p: 0.5, '&:hover': { color: outputColor } }} - > - - - )} - - - {/* Collapsible input params */} - - - {inputEntries.map(([key, val]) => { - const display = typeof val === 'string' ? val : JSON.stringify(val); - return ( - - - {key} - - - {display.length > 120 ? display.slice(0, 120) + '…' : display} - - - ); - })} - - - - {/* Preview */} - {hasPreview && ( - - - - )} - - {parsedResult?.error && ( - - - {parsedResult.error} - - - )} - - - - {/* Fullscreen dialog */} - setExpanded(false)} - maxWidth="lg" - fullWidth - PaperProps={{ - sx: { - height: '85vh', - display: 'flex', - flexDirection: 'column', - borderRadius: '12px', - overflow: 'hidden', - }, - }} - > - - {outputIcon} - {outputName} - setExpanded(false)} size="small" sx={{ color: c.text.tertiary }}> - - - - - - - - - ); -}; - -export default ViewBubble; diff --git a/frontend/src/app/pages/Dashboard/DashboardViewCard.tsx b/frontend/src/app/pages/Dashboard/DashboardViewCard.tsx index 9bb843ce..c76fe958 100644 --- a/frontend/src/app/pages/Dashboard/DashboardViewCard.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardViewCard.tsx @@ -3,12 +3,10 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; -import CircularProgress from '@mui/material/CircularProgress'; import RefreshIcon from '@mui/icons-material/Refresh'; -import BoltIcon from '@mui/icons-material/Bolt'; import CloseIcon from '@mui/icons-material/Close'; import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded'; -import { Output, autoRunOutput, autoRunAgentOutput, executeOutput, OutputExecuteResult, getBackendCode, SERVE_BASE } from '@/shared/state/outputsSlice'; +import { Output, SERVE_BASE } from '@/shared/state/outputsSlice'; import { setViewCardPosition, setViewCardSize, removeViewCard } from '@/shared/state/dashboardLayoutSlice'; import { useAppDispatch } from '@/shared/hooks'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; @@ -71,11 +69,8 @@ const DashboardViewCard: React.FC = ({ const scrollOverlayRef = useOverlayScrollPassthrough(isSelected); const previewRef = useRef(null); - const [inputData, setInputData] = useState>(() => getDefault(output.input_schema)); - const [backendResult, setBackendResult] = useState | null>(null); - const [autoRunning, setAutoRunning] = useState(false); - - const hasAutoRun = !!(output.auto_run_config?.enabled && output.auto_run_config?.prompt); + const [inputData] = useState>(() => getDefault(output.input_schema)); + const [backendResult] = useState | null>(null); // ---- Drag via header ---- const DRAG_THRESHOLD = 3; @@ -235,70 +230,6 @@ const DashboardViewCard: React.FC = ({ previewRef.current?.reload(); }; - const handleAutoRun = async (e: React.MouseEvent) => { - e.stopPropagation(); - if (!output.auto_run_config?.prompt) return; - setAutoRunning(true); - - const config = output.auto_run_config; - const forcedToolNames = config.forced_tools?.flatMap((ft) => ft.tools) ?? []; - - try { - if (forcedToolNames.length > 0) { - const res = await dispatch(autoRunAgentOutput({ - prompt: config.prompt, - input_schema: output.input_schema, - output_id: output.id, - model: config.model, - forced_tools: forcedToolNames, - context_paths: config.context_paths, - })).unwrap(); - - // For agent-based auto-run, we execute with default input for now - // since the agent session result flow is complex for dashboard cards - const execRes = await dispatch(executeOutput({ - output_id: output.id, - input_data: inputData, - })).unwrap(); - setInputData(execRes.input_data); - setBackendResult(execRes.backend_result); - } else { - const res = await dispatch(autoRunOutput({ - prompt: config.prompt, - input_schema: output.input_schema, - context_paths: config.context_paths, - forced_tools: forcedToolNames.length > 0 ? forcedToolNames : undefined, - model: config.model, - })).unwrap(); - if (res.input_data) { - setInputData(res.input_data); - // Auto-run no longer executes backend.py inline (the server-side - // endpoint dropped that field — it was a direct RCE primitive). - // Chain a separate executeOutput against the persisted Output so - // backend code still runs for dashboards that need backend_result. - if (getBackendCode(output)) { - try { - const execRes = await dispatch(executeOutput({ - output_id: output.id, - input_data: res.input_data, - })).unwrap(); - setBackendResult(execRes.backend_result); - } catch { - // Backend execution failure shouldn't break the input render. - setBackendResult(null); - } - } else { - setBackendResult(res.backend_result); - } - } - } - } catch { - // Silently handle errors on dashboard - } finally { - setAutoRunning(false); - } - }; - const mdDx = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dx : 0; const mdDy = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dy : 0; const displayX = localResize?.x ?? localDragPos?.x ?? (cardX + mdDx); @@ -437,22 +368,6 @@ const DashboardViewCard: React.FC = ({ - {hasAutoRun && ( - - - e.stopPropagation()} - disabled={autoRunning} - sx={{ color: '#f59e0b', p: 0.5, '&:hover': { color: '#d97706' } }} - > - {autoRunning ? : } - - - - )} - = { - name: 'Jane Smith', first_name: 'Jane', last_name: 'Smith', firstName: 'Jane', lastName: 'Smith', - email: 'jane@example.com', url: 'https://example.com', website: 'https://example.com', - phone: '+1 (555) 123-4567', address: '123 Main St, Springfield', - city: 'Springfield', state: 'CA', country: 'US', zip: '90210', - title: 'Sample Title', subject: 'Hello World', message: 'This is a sample message.', - description: 'A brief description of the item.', content: 'Lorem ipsum dolor sit amet.', - username: 'janesmith', password: 'P@ssw0rd!', token: 'tok_sample_abc123', - id: 'item_001', uuid: '550e8400-e29b-41d4-a716-446655440000', - date: '2025-03-15', time: '14:30', datetime: '2025-03-15T14:30:00Z', - color: '#4a90d9', label: 'Important', tag: 'sample', category: 'general', - query: 'search term', search: 'example query', text: 'Sample text content', - path: '/home/user/file.txt', file: 'document.pdf', filename: 'report.pdf', - company: 'Acme Corp', organization: 'Acme Corp', -}; - -function stubString(key: string): string { - const lower = key.toLowerCase().replace(/[-_]/g, ''); - for (const [pattern, val] of Object.entries(STRING_STUBS)) { - if (lower === pattern.toLowerCase().replace(/[-_]/g, '') || lower.endsWith(pattern.toLowerCase().replace(/[-_]/g, ''))) { - return val; - } - } - return `sample_${key}`; -} - -function getStubbed(schema: SchemaNode, key?: string): any { - if (schema.default !== undefined) return schema.default; - if (schema.enum && schema.enum.length > 0) return schema.enum[0]; - switch (schema.type) { - case 'string': return stubString(key || 'value'); - case 'number': return 42; - case 'integer': return 7; - case 'boolean': return true; - case 'array': { - if (!schema.items) return []; - return [getStubbed(schema.items, key ? `${key}_item` : 'item')]; - } - case 'object': { - const obj: Record = {}; - if (schema.properties) { - for (const [k, v] of Object.entries(schema.properties)) { - obj[k] = getStubbed(v, k); - } - } - return obj; - } - default: return stubString(key || 'value'); - } -} - const InputSchemaForm: React.FC = ({ schema, value, onChange, label, depth = 0 }) => { const c = useClaudeTokens(); @@ -309,5 +258,5 @@ const InputSchemaForm: React.FC = ({ schema, value, onChange, label, dept ); }; -export { getDefault, getStubbed }; +export { getDefault }; export default InputSchemaForm; diff --git a/frontend/src/app/pages/Views/ViewEditor.tsx b/frontend/src/app/pages/Views/ViewEditor.tsx index 9f9c47f5..a47e509d 100644 --- a/frontend/src/app/pages/Views/ViewEditor.tsx +++ b/frontend/src/app/pages/Views/ViewEditor.tsx @@ -7,7 +7,6 @@ import TextField from '@mui/material/TextField'; import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; import Tooltip from '@mui/material/Tooltip'; -import Switch from '@mui/material/Switch'; import CircularProgress from '@mui/material/CircularProgress'; import SaveIcon from '@mui/icons-material/Save'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; @@ -20,23 +19,18 @@ import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; import FolderIcon from '@mui/icons-material/Folder'; import AddIcon from '@mui/icons-material/Add'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; -import BoltIcon from '@mui/icons-material/Bolt'; import Collapse from '@mui/material/Collapse'; import Chip from '@mui/material/Chip'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; -import { createDraftSession, removeDraftSession, fetchSession, AgentMessage } from '@/shared/state/agentsSlice'; -import { createOutput, updateOutput, Output, executeOutput, OutputExecuteResult, autoRunOutput, autoRunAgentOutput, cleanupAutoRunAgent, AutoRunConfig, SERVE_BASE } from '@/shared/state/outputsSlice'; -import { createSessionWs } from '@/shared/ws/WebSocketManager'; +import { createDraftSession, removeDraftSession, fetchSession } from '@/shared/state/agentsSlice'; +import { createOutput, updateOutput, Output, executeOutput, OutputExecuteResult, SERVE_BASE } from '@/shared/state/outputsSlice'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import AgentChat from '../AgentChat/AgentChat'; -import ChatInput, { ChatInputHandle } from '../AgentChat/ChatInput'; import RefreshIcon from '@mui/icons-material/Refresh'; import ViewPreview, { ViewPreviewHandle } from './ViewPreview'; -import InputSchemaForm, { getDefault, getStubbed } from './InputSchemaForm'; -import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'; +import { getDefault } from './InputSchemaForm'; import CodeEditor from './CodeEditor'; import { ElementSelectionProvider } from '@/app/components/ElementSelectionContext'; import { captureViewThumbnail } from './captureViewThumbnail'; @@ -107,160 +101,6 @@ function buildFileTree(filePaths: string[]): FileTreeNode[] { return root; } -interface LogEntryProps { - msg: AgentMessage; - c: ReturnType; -} - -const LogEntry: React.FC = ({ msg, c }) => { - const [open, setOpen] = useState(false); - - if (msg.role === 'user') return null; - - if (msg.role === 'assistant') { - const text = typeof msg.content === 'string' - ? msg.content - : Array.isArray(msg.content) - ? msg.content.filter((b: any) => b.type === 'text').map((b: any) => b.text).join('') - : JSON.stringify(msg.content); - if (!text.trim()) return null; - return ( - - setOpen(!open)} - sx={{ display: 'flex', alignItems: 'center', gap: 0.5, cursor: 'pointer', '&:hover': { opacity: 0.8 } }} - > - - - {text.slice(0, 120)}{text.length > 120 ? '…' : ''} - - - - - {text} - - - - ); - } - - if (msg.role === 'tool_call') { - const tc = typeof msg.content === 'object' ? msg.content as Record : {}; - return ( - - setOpen(!open)} - sx={{ display: 'flex', alignItems: 'center', gap: 0.5, cursor: 'pointer', '&:hover': { opacity: 0.8 } }} - > - - - {tc.input && ( - - {JSON.stringify(tc.input).slice(0, 80)}… - - )} - - - - - {JSON.stringify(tc.input, null, 2)} - - - - - ); - } - - if (msg.role === 'tool_result') { - const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content); - return ( - - setOpen(!open)} - sx={{ display: 'flex', alignItems: 'center', gap: 0.5, cursor: 'pointer', '&:hover': { opacity: 0.8 } }} - > - - - result ({content.length > 60 ? `${content.length} chars` : content.slice(0, 60)}) - - - - - {content} - - - - ); - } - - if (msg.role === 'system') { - const text = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content); - return ( - - {text} - - ); - } - - return null; -}; - -interface AutoRunLogProps { - messages: AgentMessage[]; - status: string | null; - logEndRef: React.RefObject; - c: ReturnType; -} - -const AutoRunLog: React.FC = ({ messages, status, logEndRef, c }) => { - const isRunning = status === 'running' || status === 'waiting_approval'; - const isDone = status === 'completed' || status === 'stopped'; - const isError = status === 'error'; - - return ( - - - {isRunning && } - {isDone && } - {isError && } - - {isRunning ? 'Agent running…' : isDone ? 'Agent completed' : isError ? 'Agent error' : 'Execution log'} - - - {messages.length} messages - - - - {messages.map((msg) => ( - - ))} -
- - - ); -}; - interface ConsoleEntry { timestamp: number; inputData: Record; @@ -495,8 +335,6 @@ const ViewEditor: React.FC = ({ output, onClose }) => { const TAB_PREVIEW = 0; const TAB_CODE = 1; - const TAB_TEST_INPUT = 2; - const TAB_AUTO_RUN = 3; const TAB_CONSOLE = 4; const [activeTab, setActiveTab] = useState(TAB_PREVIEW); @@ -523,25 +361,8 @@ const ViewEditor: React.FC = ({ output, onClose }) => { const [consoleEntry, setConsoleEntry] = useState(null); const [hasNewConsoleOutput, setHasNewConsoleOutput] = useState(false); - const savedAutoRun = output?.auto_run_config; - const [autoRunEnabled, setAutoRunEnabled] = useState(savedAutoRun?.enabled ?? false); - const [autoRunMode, setAutoRunMode] = useState(savedAutoRun?.mode ?? 'agent'); - const [autoRunModel, setAutoRunModel] = useState(savedAutoRun?.model ?? 'sonnet'); - const [autoRunning, setAutoRunning] = useState(false); - const autoRunInputRef = useRef(null); - const autoRunInitialized = useRef(false); const previewRef = useRef(null); - const [autoRunSessionId, setAutoRunSessionId] = useState(null); - const autoRunWsRef = useRef | null>(null); - const autoRunLogEndRef = useRef(null); - - const autoRunSession = useAppSelector((state) => - autoRunSessionId ? state.agents.sessions[autoRunSessionId] : null - ); - const autoRunMessages = autoRunSession?.messages ?? []; - const autoRunSessionStatus = autoRunSession?.status ?? null; - const SIDEBAR_MIN = 280; const SIDEBAR_MAX = 800; const [sidebarWidth, setSidebarWidth] = useState(420); @@ -798,41 +619,10 @@ const ViewEditor: React.FC = ({ output, onClose }) => { try { return JSON.parse(schemaText); } catch { return { type: 'object', properties: {} }; } }, [schemaText]); - const testInputDefault = useMemo(() => getDefault(parsedSchema), [parsedSchema]); - const [testInput, setTestInput] = useState>(testInputDefault); - - useEffect(() => { - setTestInput(getDefault(parsedSchema)); - }, [schemaText]); - - useEffect(() => { - if (autoRunInitialized.current || !savedAutoRun) return; - if (!autoRunEnabled) return; - autoRunInitialized.current = true; - const timer = setTimeout(() => { - autoRunInputRef.current?.setContent( - savedAutoRun.prompt || '', - savedAutoRun.context_paths?.map((cp) => ({ path: cp.path, type: (cp.type as 'file' | 'directory') || 'file' })), - savedAutoRun.forced_tools, - ); - }, 100); - return () => clearTimeout(timer); - }, [savedAutoRun, autoRunEnabled]); + const testInput = useMemo>(() => getDefault(parsedSchema), [parsedSchema]); const savedRef = useRef(!!output); - const getAutoRunConfig = (): AutoRunConfig => { - const config = autoRunInputRef.current?.getConfig(); - return { - enabled: autoRunEnabled, - prompt: config?.prompt ?? '', - context_paths: config?.contextPaths?.map((cp) => ({ path: cp.path, type: cp.type })) ?? [], - forced_tools: (config?.forcedTools ?? []).map(({ label, tools, iconKey }) => ({ label, tools, iconKey })), - mode: autoRunMode, - model: autoRunModel, - }; - }; - const buildBody = () => { let schema: Record; try { schema = JSON.parse(schemaText); } catch { schema = { type: 'object', properties: {} }; } @@ -848,7 +638,6 @@ const ViewEditor: React.FC = ({ output, onClose }) => { icon: 'view_quilt', input_schema: schema, files: outputFiles, - auto_run_config: getAutoRunConfig(), }; }; @@ -944,137 +733,6 @@ const ViewEditor: React.FC = ({ output, onClose }) => { } }; - const handleAutoRun = async () => { - const config = autoRunInputRef.current?.getConfig(); - if (!config?.prompt?.trim()) return; - setAutoRunning(true); - - let schema: Record; - try { schema = JSON.parse(schemaText); } catch { schema = { type: 'object', properties: {} }; } - const forcedToolNames = config.forcedTools.flatMap((ft) => ft.tools); - - const eid = output?.id ?? createdIdRef.current; - if (forcedToolNames.length > 0 && eid) { - try { - const res = await dispatch(autoRunAgentOutput({ - prompt: config.prompt, - input_schema: schema, - output_id: eid, - model: autoRunModel, - forced_tools: forcedToolNames, - context_paths: config.contextPaths.map((cp) => ({ path: cp.path, type: cp.type })), - })).unwrap(); - setAutoRunSessionId(res.session_id); - const ws = createSessionWs(res.session_id); - ws.connect(); - autoRunWsRef.current = ws; - } catch { - setAutoRunning(false); - } - } else { - try { - const backendCode = files['backend.py'] ?? null; - const res = await dispatch(autoRunOutput({ - prompt: config.prompt, - input_schema: schema, - backend_code: backendCode || undefined, - context_paths: config.contextPaths.map((cp) => ({ path: cp.path, type: cp.type })), - forced_tools: forcedToolNames.length > 0 ? forcedToolNames : undefined, - model: autoRunModel, - })).unwrap(); - if (res.input_data) { - setTestInput(res.input_data); - setExecuteResult({ - output_id: output?.id ?? createdIdRef.current ?? '', - output_name: name, - frontend_code: files['index.html'] ?? '', - input_data: res.input_data, - backend_result: res.backend_result, - stdout: res.stdout ?? null, - stderr: res.stderr ?? null, - error: res.error, - }); - setConsoleEntry({ timestamp: Date.now(), inputData: res.input_data, stdout: res.stdout ?? null, stderr: res.stderr ?? null, backendResult: res.backend_result, error: res.error, source: 'auto-run' }); - setHasNewConsoleOutput(true); - setActiveTab(TAB_PREVIEW); - } - } catch {} - setAutoRunning(false); - } - }; - - useEffect(() => { - if (!autoRunSessionId || !autoRunSessionStatus) return; - if (autoRunSessionStatus !== 'completed' && autoRunSessionStatus !== 'error' && autoRunSessionStatus !== 'stopped') return; - - let extracted = false; - for (const msg of autoRunMessages) { - if (msg.role !== 'tool_call' || typeof msg.content !== 'object') continue; - const tc = msg.content as { tool?: string; input?: Record }; - if (tc.tool !== 'RenderOutput' || !tc.input?.input_data) continue; - setTestInput(tc.input.input_data); - setExecuteResult({ - output_id: output?.id ?? createdIdRef.current ?? '', - output_name: name, - frontend_code: files['index.html'] ?? '', - input_data: tc.input.input_data, - backend_result: null, - stdout: null, - stderr: null, - error: null, - }); - setConsoleEntry({ timestamp: Date.now(), inputData: tc.input.input_data, stdout: null, stderr: null, backendResult: null, error: null, source: 'agent' }); - setHasNewConsoleOutput(true); - setActiveTab(TAB_PREVIEW); - extracted = true; - break; - } - - if (!extracted && autoRunSessionStatus === 'error') { - const lastSys = [...autoRunMessages].reverse().find((m) => m.role === 'system'); - if (lastSys) { - const errMsg = typeof lastSys.content === 'string' ? lastSys.content : JSON.stringify(lastSys.content); - setExecuteResult({ - output_id: output?.id ?? createdIdRef.current ?? '', - output_name: name, - frontend_code: files['index.html'] ?? '', - input_data: {}, - backend_result: null, - stdout: null, - stderr: null, - error: errMsg, - }); - setConsoleEntry({ timestamp: Date.now(), inputData: {}, stdout: null, stderr: null, backendResult: null, error: errMsg, source: 'agent' }); - setHasNewConsoleOutput(true); - } - } - - setAutoRunning(false); - - if (autoRunWsRef.current) { - autoRunWsRef.current.disconnect(); - autoRunWsRef.current = null; - } - cleanupAutoRunAgent(autoRunSessionId).catch(() => {}); - setTimeout(() => setAutoRunSessionId(null), 300); - }, [autoRunSessionId, autoRunSessionStatus]); - - useEffect(() => { - autoRunLogEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [autoRunMessages.length]); - - useEffect(() => { - return () => { - if (autoRunWsRef.current) { - autoRunWsRef.current.disconnect(); - autoRunWsRef.current = null; - } - if (autoRunSessionId) { - cleanupAutoRunAgent(autoRunSessionId).catch(() => {}); - } - }; - }, [autoRunSessionId]); - const workspaceServeUrl = workspaceId ? `${SERVE_BASE}/workspace/${workspaceId}/serve/index.html` : undefined; @@ -1164,7 +822,7 @@ const ViewEditor: React.FC = ({ output, onClose }) => { return () => { if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); }; - }, [files, name, description, autoRunEnabled, autoRunMode, autoRunModel]); + }, [files, name, description]); useEffect(() => { return () => { @@ -1280,26 +938,6 @@ const ViewEditor: React.FC = ({ output, onClose }) => { }} /> - {autoRunEnabled && ( - - )} {saveStatus === 'unsaved' && ( Unsaved changes @@ -1371,8 +1009,6 @@ const ViewEditor: React.FC = ({ output, onClose }) => { > - - {showConsole && } {activeTab === TAB_PREVIEW && ( @@ -1566,162 +1202,9 @@ const ViewEditor: React.FC = ({ output, onClose }) => { )} - {activeTab === TAB_TEST_INPUT && ( - - - - - - - - - - )} {activeTab === TAB_CONSOLE && ( )} - - - setAutoRunEnabled(v)} - size="small" - sx={{ - '& .MuiSwitch-switchBase.Mui-checked': { color: '#f59e0b' }, - '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: '#f59e0b' }, - }} - /> - - {autoRunEnabled ? 'Auto Run enabled' : 'Auto Run disabled'} - - - {autoRunEnabled && ( - - )} - - - {autoRunEnabled ? ( - - - Describe what data to generate for this app. When triggered, an LLM will produce input data matching your schema and populate the preview. - - - {}} - mode={autoRunMode} - onModeChange={setAutoRunMode} - model={autoRunModel} - onModelChange={setAutoRunModel} - /> - - - {(autoRunSessionId || autoRunMessages.length > 0) && ( - - )} - - ) : ( - - - - Enable Auto Run to generate live data for this app - - - Configure a prompt that describes what data to generate. An LLM will produce input matching your schema and populate the preview automatically. - - - )} - - diff --git a/frontend/src/shared/state/agentsSlice.ts b/frontend/src/shared/state/agentsSlice.ts index 71cb3ea1..80cc7db1 100644 --- a/frontend/src/shared/state/agentsSlice.ts +++ b/frontend/src/shared/state/agentsSlice.ts @@ -103,7 +103,6 @@ export interface AgentSession { context_overflow?: { reason: string; message: string; at: string } | null; mcp_suggestions?: Array<{ id: string; title: string; description: string; reason?: string }>; mcp_suggestions_is_vague?: boolean; - active_outputs?: string[]; compacted_through_msg_id?: string | null; // Transient frontend-only WS connection state. Independent of // `status` (which describes the agent run itself). When the WS diff --git a/frontend/src/shared/state/outputsSlice.ts b/frontend/src/shared/state/outputsSlice.ts index 7a22c71f..5837208a 100644 --- a/frontend/src/shared/state/outputsSlice.ts +++ b/frontend/src/shared/state/outputsSlice.ts @@ -6,15 +6,6 @@ const OUTPUTS_API = `${API_BASE}/outputs`; export const SERVE_BASE = `${API_BASE}/outputs`; -export interface AutoRunConfig { - enabled: boolean; - prompt: string; - context_paths: Array<{ path: string; type: string }>; - forced_tools: Array<{ label: string; tools: string[]; iconKey?: string }>; - mode: string; - model: string; -} - export interface Output { id: string; name: string; @@ -23,7 +14,6 @@ export interface Output { input_schema: Record; files: Record; permission: string; - auto_run_config?: AutoRunConfig | null; thumbnail?: string | null; // Linkage so reopening App Builder reattaches to the in-progress session // and reuses the on-disk workspace folder instead of seeding a fresh one. @@ -135,57 +125,6 @@ export const executeOutput = createAsyncThunk( } ); -export interface AutoRunResult { - input_data: Record | null; - backend_result: Record | null; - stdout: string | null; - stderr: string | null; - error: string | null; -} - -export const autoRunOutput = createAsyncThunk( - 'outputs/autoRun', - // backend_code intentionally NOT in the request shape. The server endpoint - // ignores it now (it was an unsandboxed-RCE primitive); callers that want - // backend execution should chain executeOutput against a persisted Output. - async (body: { prompt: string; input_schema: Record; context_paths?: Array<{ path: string; type: string }>; forced_tools?: string[]; model?: string }) => { - const res = await fetch(`${OUTPUTS_API}/auto-run`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - return (await res.json()) as AutoRunResult; - } -); - -export interface AutoRunAgentResult { - session_id: string; -} - -export const autoRunAgentOutput = createAsyncThunk( - 'outputs/autoRunAgent', - async (body: { - prompt: string; - input_schema: Record; - output_id: string; - model?: string; - forced_tools?: string[]; - context_paths?: Array<{ path: string; type: string }>; - }) => { - const res = await fetch(`${OUTPUTS_API}/auto-run-agent`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - if (!res.ok) throw new Error(`Auto-run agent launch failed: ${res.status}`); - return (await res.json()) as AutoRunAgentResult; - } -); - -export async function cleanupAutoRunAgent(sessionId: string): Promise { - await fetch(`${OUTPUTS_API}/auto-run-agent/${sessionId}`, { method: 'DELETE' }); -} - const outputsSlice = createSlice({ name: 'outputs', initialState,