"""Run native agent turns with terminal attachments and saved conversation state.""" import copy from open_webui.models.chats import Chats from open_webui.utils.chat_id import is_saved_chat_id from open_webui.utils.terminals import get_terminal_json, upload_files_to_terminal from .session import get_session from .output import Output from .transport import TerminalTransport from . import acp, codex, claude_code, pi PROVIDERS = { 'codex': codex.run, 'claude-code': claude_code.run, 'cursor': acp.run, 'grok': acp.run, 'opencode': acp.run, 'cline': acp.run, 'gemini': acp.run, 'pi': pi.run, } async def run(request, form_data, user, metadata, model): if not is_saved_chat_id(metadata.get('chat_id')): raise ValueError('Agent conversations require a saved chat') if metadata.get('regeneration_message_id') or metadata.get('assistant_message_id'): raise ValueError('Continue an agent conversation with a new message') agent = model['agent'] if agent['provider'] not in PROVIDERS: raise ValueError('Unsupported terminal agent') chat = await Chats.get_accessible_chat_by_id(metadata['chat_id'], user, permission='write') if chat is None: raise ValueError('Chat reply access denied') parent = (metadata.get('user_message') or {}).get('parentId') previous = await get_session(chat, parent) if parent else None if previous and any(previous.get(key) != agent[key] for key in ('terminal_id', 'provider')): previous = None metadata['terminal_id'] = agent['terminal_id'] transport = await TerminalTransport.resolve(request, user, metadata) output = Output(metadata, user, transport.raise_if_failed, form_data) output.state = { **{key: previous[key] for key in ('session_id', 'model_config') if previous and key in previous}, **agent, 'terminal_user_id': (previous or {}).get('terminal_user_id') or (previous or {}).get('user') or user.id, } # Browsing files must not move an existing agent conversation's workspace. cwd = (previous or {}).get('cwd') or metadata.get('initial_terminal_cwd') if not cwd: directory = await get_terminal_json(request, user, metadata, '/files/cwd') cwd = (directory or {}).get('cwd') if not cwd: raise ValueError('Agent working directory is unavailable') output.state['cwd'] = cwd files = copy.deepcopy(form_data.get('files') or []) uploads = [f for f in files if f.get('type') != 'filesystem' and f.get('id')] await upload_files_to_terminal(request, user, uploads, metadata, {}) if any(f.get('terminal_error') for f in uploads): raise ValueError('An attachment could not be uploaded to the agent environment') paths = [f['terminal_path'] for f in uploads] for file in files: if file.get('type') == 'filesystem': if file.get('terminal_id') != agent['terminal_id']: raise ValueError('Attachment belongs to another terminal') paths.append(file['path']) messages = form_data.get('messages') or [] user_message = metadata.get('user_message') or next((m for m in reversed(messages) if m.get('role') == 'user'), {}) def text(content): return ( content if isinstance(content, str) else '\n'.join(p.get('text', '') for p in content or [] if p.get('type') == 'text') ) prompt = text(user_message.get('content', '')) if not previous or not previous.get('session_id'): history = [m for m in messages if m.get('role') in {'user', 'assistant'}] if len(history) > 1: prompt = '\n\n'.join(f'{m["role"]}: {text(m.get("content", ""))}' for m in history) if paths: prompt += '\n\nAttached files in this environment:\n' + '\n'.join(paths) images = [ p['image_url']['url'] for m in [user_message, *messages[-1:]] for p in (m.get('content') if isinstance(m.get('content'), list) else []) if p.get('type') == 'image_url' and p.get('image_url', {}).get('url', '').startswith('data:image/') ] images = list(dict.fromkeys(images)) instructions = '\n\n'.join(text(m.get('content')) for m in messages if m.get('role') == 'system') options = { 'request_id': metadata['message_id'], 'cwd': cwd, 'model': agent['model'], 'resume': (previous or {}).get('session_id'), 'instructions': instructions, } succeeded = False try: await transport.start(agent['provider'], options) extra = {'auth_method': 'cursor_login'} if agent['provider'] == 'cursor' else {} await PROVIDERS[agent['provider']]( transport, output, prompt, images, {**options, 'resume_state': previous or {}, 'params': form_data.get('params') or {}}, **extra, ) succeeded = True finally: await transport.close() for item in output.items: if item.get('status') in {'pending', 'in_progress'}: item['status'] = 'completed' if succeeded else 'incomplete' await output.flush(done=True, force=True) return {'status': True, 'chat_id': metadata['chat_id']}