"""Opt-in v3 tool loop. No intent routing or argument substitutions.""" import asyncio import copy import calendar as month_calendar import base64 import importlib.util import io import json import os import re import sys import time from contextlib import asynccontextmanager from dataclasses import dataclass, replace from datetime import datetime, timezone from functools import lru_cache from pathlib import Path import httpx import jsonschema from src.context_compactor import prune_multimodal_images, trim_for_context from src.agent_evidence import command_has_mutation_effect, workspace_artifact_is_usable from src.tool_capabilities import ToolEffect, ToolRunSecurityContext, capabilities_for_action from src.tool_execution import execute_tool_block from src.tool_schemas import ( function_call_to_tool_block, normalize_native_function_args, normalized_native_function_argument_error, ) from src.tool_types import ToolBlock from src.turn_contract import ( FAMILY_TOOLS, broad_web_briefing_request, required_read_operation_for_request, targets_bound_editor_request, ) from src.prompt_security import untrusted_context_message from src.model_profiles import ( is_odysseus_merged_tools_model, uses_odysseus_progressive_thinking, ) ENDPOINT_ID = 'cleanv3' MODE = 'clean_compact_v3_preview' # Native unattended workspaces routinely require several inspections followed # by several artifact writes. The interactive preview keeps its six-call # limit below; this larger budget applies only after server-side validation of # a confined native workspace. Duplicate-call suppression still bounds loops. NATIVE_TOOL_CALL_LIMIT = 32 NATIVE_ROUND_LIMIT = 64 # Interactive turns still have duplicate-call and round guards, but legitimate # multi-step work should not be cut off after only a handful of executions. # Keep browser workflows proportionally larger because navigation, inspection, # and interaction are separate observable actions. INTERACTIVE_TOOL_CALL_LIMIT = 18 INTERACTIVE_BROWSER_TOOL_CALL_LIMIT = 30 INTERACTIVE_ROUND_LIMIT = 8 NATIVE_ARTIFACT_RESEARCH_LIMIT = 12 SAME_TARGET_WRITE_LIMIT = 3 ARTIFACT_RESEARCH_TOOLS = frozenset({ 'web_search', 'web_fetch', 'private_browser', 'pdf_extract', 'youtube_tool', 'inspect_media', 'extract_text', 'transcribe_media', }) DETAILED_VIDEO_REQUEST = re.compile( r"\b(?:how\s+many|count|sequence|in\s+order|chronological|" r"timestamps?|what\s+time|at\s+what\s+time|when\s+.*(?:end|happen)|" r"first\s+.*(?:save|attempt|event)|score(?:board)?s?)\b|" r"(?:多少|几次|何时|什么时候|时间|顺序)", re.IGNORECASE, ) READ_TOOLS = frozenset({ 'manage_notes', 'manage_calendar', 'manage_memory', 'manage_skills', 'manage_tasks', 'manage_documents', 'manage_research', 'manage_contact', 'list_sessions', 'search_chats', 'list_email_accounts', 'list_emails', 'search_emails', 'read_email', 'download_attachment', 'scan_spam', 'scan_email_unsubscribes', 'manage_email_state', 'web_search', 'web_fetch', 'youtube_tool', 'search_hf_models', 'pdf_extract', 'private_browser', 'list_cookbook_servers', 'list_models', 'list_served_models', 'list_cached_models', 'list_serve_presets', 'list_downloads', 'tail_serve_output', 'extract_text', 'manage_endpoints', 'manage_mcp', 'manage_tokens', 'manage_webhooks', 'manage_settings', 'app_api', }) SAFE_WRITE_TOOLS = frozenset({ 'manage_notes', 'manage_calendar', 'manage_memory', 'manage_skills', 'manage_tasks', 'create_document', 'manage_documents', 'edit_document', 'update_document', 'suggest_document', 'draft_email', 'draft_email_reply', 'edit_image', }) EXPLICIT_EXECUTE_TOOLS = frozenset({'bash'}) SAFE_UI_TOOLS = frozenset({'ui_control'}) BROKERED_JOB_TOOLS = frozenset({'trigger_research'}) CONTRACT_REQUIRED_TOOLS = frozenset({ 'send_email', 'reply_to_email', 'create_session', 'send_to_session', 'manage_session', 'chat_with_model', 'pipeline', 'serve_preset', 'stop_served_model', 'download_model', 'ask_teacher', }) PREVIEW_TOOLS = ( READ_TOOLS | SAFE_WRITE_TOOLS | EXPLICIT_EXECUTE_TOOLS | SAFE_UI_TOOLS | BROKERED_JOB_TOOLS | CONTRACT_REQUIRED_TOOLS ) # Keep a small recovery-capable surface on every interactive compact agent # turn. Routing still adds domain tools, while policy and action guards remain # authoritative for execution. Python is deliberately excluded here because # the WebUI does not own a confined workspace. INTERACTIVE_CORE_TOOLS = frozenset({ 'web_search', 'web_fetch', 'private_browser', 'bash', 'ask_user', }) # The interactive compact-v5 surface above stays unchanged. These tools are # added only for a server-validated ``odysseus-native`` request with an active, # confined workspace. This lets the model-specific clean runtime serve native # media/artifact tasks without granting the WebUI arbitrary filesystem access. NATIVE_WORKSPACE_READ_TOOLS = frozenset({ 'inspect_media', 'extract_text', 'transcribe_media', 'read_file', 'ls', 'get_workspace', 'pdf_extract', 'glob', 'grep', }) NATIVE_WORKSPACE_WRITE_TOOLS = frozenset({'write_file', 'edit_file'}) NATIVE_WORKSPACE_EXECUTE_TOOLS = frozenset({'python'}) NATIVE_WORKSPACE_TOOLS = ( NATIVE_WORKSPACE_READ_TOOLS | NATIVE_WORKSPACE_WRITE_TOOLS | NATIVE_WORKSPACE_EXECUTE_TOOLS ) ALLOWED_EFFECTS = frozenset({ ToolEffect.READ_PUBLIC, ToolEffect.READ_PRIVATE, ToolEffect.READ_WORKSPACE, ToolEffect.BROKERED_NETWORK_READ, ToolEffect.WRITE_PRIVATE, }) SAFE_ACTIONS = { 'manage_notes': frozenset({'list', 'search', 'find', 'view', 'add', 'update', 'delete', 'toggle_item'}), 'manage_calendar': frozenset({'list_calendars', 'list_events', 'create_event', 'update_event', 'delete_event'}), 'manage_memory': frozenset({'list', 'search', 'add', 'edit', 'delete'}), 'manage_skills': frozenset({'list', 'index', 'view', 'view_ref', 'search', 'add', 'edit', 'patch', 'delete'}), 'manage_tasks': frozenset({'list', 'create', 'edit', 'delete', 'pause', 'resume'}), 'manage_documents': frozenset({'list', 'read', 'view', 'open', 'get', 'delete'}), 'manage_research': frozenset({'list', 'read', 'open', 'view', 'get'}), 'manage_contact': frozenset({'list', 'search', 'find'}), 'private_browser': frozenset({ 'open', 'read', 'snapshot', 'find', 'evaluate', 'click', 'fill', 'press', 'scroll', 'wait', 'screenshot', 'close', 'batch', }), # These UI effects are reversible. A model switch is additionally bound # below to explicit user wording; keep toggle mutation, mode changes, and # email-draft actions outside this subset. 'ui_control': frozenset({ 'open_panel', 'set_theme', 'create_theme', 'get_theme', 'get_toggles', 'switch_model', }), 'manage_endpoints': frozenset({'list'}), 'manage_mcp': frozenset({'list', 'list_tools'}), 'manage_tokens': frozenset({'list'}), 'manage_webhooks': frozenset({'list'}), 'manage_settings': frozenset({'list', 'get', 'list_tools'}), 'manage_email_state': frozenset({'list_blocked'}), 'manage_session': frozenset({ 'rename', 'archive', 'unarchive', 'delete', 'important', 'unimportant', 'truncate', 'fork', }), } def bounded_search_observation(output, budget=8000): """Share the observation budget across fetched sources, not prefix order. Search's full report repeats bodies in summaries/quotes/statistics. Retain source attribution and an excerpt of every fetched page before truncating. This is evidence selection, never answer generation. """ if len(output) <= budget: return output pattern = re.compile(r'\n(\[CONTENT(?: \d+)?\] From: [^\n]+\nTitle: [^\n]*\n-+\n)') matches = list(pattern.finditer(output)) if not matches: return output source_match = re.search(r'```sources\n.*?```', output, re.DOTALL) query_match = re.search(r'^Query: .*$', output, re.MULTILINE) prefix = '\n'.join(match.group(0) for match in (source_match, query_match) if match) suffix = '\n[Excerpts shortened across sources; use web_fetch on a source URL for full details.]' headers = [match.group(1) for match in matches] room = budget - len(prefix) - len(suffix) - sum(len(h) + 2 for h in headers) if room < 100 * len(matches): return output # Unusually large metadata: preserve existing hard cap. per_page = room // len(matches) blocks = [] for index, match in enumerate(matches): end = matches[index + 1].start() if index + 1 < len(matches) else len(output) body = output[match.end():end] body = re.split(r'\n(?:Key Points:|TL;DR:|Important Quotes:|Data / Statistics:|={20,}|