|
19 | 19 | from openmax.pane_backend import PaneBackendError |
20 | 20 | from openmax.pane_manager import PaneManager |
21 | 21 | from openmax.session_runtime import anchor_payload |
22 | | -from openmax.task_file import read_report, read_shared_context, report_path |
| 22 | +from openmax.task_file import read_report, read_shared_context, report_path, write_tool_output |
23 | 23 |
|
24 | 24 | logger = logging.getLogger(__name__) |
25 | 25 |
|
@@ -122,13 +122,100 @@ def _record_phase_anchor(phase: str, summary: str, completion_pct: int | None = |
122 | 122 | _update_session_phase(normalized_phase) |
123 | 123 |
|
124 | 124 |
|
125 | | -_TOOL_RESPONSE_MAX_CHARS = 500_000 |
| 125 | +_TOOL_RESPONSE_MAX_CHARS = 100_000 |
| 126 | +_TOOL_OUTPUT_SPILL_THRESHOLD = 12_000 |
| 127 | +_TOOL_OUTPUT_PREVIEW_CHARS = 240 |
| 128 | + |
| 129 | + |
| 130 | +def _runtime_cwd_or_fallback() -> str: |
| 131 | + try: |
| 132 | + return _runtime().cwd |
| 133 | + except RuntimeError: |
| 134 | + return str(Path.cwd()) |
| 135 | + |
| 136 | + |
| 137 | +def _compact_preview(text: str, limit: int = _TOOL_OUTPUT_PREVIEW_CHARS) -> str: |
| 138 | + preview = " ".join(text.split()) |
| 139 | + if len(preview) <= limit: |
| 140 | + return preview |
| 141 | + return preview[: limit - 3].rstrip() + "..." |
| 142 | + |
| 143 | + |
| 144 | +def _spill_large_text( |
| 145 | + text: str, |
| 146 | + *, |
| 147 | + prefix: str, |
| 148 | + ext: str = ".txt", |
| 149 | + label: str = "output", |
| 150 | +) -> dict[str, Any]: |
| 151 | + cwd = _runtime_cwd_or_fallback() |
| 152 | + path = write_tool_output(cwd, prefix, text, ext=ext) |
| 153 | + try: |
| 154 | + rel_path = str(path.relative_to(cwd)) |
| 155 | + except ValueError: |
| 156 | + rel_path = str(path) |
| 157 | + return { |
| 158 | + "spilled": True, |
| 159 | + "label": label, |
| 160 | + "path": rel_path, |
| 161 | + "chars": len(text), |
| 162 | + "preview": _compact_preview(text), |
| 163 | + } |
| 164 | + |
| 165 | + |
| 166 | +def _spill_large_values(value: Any, *, prefix: str) -> Any: |
| 167 | + if isinstance(value, str): |
| 168 | + if len(value) <= _TOOL_OUTPUT_SPILL_THRESHOLD: |
| 169 | + return value |
| 170 | + return _spill_large_text(value, prefix=prefix, label="text") |
| 171 | + if isinstance(value, dict): |
| 172 | + result: dict[str, Any] = {} |
| 173 | + for key, child in value.items(): |
| 174 | + result[key] = _spill_large_values(child, prefix=f"{prefix}-{key}") |
| 175 | + return result |
| 176 | + if isinstance(value, list): |
| 177 | + return [ |
| 178 | + _spill_large_values(child, prefix=f"{prefix}-{idx}") for idx, child in enumerate(value) |
| 179 | + ] |
| 180 | + return value |
| 181 | + |
| 182 | + |
| 183 | +def _finalize_payload(payload: Any, max_chars: int) -> str: |
| 184 | + if isinstance(payload, (dict, list)): |
| 185 | + text = json.dumps(payload, ensure_ascii=False) |
| 186 | + else: |
| 187 | + text = str(payload) |
| 188 | + if len(text) <= max_chars: |
| 189 | + return text |
| 190 | + |
| 191 | + ext = ".json" if isinstance(payload, (dict, list)) else ".txt" |
| 192 | + spilled = _spill_large_text( |
| 193 | + text, |
| 194 | + prefix="tool-response", |
| 195 | + ext=ext, |
| 196 | + label="payload", |
| 197 | + ) |
| 198 | + compact = { |
| 199 | + "spilled": True, |
| 200 | + "reason": "response_too_large", |
| 201 | + "path": spilled["path"], |
| 202 | + "chars": spilled["chars"], |
| 203 | + "preview": spilled["preview"], |
| 204 | + } |
| 205 | + return json.dumps(compact, ensure_ascii=False) |
126 | 206 |
|
127 | 207 |
|
128 | 208 | def _tool_response(data: Any, max_chars: int = _TOOL_RESPONSE_MAX_CHARS) -> dict[str, Any]: |
129 | | - text = json.dumps(data, ensure_ascii=False) if isinstance(data, (dict, list)) else str(data) |
130 | | - if len(text) > max_chars: |
131 | | - text = text[:max_chars] + "\n...[truncated]" |
| 209 | + if isinstance(data, str) and len(data) > _TOOL_OUTPUT_SPILL_THRESHOLD: |
| 210 | + spill = _spill_large_text(data, prefix="tool-response", label="text") |
| 211 | + text = ( |
| 212 | + f"large output written to {spill['path']} " |
| 213 | + f"({spill['chars']} chars). preview: {spill['preview']}" |
| 214 | + ) |
| 215 | + elif isinstance(data, (dict, list)): |
| 216 | + text = _finalize_payload(_spill_large_values(data, prefix="tool-response"), max_chars) |
| 217 | + else: |
| 218 | + text = _finalize_payload(data, max_chars) |
132 | 219 | return {"content": [{"type": "text", "text": text}]} |
133 | 220 |
|
134 | 221 |
|
@@ -274,6 +361,8 @@ async def _wait_for_pane_ready( |
274 | 361 | text = "" |
275 | 362 | if any(pat in text for pat in ready_patterns): |
276 | 363 | return True |
| 364 | + if not pane_mgr.is_pane_alive(pane_id): |
| 365 | + return False |
277 | 366 | lines = [ln for ln in text.strip().splitlines() if ln.strip()] |
278 | 367 | if len(lines) >= 3 and text == prev_text: |
279 | 368 | stable_count += 1 |
@@ -539,11 +628,12 @@ async def _wait_and_send_prompt( |
539 | 628 |
|
540 | 629 | ready = True |
541 | 630 | if cmd_spec.ready_patterns: |
| 631 | + ready_timeout = max(min(cmd_spec.ready_delay_seconds * 4, 12.0), 5.0) |
542 | 632 | ready = await _wait_for_pane_ready( |
543 | 633 | runtime.pane_mgr, |
544 | 634 | pane.pane_id, |
545 | 635 | cmd_spec.ready_patterns, |
546 | | - timeout=max(cmd_spec.ready_delay_seconds * 4, 30.0), |
| 636 | + timeout=ready_timeout, |
547 | 637 | ) |
548 | 638 | if not ready: |
549 | 639 | console.print( |
|
0 commit comments