package cc.unitmesh.agent.webagent import cc.unitmesh.agent.webagent.model.* import cc.unitmesh.devins.compiler.variable.VariableTable import cc.unitmesh.devins.compiler.variable.VariableType import kotlinx.serialization.Serializable /** * Context for Web Agent execution. * * Contains all information needed for the agent to understand the current state * and make decisions about next actions. * * @see Issue #532 */ @Serializable data class WebAgentContext( /** * The test scenario being executed */ val scenario: TestScenario, /** * Current page state */ val pageState: PageState, /** * Index of current step being executed */ val currentStepIndex: Int, /** * Results of previously executed steps */ val previousResults: List, /** * Short-term memory of recent actions and observations */ val memory: TestMemory, /** * Test variables (can be updated during execution) */ val variables: MutableMap, /** * Configuration options */ val config: WebAgentConfig ) : cc.unitmesh.agent.AgentContext { override fun toVariableTable(): VariableTable { val table = VariableTable() table.addVariable("scenarioId", VariableType.STRING, scenario.id) table.addVariable("scenarioName", VariableType.STRING, scenario.name) table.addVariable("currentUrl", VariableType.STRING, pageState.url) table.addVariable("pageTitle", VariableType.STRING, pageState.title) table.addVariable("currentStep", VariableType.NUMBER, currentStepIndex) table.addVariable("totalSteps", VariableType.NUMBER, scenario.steps.size) table.addVariable("passedSteps", VariableType.NUMBER, previousResults.count { it.success }) table.addVariable("failedSteps", VariableType.NUMBER, previousResults.count { !it.success }) table.addVariable("actionableElements", VariableType.NUMBER, pageState.actionableElements.size) variables.forEach { (key, value) -> table.addVariable(key, VariableType.STRING, value) } return table } /** * Get the current step to execute */ val currentStep: TestStep? get() = scenario.steps.getOrNull(currentStepIndex) /** * Check if all steps have been executed */ val isComplete: Boolean get() = currentStepIndex >= scenario.steps.size /** * Get remaining steps count */ val remainingSteps: Int get() = (scenario.steps.size - currentStepIndex).coerceAtLeast(0) } /** * Short-term memory for the testing agent. * * Maintains recent history to prevent loops and provide context for decisions. */ @Serializable data class TestMemory( /** * Recent actions taken (last N) */ val recentActions: List, /** * Recent page states (last N) */ val recentPageStates: List, /** * Errors encountered */ val errors: List, /** * Self-healing history */ val healingHistory: List, /** * Maximum items to keep in memory */ val maxSize: Int = 10 ) { /** * Check if we're in a potential loop (same action repeated) */ fun isInLoop(): Boolean { if (recentActions.size < 3) return false val last3 = recentActions.takeLast(3) return last3.all { it.actionType == last3.first().actionType && it.targetId == last3.first().targetId } } /** * Add an action to memory */ fun withAction(action: ActionRecord): TestMemory { val updated = (recentActions + action).takeLast(maxSize) return copy(recentActions = updated) } /** * Add a page state summary to memory */ fun withPageState(state: PageStateSummary): TestMemory { val updated = (recentPageStates + state).takeLast(maxSize) return copy(recentPageStates = updated) } /** * Add an error to memory */ fun withError(error: ErrorRecord): TestMemory { val updated = (errors + error).takeLast(maxSize) return copy(errors = updated) } /** * Add a healing record to memory */ fun withHealing(healing: HealingRecord): TestMemory { val updated = (healingHistory + healing).takeLast(maxSize) return copy(healingHistory = updated) } companion object { fun empty(maxSize: Int = 10) = TestMemory( recentActions = emptyList(), recentPageStates = emptyList(), errors = emptyList(), healingHistory = emptyList(), maxSize = maxSize ) } } @Serializable data class ActionRecord( val actionType: String, val targetId: Int?, val timestamp: Long, val success: Boolean, val description: String ) @Serializable data class PageStateSummary( val url: String, val title: String, val actionableCount: Int, val timestamp: Long ) @Serializable data class ErrorRecord( val stepId: String, val errorType: String, val message: String, val timestamp: Long ) @Serializable data class HealingRecord( val stepId: String, val originalSelector: String, val healedSelector: String, val healingLevel: HealingLevel, val timestamp: Long ) @Serializable enum class HealingLevel { /** * Algorithm-based healing (milliseconds, low cost) */ L1_ALGORITHM, /** * LLM semantic healing (seconds, high cost) */ L2_LLM_SEMANTIC } /** * Configuration for Web Agent execution */ @Serializable data class WebAgentConfig( /** * Default timeout for actions in milliseconds */ val defaultTimeoutMs: Long = 5000, /** * Default retry count for flaky steps */ val defaultRetryCount: Int = 2, /** * Enable self-healing locators */ val enableSelfHealing: Boolean = true, /** * Self-healing confidence threshold (0.0 to 1.0) */ val healingThreshold: Double = 0.8, /** * Enable L2 LLM semantic healing (more expensive) */ val enableLLMHealing: Boolean = true, /** * Capture screenshots on failure */ val screenshotOnFailure: Boolean = true, /** * Capture screenshots on each step */ val screenshotOnEachStep: Boolean = false, /** * Viewport width */ val viewportWidth: Int = 1280, /** * Viewport height */ val viewportHeight: Int = 720, /** * Headless mode (no visible browser) */ val headless: Boolean = false, /** * Slow motion delay between actions (for debugging) */ val slowMotionMs: Long = 0, /** * Maximum memory items to keep */ val maxMemorySize: Int = 10 )