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
38 changes: 38 additions & 0 deletions libs/code/deepagents_code/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -26170,6 +26170,26 @@ class AppResult:
"""`(is_available, latest_version)` for post-exit update warning."""


class TextualAppError(Exception):
"""`run_textual_app` failure that still carries the app's final state.

The TUI resolves resume intent and `/threads` switches asynchronously, so
only the app knows which thread was active when it crashed. Callers catch
this to render teardown hints against the right thread.
"""

def __init__(self, message: str, result: AppResult) -> None:
"""Store the partial result alongside the original error message.

Args:
message: The underlying exception's message.
result: Snapshot of the app's return code, thread ID, and session
stats at the moment of the crash.
"""
super().__init__(message)
self.result = result


async def run_textual_app(
*,
agent: Any = None, # noqa: ANN401
Expand Down Expand Up @@ -26265,6 +26285,11 @@ async def run_textual_app(

Returns:
An `AppResult` with the return code and final thread ID.

Raises:
TextualAppError: The app crashed; the exception carries an `AppResult`
snapshot with the final thread ID so callers can still render
teardown hints for the thread that was active at the crash.
"""
app = DeepAgentsApp(
agent=agent,
Expand Down Expand Up @@ -26295,6 +26320,19 @@ async def run_textual_app(
)
try:
await app.run_async()
except Exception as e:
# The app resolves resume intent and `/threads` switches internally, so
# only it knows which thread was active at the crash. Attach that state
# so callers can aim teardown resume hints at the right thread.
raise TextualAppError(
str(e),
AppResult(
return_code=app.return_code or 1,
thread_id=app._lc_thread_id,
session_stats=app._session_stats,
update_available=app._update_available,
),
) from e
finally:
# Guarantee server cleanup regardless of how the app exits.
# Covers both the pre-started server_proc path and the deferred
Expand Down
84 changes: 58 additions & 26 deletions libs/code/deepagents_code/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,7 @@ def _render_teardown_thread_hints(
Args:
console: Console to print the hints to.
thread_id: Thread whose checkpoints back the hints.
return_code: Process exit code; the resume hint is shown only on a clean
exit (`0`).
return_code: Process exit code; failed sessions add a resume safety caveat.
"""
from rich.style import Style
from rich.text import Text
Expand Down Expand Up @@ -317,15 +316,19 @@ def _render_teardown_thread_hints(
exc_info=True,
)

if return_code == 0:
console.print()
console.print("[dim]Resume this thread with:[/dim]")
# Echo the command the user actually launched (a shim or the
# `deepagents-code` alias), not a hardcoded `dcode` they may not have.
hint = Text(invoked_name(), style="cyan")
hint.append(" -r ", style="cyan")
hint.append(str(thread_id), style="cyan")
console.print(hint)
console.print()
console.print("[dim]Resume this thread with:[/dim]")
# Echo the command the user actually launched (a shim or the
# `deepagents-code` alias), not a hardcoded `dcode` they may not have.
hint = Text(invoked_name(), style="cyan")
hint.append(" -r ", style="cyan")
hint.append(str(thread_id), style="cyan")
console.print(hint)
if return_code != 0:
console.print(
"[dim]Note: the session exited with a non-zero status. Attempting "
"to resume this thread may fail.[/dim]"
Comment thread
open-swe[bot] marked this conversation as resolved.
)


def _confirm_update_after_restart(console: "Console", version: str) -> None:
Expand Down Expand Up @@ -2790,14 +2793,21 @@ async def run_textual_cli_async(
)
except Exception as e:
logger.debug("App error", exc_info=True)
from deepagents_code.app import TextualAppError
from deepagents_code.config import console

error_text = Text("Application error: ", style="red")
error_text.append(str(e))
console.print(error_text)
if logger.isEnabledFor(logging.DEBUG):
console.print(Text(traceback.format_exc(), style="dim"))
return AppResult(return_code=1, thread_id=None)
# The app resolves resume intent and `/threads` switches asynchronously,
# so the crashed session's final thread ID only exists on the exception.
# Returning its snapshot lets the caller's teardown print a resume hint
# for the thread that was actually active when the session died.
if isinstance(e, TextualAppError):
return e.result
return AppResult(return_code=1, thread_id=thread_id)

return result

Expand Down Expand Up @@ -3953,7 +3963,15 @@ def _verify_interpreter_or_exit() -> None:


def cli_main() -> None:
"""Entry point for console script."""
"""Entry point for console script.

Raises:
SystemExit: On shutdown, with the session's exit code (0 on success,
1 on error, 128+signum when a terminating signal unwound the
process).
KeyboardInterrupt: Re-raised out of the TUI teardown block so the
outer handler can print the interruption notice and exit 130.
"""
# Fix for gRPC fork issue on macOS
# https://github.com/grpc/grpc/issues/37642
if sys.platform == "darwin":
Expand Down Expand Up @@ -4911,6 +4929,7 @@ def cli_main() -> None:

# Run Textual TUI
return_code = 0
request_count = 0
try:
interpreter_ptc = _parse_interpreter_tools_flag(
getattr(args, "interpreter_tools", None)
Expand Down Expand Up @@ -4976,26 +4995,39 @@ def cli_main() -> None:
# The user may have switched threads via /threads during the
# session; use the final thread ID for teardown messages.
thread_id = result.thread_id or thread_id
request_count = result.session_stats.request_count
_print_session_stats(result.session_stats, console)
except Exception as e: # noqa: BLE001 # Top-level error handler for the application
return_code = 1
error_msg = Text("\nApplication error: ", style="red")
error_msg.append(str(e))
console.print(error_msg)
console.print(Text(traceback.format_exc(), style="dim"))
sys.exit(1)

# Show LangSmith thread link and resume hint for threads with
# checkpointed content. The `thread_id is not None` check narrows the
# type to `str` for the helper; `_should_check_teardown_thread` gates
# whether the teardown lookup runs at all.
if thread_id is not None and _should_check_teardown_thread(
thread_id,
request_count=result.session_stats.request_count,
resume_thread=args.resume_thread,
):
_render_teardown_thread_hints(
console, thread_id, return_code=return_code
)
except KeyboardInterrupt:
# Ctrl+C; the outer handler prints "Interrupted" and exits 130.
# Mark non-zero so the teardown hint carries the safety caveat.
return_code = 130
raise
except SystemExit as e:
# The termination-signal handler raises SystemExit(128+signum);
# forward non-zero codes so the teardown hint adds the caveat.
if isinstance(e.code, int) and e.code != 0:
return_code = e.code
raise
finally:
# Show LangSmith thread link and resume hint for threads with
# checkpointed content. The `thread_id is not None` check narrows the
# type to `str` for the helper; `_should_check_teardown_thread` gates
# whether the teardown lookup runs at all.
if thread_id is not None and _should_check_teardown_thread(
thread_id,
request_count=request_count,
resume_thread=args.resume_thread,
):
Comment thread
mdrxy marked this conversation as resolved.
_render_teardown_thread_hints(
console, thread_id, return_code=return_code
)
Comment thread
open-swe[bot] marked this conversation as resolved.

# Warn about available update on exit
try:
Expand Down
Loading