mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-20 19:52:23 +02:00
77 lines
3.1 KiB
Python
77 lines
3.1 KiB
Python
"""The SDK Stop hook: an end-of-turn render gate for App Builder (view-builder) sessions.
|
|
If the live preview failed to render, it blocks the stop with the error so the agent fixes it,
|
|
up to a retry cap, then lets the turn end. Operates on the HookContext; the dict returns are
|
|
the claude_agent_sdk Stop hook protocol, not internal state."""
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Dict
|
|
|
|
from typeguard import typechecked
|
|
|
|
from backend.apps.agents.manager.streaming.HookContext import HookContext
|
|
from backend.apps.agents.manager.view_builder_state import (
|
|
VIEW_BUILDER_RENDER_MAX_RETRIES,
|
|
view_builder_render_retry_counts,
|
|
view_builder_dirty_sessions,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@typechecked
|
|
async def stop_hook(ctx: HookContext, input_data: dict, tool_use_id, context) -> Dict[str, object]:
|
|
"""End-of-turn render gate for App Builder sessions. Reads the
|
|
browser-reported render-state of the preview; if the app fails
|
|
to render, blocks with the error so the agent fixes it, up to
|
|
MAX_RETRIES then lets the stop through."""
|
|
session = ctx.session
|
|
# Gate on the DIRTY set (any app-building session that wrote a frontend file with a runtime
|
|
# attached), not the mode: onboarding builds its dashboard in a plain agent session via CreateApp,
|
|
# which must be render-gated too, not just the dedicated view-builder.
|
|
if session.id not in view_builder_dirty_sessions:
|
|
return {}
|
|
from backend.apps.outputs.runtime import (
|
|
manager as outputs_runtime_manager,
|
|
)
|
|
if outputs_runtime_manager.get(session.id) is None:
|
|
return {}
|
|
state, error_text = outputs_runtime_manager.get_render_state_for_workspace(session.id)
|
|
waited = 0.0
|
|
while state is None and waited < 5.0:
|
|
await asyncio.sleep(0.25)
|
|
waited += 0.25
|
|
state, error_text = outputs_runtime_manager.get_render_state_for_workspace(session.id)
|
|
|
|
if state != "error":
|
|
view_builder_render_retry_counts.pop(session.id, None)
|
|
view_builder_dirty_sessions.discard(session.id)
|
|
return {}
|
|
|
|
attempts = view_builder_render_retry_counts.get(session.id, 0)
|
|
if attempts >= VIEW_BUILDER_RENDER_MAX_RETRIES:
|
|
logger.warning(
|
|
"view-builder preview still failing after %s attempts for session %s; allowing stop",
|
|
attempts, session.id,
|
|
)
|
|
view_builder_render_retry_counts.pop(session.id, None)
|
|
view_builder_dirty_sessions.discard(session.id)
|
|
return {}
|
|
|
|
view_builder_render_retry_counts[session.id] = attempts + 1
|
|
logger.info(
|
|
"view-builder render block (attempt %s/%s) for session %s",
|
|
attempts + 1, VIEW_BUILDER_RENDER_MAX_RETRIES, session.id,
|
|
)
|
|
trimmed = error_text[-3000:] if len(error_text) > 3000 else error_text
|
|
return {
|
|
"decision": "block",
|
|
"reason": (
|
|
f"The preview failed to render (attempt {attempts + 1}/"
|
|
f"{VIEW_BUILDER_RENDER_MAX_RETRIES}):\n\n"
|
|
f"{trimmed}\n\n"
|
|
"Fix this so the app renders before finishing; the user "
|
|
"currently sees an error instead of the app."
|
|
),
|
|
}
|