Improve WebUI streaming and Python tooling
This commit is contained in:
+20
-3
@@ -22,6 +22,7 @@ class PromptContext:
|
||||
is_git_repo: bool
|
||||
is_git_worktree: bool
|
||||
scratchpad_directory: str | None = None
|
||||
python_env_directory: str | None = None
|
||||
additional_working_directories: tuple[str, ...] = ()
|
||||
user_context: dict[str, str] = field(default_factory=dict)
|
||||
system_context: dict[str, str] = field(default_factory=dict)
|
||||
@@ -56,6 +57,11 @@ def build_prompt_context(
|
||||
is_git_repo=snapshot.is_git_repo,
|
||||
is_git_worktree=snapshot.is_git_worktree,
|
||||
scratchpad_directory=snapshot.scratchpad_directory,
|
||||
python_env_directory=(
|
||||
str(runtime_config.python_env_dir.resolve())
|
||||
if runtime_config.python_env_dir is not None
|
||||
else None
|
||||
),
|
||||
additional_working_directories=snapshot.additional_working_directories,
|
||||
user_context=snapshot.user_context,
|
||||
system_context=snapshot.system_context,
|
||||
@@ -154,7 +160,9 @@ def get_doing_tasks_section() -> str:
|
||||
'除非确实需要新文件,否则优先编辑现有文件。',
|
||||
'对于文件解析、表格转换、JSON 或 JSONL 处理、日志分析、批量校验、数据抽样等任务,如果小型 Python 脚本比手工文本处理更可靠,可以编写小型 Python 脚本。',
|
||||
'小脚本应短小、可读,并明确输入和输出。需要执行 Python 代码或 Python 脚本时,使用 python_exec;不要通过 bash 执行 python、python3 或 .venv/bin/python。',
|
||||
'一次性脚本放到临时目录或任务专属目录;只有用户需要长期复用时,才加入项目代码。',
|
||||
'一次性 Python 分析优先直接传给 python_exec.code,不要为了临时分析创建项目文件。',
|
||||
'如果确实需要临时脚本、缓存或中间产物,必须写入当前会话 scratchpad 目录,或写入明确的任务产物目录;禁止在项目根目录创建 analyze_*.py、tmp_*.py、scratch_*.py 等临时脚本。',
|
||||
'只有用户明确要求长期复用或该脚本属于产品代码时,才把 Python 脚本加入项目源码目录。',
|
||||
'当事情失败时,先诊断原因再改变方向。不要对同一个失败动作反复循环。',
|
||||
'注意不要引入命令注入、SQL 注入、XSS 或不安全 shell 行为等安全问题。',
|
||||
'如实汇报结果。如果没有运行某个验证步骤,需要明确说明。',
|
||||
@@ -201,10 +209,17 @@ def get_using_your_tools_section(enabled_tool_names: set[str]) -> str:
|
||||
'需要结构化文件分析、批量数据处理、JSON/JSONL 转换、抽样、校验或快速计算时,必须优先使用 python_exec,而不是通过 bash 手写 python 命令。'
|
||||
)
|
||||
items.append(
|
||||
'python_exec 默认使用项目 .venv/bin/python;不要用它安装依赖。缺少依赖时先说明缺失包并向用户确认包管理方式。'
|
||||
'python_exec 默认使用当前用户独立 Python venv,不使用项目 .venv;不要用 bash 执行 python 或 pip。'
|
||||
)
|
||||
items.append(
|
||||
'如果已经用 write_file 生成了临时 Python 脚本,下一步也应使用 python_exec 的 script_path 执行该脚本,不要改用 bash。'
|
||||
'python_exec 会注入 PYTHON_EXEC_SCRATCHPAD 环境变量,指向当前用户当前会话隔离的 scratchpad。一次性脚本、缓存和中间输出都应写入这里。'
|
||||
)
|
||||
if 'python_package' in enabled_tool_names:
|
||||
items.append(
|
||||
'当 python_exec 因缺少 pandas、pyarrow、openpyxl 等 Python 包失败时,使用 python_package 在当前用户独立 venv 中安装缺失包,然后重试;不要安装到系统 Python 或项目 .venv。'
|
||||
)
|
||||
items.append(
|
||||
'不要用 write_file 在项目根目录生成临时 Python 脚本;如果用户明确要求保留脚本,才写入合适的项目路径,并用 python_exec 的 script_path 执行。'
|
||||
)
|
||||
if 'bash' in enabled_tool_names:
|
||||
items.append(
|
||||
@@ -444,6 +459,8 @@ def compute_simple_env_info(prompt_context: PromptContext) -> str:
|
||||
items.append(list(prompt_context.additional_working_directories))
|
||||
if prompt_context.scratchpad_directory:
|
||||
items.append(f'会话 scratchpad 目录: {prompt_context.scratchpad_directory}')
|
||||
if prompt_context.python_env_directory:
|
||||
items.append(f'当前用户 Python venv: {prompt_context.python_env_directory}')
|
||||
items.extend(
|
||||
[
|
||||
f'平台: {prompt_context.platform_name}',
|
||||
|
||||
+62
-8
@@ -4,7 +4,7 @@ from dataclasses import dataclass, field, replace
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
from uuid import uuid4
|
||||
|
||||
from .account_runtime import AccountRuntime
|
||||
@@ -68,6 +68,43 @@ from .tokenizer_runtime import describe_token_counter
|
||||
from .workflow_runtime import WorkflowRuntime
|
||||
from .worktree_runtime import WorktreeRuntime
|
||||
from .session_env_vars import clear_session_env_vars
|
||||
|
||||
RuntimeEventSink = Callable[[dict[str, object]], None]
|
||||
|
||||
|
||||
class _RuntimeEventBuffer(list[dict[str, object]]):
|
||||
def __init__(self, event_sink: RuntimeEventSink | None = None) -> None:
|
||||
super().__init__()
|
||||
self._event_sink = event_sink
|
||||
|
||||
def append(self, event: dict[str, object]) -> None: # type: ignore[override]
|
||||
super().append(event)
|
||||
if self._event_sink is not None:
|
||||
self._event_sink(dict(event))
|
||||
|
||||
def extend(self, events: Any) -> None: # type: ignore[override]
|
||||
for event in events:
|
||||
self.append(event)
|
||||
|
||||
|
||||
def _append_final_text_stream_events(
|
||||
stream_events: list[dict[str, object]],
|
||||
text: str,
|
||||
) -> None:
|
||||
if not text:
|
||||
return
|
||||
stream_events.append({'type': 'final_text_start'})
|
||||
chunk_size = 96
|
||||
for start in range(0, len(text), chunk_size):
|
||||
stream_events.append(
|
||||
{
|
||||
'type': 'final_text_delta',
|
||||
'delta': text[start : start + chunk_size],
|
||||
}
|
||||
)
|
||||
stream_events.append({'type': 'final_text_end'})
|
||||
|
||||
|
||||
from .session_store import (
|
||||
StoredAgentSession,
|
||||
load_agent_session,
|
||||
@@ -375,6 +412,7 @@ class LocalCodingAgent:
|
||||
session_id: str | None = None,
|
||||
*,
|
||||
runtime_context: str | None = None,
|
||||
event_sink: RuntimeEventSink | None = None,
|
||||
) -> AgentRunResult:
|
||||
self.managed_agent_id = None
|
||||
self.resume_source_session_id = None
|
||||
@@ -389,6 +427,7 @@ class LocalCodingAgent:
|
||||
scratchpad_directory=scratchpad_directory,
|
||||
existing_file_history=(),
|
||||
runtime_context=runtime_context,
|
||||
event_sink=event_sink,
|
||||
)
|
||||
self._accumulate_usage(result)
|
||||
self._finalize_managed_agent(result)
|
||||
@@ -400,6 +439,7 @@ class LocalCodingAgent:
|
||||
stored_session: StoredAgentSession,
|
||||
*,
|
||||
runtime_context: str | None = None,
|
||||
event_sink: RuntimeEventSink | None = None,
|
||||
) -> AgentRunResult:
|
||||
self.managed_agent_id = None
|
||||
self.resume_source_session_id = stored_session.session_id
|
||||
@@ -435,6 +475,7 @@ class LocalCodingAgent:
|
||||
scratchpad_directory=scratchpad_directory,
|
||||
existing_file_history=stored_session.file_history,
|
||||
runtime_context=runtime_context,
|
||||
event_sink=event_sink,
|
||||
)
|
||||
self._accumulate_usage(result)
|
||||
self._finalize_managed_agent(result)
|
||||
@@ -449,6 +490,7 @@ class LocalCodingAgent:
|
||||
scratchpad_directory: Path | None,
|
||||
existing_file_history: tuple[dict[str, object], ...],
|
||||
runtime_context: str | None = None,
|
||||
event_sink: RuntimeEventSink | None = None,
|
||||
) -> AgentRunResult:
|
||||
slash_result = preprocess_slash_command(self, prompt)
|
||||
if slash_result.handled and not slash_result.should_query:
|
||||
@@ -495,6 +537,10 @@ class LocalCodingAgent:
|
||||
session.append_user(effective_prompt, model_content=model_prompt)
|
||||
self.last_session = session
|
||||
self.active_session_id = session_id
|
||||
self.tool_context = replace(
|
||||
self.tool_context,
|
||||
scratchpad_directory=scratchpad_directory,
|
||||
)
|
||||
tool_specs = [tool.to_openai_tool() for tool in self.tool_registry.values()]
|
||||
starting_usage = UsageStats()
|
||||
starting_cost_usd = 0.0
|
||||
@@ -525,7 +571,7 @@ class LocalCodingAgent:
|
||||
total_usage = starting_usage
|
||||
total_cost_usd = starting_cost_usd
|
||||
file_history = list(existing_file_history)
|
||||
stream_events: list[dict[str, object]] = []
|
||||
stream_events: list[dict[str, object]] = _RuntimeEventBuffer(event_sink)
|
||||
assistant_response_segments: list[str] = []
|
||||
delegated_tasks = sum(
|
||||
1 for entry in file_history if entry.get('action') in ('delegate_agent', 'Agent')
|
||||
@@ -715,8 +761,10 @@ class LocalCodingAgent:
|
||||
)
|
||||
last_content = ''.join(assistant_response_segments)
|
||||
continue
|
||||
final_output = ''.join(assistant_response_segments)
|
||||
_append_final_text_stream_events(stream_events, final_output)
|
||||
result = AgentRunResult(
|
||||
final_output=''.join(assistant_response_segments),
|
||||
final_output=final_output,
|
||||
turns=turn_index,
|
||||
tool_calls=tool_calls,
|
||||
transcript=session.transcript(),
|
||||
@@ -816,8 +864,10 @@ class LocalCodingAgent:
|
||||
)
|
||||
last_content = ''.join(assistant_response_segments)
|
||||
continue
|
||||
final_output = ''.join(assistant_response_segments)
|
||||
_append_final_text_stream_events(stream_events, final_output)
|
||||
result = AgentRunResult(
|
||||
final_output=''.join(assistant_response_segments),
|
||||
final_output=final_output,
|
||||
turns=turn_index,
|
||||
tool_calls=tool_calls,
|
||||
transcript=session.transcript(),
|
||||
@@ -896,6 +946,8 @@ class LocalCodingAgent:
|
||||
'type': 'tool_start',
|
||||
'tool_name': tool_call.name,
|
||||
'tool_call_id': tool_call.id,
|
||||
'arguments': dict(tool_call.arguments),
|
||||
'assistant_content': turn.content,
|
||||
'message_id': session.messages[tool_message_index].message_id,
|
||||
}
|
||||
)
|
||||
@@ -1194,11 +1246,13 @@ class LocalCodingAgent:
|
||||
self.last_run_result = result
|
||||
return result
|
||||
|
||||
final_output = (
|
||||
last_content
|
||||
or 'Stopped: max turns reached before the model produced a final answer.'
|
||||
)
|
||||
_append_final_text_stream_events(stream_events, final_output)
|
||||
result = AgentRunResult(
|
||||
final_output=(
|
||||
last_content
|
||||
or 'Stopped: max turns reached before the model produced a final answer.'
|
||||
),
|
||||
final_output=final_output,
|
||||
turns=self.runtime_config.max_turns,
|
||||
tool_calls=tool_calls,
|
||||
transcript=session.transcript(),
|
||||
|
||||
+164
-6
@@ -76,6 +76,8 @@ class ToolExecutionContext:
|
||||
command_timeout_seconds: float
|
||||
max_output_chars: int
|
||||
permissions: AgentPermissions
|
||||
scratchpad_directory: Path | None = None
|
||||
python_env_dir: Path | None = None
|
||||
extra_env: dict[str, str] = field(default_factory=dict)
|
||||
tool_registry: dict[str, 'AgentTool'] | None = None
|
||||
search_runtime: 'SearchRuntime | None' = None
|
||||
@@ -152,6 +154,8 @@ class ToolStreamUpdate:
|
||||
def build_tool_context(
|
||||
config: AgentRuntimeConfig,
|
||||
*,
|
||||
scratchpad_directory: Path | None = None,
|
||||
python_env_dir: Path | None = None,
|
||||
extra_env: dict[str, str] | None = None,
|
||||
tool_registry: dict[str, AgentTool] | None = None,
|
||||
search_runtime: 'SearchRuntime | None' = None,
|
||||
@@ -173,6 +177,14 @@ def build_tool_context(
|
||||
command_timeout_seconds=config.command_timeout_seconds,
|
||||
max_output_chars=config.max_output_chars,
|
||||
permissions=config.permissions,
|
||||
scratchpad_directory=scratchpad_directory.resolve() if scratchpad_directory else None,
|
||||
python_env_dir=(
|
||||
python_env_dir.resolve()
|
||||
if python_env_dir
|
||||
else config.python_env_dir.resolve()
|
||||
if config.python_env_dir
|
||||
else None
|
||||
),
|
||||
extra_env=dict(extra_env or {}),
|
||||
tool_registry=tool_registry,
|
||||
search_runtime=search_runtime,
|
||||
@@ -339,20 +351,22 @@ def default_tool_registry() -> dict[str, AgentTool]:
|
||||
name='python_exec',
|
||||
description=(
|
||||
'优先用本工具执行小型 Python 代码或项目内 Python 脚本,用于结构化文件分析、'
|
||||
'JSON/JSONL 处理、批量校验、数据抽样和快速计算。默认使用项目 .venv/bin/python;'
|
||||
'JSON/JSONL 处理、批量校验、数据抽样和快速计算。默认使用当前用户独立 Python venv;'
|
||||
'不要用 bash 运行 python/python3/.venv/bin/python。不要用本工具安装依赖。'
|
||||
'缺包时应向用户确认后再处理依赖。'
|
||||
'缺包时应向用户确认后再处理依赖。一次性分析优先传 code;如需写临时文件,'
|
||||
'必须写入环境变量 PYTHON_EXEC_SCRATCHPAD 指向的会话隔离目录,'
|
||||
'不要在项目根目录创建临时 .py 脚本。'
|
||||
),
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'code': {
|
||||
'type': 'string',
|
||||
'description': '要通过 python -c 执行的 Python 代码。code 和 script_path 必须二选一。',
|
||||
'description': '要通过 python -c 执行的 Python 代码。code 和 script_path 必须二选一;一次性分析优先使用 code。',
|
||||
},
|
||||
'script_path': {
|
||||
'type': 'string',
|
||||
'description': '工作区内已有 Python 脚本路径。code 和 script_path 必须二选一。',
|
||||
'description': '工作区内已有、需要长期复用的 Python 脚本路径。code 和 script_path 必须二选一;不要为一次性分析在项目根目录新建脚本。',
|
||||
},
|
||||
'args': {
|
||||
'type': 'array',
|
||||
@@ -377,12 +391,49 @@ def default_tool_registry() -> dict[str, AgentTool]:
|
||||
},
|
||||
handler=_run_python_exec,
|
||||
),
|
||||
AgentTool(
|
||||
name='python_package',
|
||||
description=(
|
||||
'在当前用户独立 Python venv 中检查或安装 Python 包。'
|
||||
'用于 python_exec 缺少 pandas、pyarrow、openpyxl 等分析依赖时;'
|
||||
'不要通过 bash 执行 pip,也不要安装到项目 .venv。'
|
||||
),
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'action': {
|
||||
'type': 'string',
|
||||
'enum': ['show', 'install'],
|
||||
'description': 'show 查看当前 Python 环境和 pip;install 安装 packages。',
|
||||
},
|
||||
'packages': {
|
||||
'type': 'array',
|
||||
'items': {'type': 'string'},
|
||||
'description': '要安装的包名或 pip requirement spec,例如 pandas、pyarrow==15.0.0。',
|
||||
},
|
||||
'timeout_seconds': {
|
||||
'type': 'number',
|
||||
'minimum': 1,
|
||||
'maximum': 600,
|
||||
'description': '安装超时时间,默认 120 秒。',
|
||||
},
|
||||
'max_output_chars': {
|
||||
'type': 'integer',
|
||||
'minimum': 1000,
|
||||
'maximum': 50000,
|
||||
},
|
||||
},
|
||||
'required': ['action'],
|
||||
},
|
||||
handler=_run_python_package,
|
||||
),
|
||||
AgentTool(
|
||||
name='bash',
|
||||
description=(
|
||||
'运行真实 shell 命令,例如系统命令、进程控制、git 只读检查或用户已确认的依赖安装。'
|
||||
'不要用 bash 执行 python、python3 或 .venv/bin/python;结构化文件分析、'
|
||||
'不要用 bash 执行 python、python3、pip 或 .venv/bin/python;结构化文件分析、'
|
||||
'JSON/JSONL 处理、批量校验、数据抽样和快速计算必须优先使用 python_exec。'
|
||||
'Python 包安装必须优先使用 python_package。'
|
||||
),
|
||||
parameters={
|
||||
'type': 'object',
|
||||
@@ -2604,6 +2655,8 @@ def _run_python_exec(arguments: dict[str, Any], context: ToolExecutionContext) -
|
||||
payload = [
|
||||
'timed_out=true',
|
||||
f'timeout_seconds={timeout_seconds:g}',
|
||||
f'scratchpad={context.scratchpad_directory or ""}',
|
||||
f'python_env={context.python_env_dir or ""}',
|
||||
'[stdout]',
|
||||
stdout.rstrip(),
|
||||
'[stderr]',
|
||||
@@ -2615,6 +2668,8 @@ def _run_python_exec(arguments: dict[str, Any], context: ToolExecutionContext) -
|
||||
'action': 'python_exec',
|
||||
'mode': mode,
|
||||
'interpreter': interpreter,
|
||||
'scratchpad_directory': str(context.scratchpad_directory) if context.scratchpad_directory else '',
|
||||
'python_env_dir': str(context.python_env_dir) if context.python_env_dir else '',
|
||||
'timed_out': True,
|
||||
'timeout_seconds': timeout_seconds,
|
||||
'stdout_preview': _snapshot_text(stdout),
|
||||
@@ -2628,6 +2683,8 @@ def _run_python_exec(arguments: dict[str, Any], context: ToolExecutionContext) -
|
||||
payload = [
|
||||
f'exit_code={completed.returncode}',
|
||||
f'interpreter={interpreter}',
|
||||
f'scratchpad={context.scratchpad_directory or ""}',
|
||||
f'python_env={context.python_env_dir or ""}',
|
||||
'[stdout]',
|
||||
stdout.rstrip(),
|
||||
'[stderr]',
|
||||
@@ -2639,6 +2696,94 @@ def _run_python_exec(arguments: dict[str, Any], context: ToolExecutionContext) -
|
||||
'action': 'python_exec',
|
||||
'mode': mode,
|
||||
'interpreter': interpreter,
|
||||
'scratchpad_directory': str(context.scratchpad_directory) if context.scratchpad_directory else '',
|
||||
'python_env_dir': str(context.python_env_dir) if context.python_env_dir else '',
|
||||
'exit_code': completed.returncode,
|
||||
'stdout_preview': _snapshot_text(stdout),
|
||||
'stderr_preview': _snapshot_text(stderr),
|
||||
'output_preview': _snapshot_text('\n'.join(payload).strip()),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _run_python_package(arguments: dict[str, Any], context: ToolExecutionContext) -> str:
|
||||
_ensure_process_execution_allowed(context, 'Python package management')
|
||||
action = _require_string(arguments, 'action')
|
||||
interpreter = _resolve_python_interpreter(context)
|
||||
timeout_seconds = _coerce_float(
|
||||
arguments,
|
||||
'timeout_seconds',
|
||||
min(context.command_timeout_seconds * 4, 120.0),
|
||||
)
|
||||
max_output_chars = _coerce_int(arguments, 'max_output_chars', context.max_output_chars)
|
||||
if action == 'show':
|
||||
command = [interpreter, '-m', 'pip', '--version']
|
||||
elif action == 'install':
|
||||
packages = arguments.get('packages')
|
||||
if not isinstance(packages, list) or not packages:
|
||||
raise ToolExecutionError('packages must be a non-empty array for action=install')
|
||||
package_args = [str(package).strip() for package in packages if str(package).strip()]
|
||||
if not package_args:
|
||||
raise ToolExecutionError('packages must contain at least one non-empty package name')
|
||||
command = [interpreter, '-m', 'pip', 'install', *package_args]
|
||||
else:
|
||||
raise ToolExecutionError('action must be "show" or "install"')
|
||||
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
cwd=context.root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout_seconds,
|
||||
env=_build_subprocess_env(context),
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
stdout = exc.stdout if isinstance(exc.stdout, str) else ''
|
||||
stderr = exc.stderr if isinstance(exc.stderr, str) else ''
|
||||
payload = [
|
||||
'timed_out=true',
|
||||
f'timeout_seconds={timeout_seconds:g}',
|
||||
f'interpreter={interpreter}',
|
||||
f'python_env={context.python_env_dir or ""}',
|
||||
'[stdout]',
|
||||
stdout.rstrip(),
|
||||
'[stderr]',
|
||||
stderr.rstrip(),
|
||||
]
|
||||
return (
|
||||
_truncate_output('\n'.join(payload).strip(), max_output_chars),
|
||||
{
|
||||
'action': 'python_package',
|
||||
'package_action': action,
|
||||
'interpreter': interpreter,
|
||||
'python_env_dir': str(context.python_env_dir) if context.python_env_dir else '',
|
||||
'timed_out': True,
|
||||
'timeout_seconds': timeout_seconds,
|
||||
'stdout_preview': _snapshot_text(stdout),
|
||||
'stderr_preview': _snapshot_text(stderr),
|
||||
'output_preview': _snapshot_text('\n'.join(payload).strip()),
|
||||
},
|
||||
)
|
||||
|
||||
stdout = completed.stdout or ''
|
||||
stderr = completed.stderr or ''
|
||||
payload = [
|
||||
f'exit_code={completed.returncode}',
|
||||
f'interpreter={interpreter}',
|
||||
f'python_env={context.python_env_dir or ""}',
|
||||
'[stdout]',
|
||||
stdout.rstrip(),
|
||||
'[stderr]',
|
||||
stderr.rstrip(),
|
||||
]
|
||||
return (
|
||||
_truncate_output('\n'.join(payload).strip(), max_output_chars),
|
||||
{
|
||||
'action': 'python_package',
|
||||
'package_action': action,
|
||||
'interpreter': interpreter,
|
||||
'python_env_dir': str(context.python_env_dir) if context.python_env_dir else '',
|
||||
'exit_code': completed.returncode,
|
||||
'stdout_preview': _snapshot_text(stdout),
|
||||
'stderr_preview': _snapshot_text(stderr),
|
||||
@@ -2648,7 +2793,11 @@ def _run_python_exec(arguments: dict[str, Any], context: ToolExecutionContext) -
|
||||
|
||||
|
||||
def _resolve_python_interpreter(context: ToolExecutionContext) -> str:
|
||||
# 优先使用项目虚拟环境,保证数据处理依赖和 agent 运行环境一致。
|
||||
# 优先使用当前用户独立 Python 环境,避免包安装污染项目 .venv 或系统环境。
|
||||
if context.python_env_dir is not None:
|
||||
user_python = context.python_env_dir / 'bin' / 'python'
|
||||
if user_python.exists() and os.access(user_python, os.X_OK):
|
||||
return str(user_python)
|
||||
venv_python = context.root / '.venv' / 'bin' / 'python'
|
||||
if venv_python.exists() and os.access(venv_python, os.X_OK):
|
||||
return str(venv_python)
|
||||
@@ -4395,6 +4544,15 @@ def _build_subprocess_env(context: ToolExecutionContext) -> dict[str, str]:
|
||||
for key, value in get_session_env_vars().items():
|
||||
env[key] = value
|
||||
env.update(context.extra_env)
|
||||
if context.scratchpad_directory is not None:
|
||||
env['PYTHON_EXEC_SCRATCHPAD'] = str(context.scratchpad_directory)
|
||||
if context.python_env_dir is not None:
|
||||
env['VIRTUAL_ENV'] = str(context.python_env_dir)
|
||||
env['PYTHON_EXEC_VENV'] = str(context.python_env_dir)
|
||||
env['PYTHONNOUSERSITE'] = '1'
|
||||
bin_dir = str(context.python_env_dir / 'bin')
|
||||
existing_path = env.get('PATH', '')
|
||||
env['PATH'] = f'{bin_dir}{os.pathsep}{existing_path}' if existing_path else bin_dir
|
||||
return env
|
||||
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ class AgentRuntimeConfig:
|
||||
output_schema: OutputSchemaConfig | None = None
|
||||
session_directory: Path = field(default_factory=lambda: (Path('.port_sessions') / 'agent').resolve())
|
||||
scratchpad_root: Path = field(default_factory=lambda: (Path('.port_sessions') / 'scratchpad').resolve())
|
||||
python_env_dir: Path | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -186,6 +186,11 @@ def serialize_runtime_config(runtime_config: AgentRuntimeConfig) -> JSONDict:
|
||||
),
|
||||
'session_directory': str(runtime_config.session_directory),
|
||||
'scratchpad_root': str(runtime_config.scratchpad_root),
|
||||
'python_env_dir': (
|
||||
str(runtime_config.python_env_dir)
|
||||
if runtime_config.python_env_dir is not None
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -230,6 +235,11 @@ def deserialize_runtime_config(payload: JSONDict) -> AgentRuntimeConfig:
|
||||
output_schema=_deserialize_output_schema(output_schema_payload),
|
||||
session_directory=Path(str(payload.get('session_directory', DEFAULT_AGENT_SESSION_DIR))).resolve(),
|
||||
scratchpad_root=Path(str(payload.get('scratchpad_root', DEFAULT_SESSION_DIR / 'scratchpad'))).resolve(),
|
||||
python_env_dir=(
|
||||
Path(str(payload['python_env_dir'])).resolve()
|
||||
if payload.get('python_env_dir') is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user