Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 84 additions & 137 deletions docs/06_caching.md

Large diffs are not rendered by default.

268 changes: 246 additions & 22 deletions src/askui/agent_base.py

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions src/askui/models/shared/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,14 @@ def _is_max_steps_reached(self) -> bool:

@tracer.start_as_current_span("_teardown_control_loop")
def _teardown_control_loop(self) -> None:
# Finish recording if cache_manager is active and not executing from cache
# Finish recording if cache_manager is active and not executing from cache.
# This runs in the conversation's `finally`, so any error here must not
# mask the real control-loop outcome - log and swallow instead.
if self.cache_manager is not None and not self._executed_from_cache:
self.cache_manager.finish_recording(self.get_messages())
try:
self.cache_manager.finish_recording(self.get_messages())
except Exception:
logger.exception("Failed to finish cache recording")

def _setup_speaker_handoff(self) -> None:
"""Set up speaker handoff infrastructure.
Expand Down
31 changes: 29 additions & 2 deletions src/askui/models/shared/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class CacheMetadata(BaseModel):
visual_validation: Visual validation configuration
"""

version: str = "0.2"
version: str = "0.3"
created_at: datetime
goal: str | None = None
last_executed_at: datetime | None = None
Expand Down Expand Up @@ -234,11 +234,14 @@ class CacheWritingSettings(BaseModel):
Args:
filename: Name for the cache file (auto-generated if empty)
parameter_identification_strategy: How to identify parameters("llm" or "preset")
llm_parameter_id_api_provider: API provider for LLM parameter identification
visual_verification_method: Visual hash method ("phash", "ahash", or "none")
visual_validation_region_size: Size of region to hash around coordinates
"""

# Reject unknown fields so a misplaced/misspelled setting raises instead of
# being silently ignored.
model_config = ConfigDict(extra="forbid")

filename: str = ""
parameter_identification_strategy: CACHE_PARAMETER_IDENTIFICATION_STRATEGY = "llm"
visual_verification_method: CACHING_VISUAL_VERIFICATION_METHOD = "phash"
Expand All @@ -254,6 +257,10 @@ class CacheExecutionSettings(BaseModel):
visual_validation_threshold: Max Hamming distance for validation
"""

# Reject unknown fields so a misplaced/misspelled setting raises instead of
# being silently ignored.
model_config = ConfigDict(extra="forbid")

delay_time_between_actions: float = 1.0 # keep >1s to give UI time to materialize
skip_visual_validation: bool = False
visual_validation_threshold: int = 10
Expand All @@ -273,11 +280,31 @@ class CachingSettings(BaseModel):
- "auto": Execute from cache if available, otherwise record
cache_dir (str): Directory path for storing cache files.
Default: ".askui_cache".
filename (str): Name of the trajectory/cache file for this test case
(the ".json" suffix is optional). It is used as the lookup key in
"execute"/"auto" modes (the SDK checks whether
`<cache_dir>/<filename>` exists and, if so, feeds its details to the
agent automatically) and as the target filename in "record"/"auto"
modes. If empty, no trajectory is auto-detected and recordings get an
auto-generated filename.
writing_settings: Settings for cache recording (used in "record"/"auto" modes)
execution_settings: Settings for cache playback (used in "execute"/"auto" modes)

Note:
Playback options such as the delay between replayed actions live on
`execution_settings` (a `CacheExecutionSettings`), e.g.
`CachingSettings(execution_settings=CacheExecutionSettings(
delay_time_between_actions=3.0))`. Unknown top-level fields are rejected
so a misplaced option raises instead of being silently ignored.
"""

# Reject unknown fields so a misplaced/misspelled setting (e.g. passing
# delay_time_between_actions here instead of on execution_settings) raises
# instead of being silently ignored.
model_config = ConfigDict(extra="forbid")

strategy: CACHING_STRATEGY | None = None
cache_dir: str = ".askui_cache"
filename: str = ""
writing_settings: CacheWritingSettings | None = None
execution_settings: CacheExecutionSettings | None = None
33 changes: 18 additions & 15 deletions src/askui/prompts/act_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,20 +432,20 @@

CACHE_USE_PROMPT = (
"<TRAJECTORY_USE>\n"
"CRITICAL: Before taking ANY action, you MUST first call the"
" retrieve_available_trajectories_tool to check for cached trajectories. If the"
" name of an available cached trajectory matches the one specified by the user,"
" you MUST switch to the CacheExecutor speaker using the switch_speaker tool"
" before calling any other tools!\n"
" You are only allowed to use cache files with the exact names the user allowed you"
" to use. NEVER use cache files with other names without permission, even if the"
" names are very similar!"
"CRITICAL: When a cached trajectory is available for this task, its details are"
" provided directly in the conversation inside a <CACHED_TRAJECTORY_AVAILABLE>"
" block (trajectory_file path and required parameters). Before taking ANY other"
" action, you MUST switch to the CacheExecutor speaker using the switch_speaker"
" tool with exactly that trajectory_file.\n"
" Only use the trajectory_file provided in the <CACHED_TRAJECTORY_AVAILABLE>"
" block. NEVER invent or guess other trajectory paths.\n"
" If instead you see a <NO_CACHED_TRAJECTORY> block (or no block at all), no"
" trajectory is available - proceed with manual execution.\n"
"\n"
"WORKFLOW:\n"
"1. ALWAYS start by calling retrieve_available_trajectories_tool\n"
"2. If a matching cached trajectory exists, switch to CacheExecutor using"
" the switch_speaker tool with speaker_context containing the trajectory details\n"
"3. Only proceed with manual execution if no matching trajectory is available\n"
"1. If a <CACHED_TRAJECTORY_AVAILABLE> block is present, immediately switch to"
" CacheExecutor using the switch_speaker tool with the provided trajectory_file\n"
"2. Otherwise, proceed with manual execution\n"
"\n"
"EXECUTING TRAJECTORIES:\n"
"- Use switch_speaker(speaker_name='CacheExecutor', speaker_context={"
Expand All @@ -457,6 +457,7 @@
"\n"
"DYNAMIC PARAMETERS:\n"
"- Trajectories may require parameters like {{current_date}} or {{user_name}}\n"
"- The required parameters are listed in the <CACHED_TRAJECTORY_AVAILABLE> block\n"
"- Provide values via parameter_values in the speaker_context\n"
"- Example: switch_speaker(speaker_name='CacheExecutor', speaker_context={"
"'trajectory_file': 'test.json', 'parameter_values': {"
Expand All @@ -469,11 +470,13 @@
"- Trajectory pauses at non-cacheable steps, returning NEEDS_AGENT status with"
" current step index\n"
"- Execute the non-cacheable step manually\n"
"- Resume by switching to CacheExecutor again with start_from_step_index"
" in the speaker_context\n"
"- The pause message tells you the exact start_from_step_index to resume with,"
" or states that it was the final step (in which case do NOT resume - verify"
" instead)\n"
"\n"
"CONTINUING TRAJECTORIES:\n"
"- Resume after non-cacheable steps: switch_speaker(speaker_name='CacheExecutor',"
"- Resume after non-cacheable steps only when the pause message provides a"
" start_from_step_index: switch_speaker(speaker_name='CacheExecutor',"
" speaker_context={'trajectory_file': 'test.json',"
" 'start_from_step_index': 5, 'parameter_values': {...}})\n"
"\n"
Expand Down
53 changes: 30 additions & 23 deletions src/askui/prompts/caching.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,43 @@
CACHING_PARAMETER_IDENTIFIER_SYSTEM_PROMPT = """You are analyzing UI automation \
trajectories to identify values that should be parameterized as parameters.
CACHING_PARAMETER_IDENTIFIER_SYSTEM_PROMPT = """You are reviewing a short list \
of text values that a user or agent entered during a UI automation recording. \
The recording will be REPLAYED VERBATIM on future runs. Your job is to decide \
which of these values MUST be supplied fresh on each run because replaying the \
recorded literal would be wrong.

Identify values that are likely to change between executions, such as:
- Dates and timestamps (e.g., "2025-12-11", "10:30 AM", "2025-12-11T14:30:00Z")
- Usernames, emails, names (e.g., "john.doe", "test@example.com", "John Smith")
- Session IDs, tokens, UUIDs, API keys
- Dynamic text that references current state or time-sensitive information
- File paths with user-specific or time-specific components
- Temporary or generated identifiers
Be conservative and precise. Parameterize a value ONLY if replaying the exact \
recorded literal on a later run would clearly produce a wrong, stale, or invalid \
result. When in doubt, DO NOT parameterize: a value left alone is replayed \
literally, which is the desired default. Returning an empty list is a normal and \
common answer.

DO NOT mark as parameters:
- UI element coordinates (x, y positions)
- Fixed button labels or static UI text
- Configuration values that don't change (e.g., timeouts, retry counts)
- Generic action names like "click", "type", "scroll"
- Tool names
- Boolean values or common constants
Parameterize (these are genuinely run-specific / dynamic):
- Values relative to "now": today's date, a current timestamp, "tomorrow", etc.
- One-time or generated values: session IDs, tokens, OTPs, UUIDs, verification \
codes, or order/reference numbers created during this run
- A per-run identity the caller clearly varies on purpose (e.g. the specific \
username/email being tested) - and only when it is clearly run-specific

For each parameter, provide:
1. A descriptive name in snake_case (e.g., "current_date", "user_email")
2. The actual value found in the trajectory
3. A brief description of what it represents
Do NOT parameterize (replay the literal):
- Stable text that is part of the test itself: search terms, fixed field \
contents, button/label text, messages, URLs that never change
- Values that would be identical every time this test runs
- Numbers, quantities, or short tokens unless they are clearly generated/dynamic
- Anything you are unsure about

Return your analysis as a JSON object with this structure:
For each value you DO parameterize, return:
- name: a snake_case identifier (e.g. "current_date", "otp_code")
- value: the value EXACTLY as it appears in the provided list, verbatim
- description: what it represents AND why it must change on each run

Return your analysis as a JSON object with this exact structure:
{
"parameters": [
{
"name": "current_date",
"value": "2025-12-11",
"description": "Current date in YYYY-MM-DD format"
"description": "Today's date; must be the run date, not the recorded date"
}
]
}

If no parameters are found, return an empty parameters array."""
If nothing qualifies, return {"parameters": []}."""
Loading
Loading