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
3 changes: 3 additions & 0 deletions apps/desktop/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import path from 'path'
export default defineConfig({
base: './',
plugins: [react(), tailwindcss()],
test: {
environment: 'jsdom'
},
build: {
// Keep desktop packaging stable: Shiki ships many dynamic chunks by
// default, and electron-builder can OOM scanning thousands of files.
Expand Down
13 changes: 10 additions & 3 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5324,7 +5324,13 @@ def chat(self, message: str, stream_callback: Optional[callable] = None) -> str:
str: Final assistant response
"""
result = self.run_conversation(message, stream_callback=stream_callback)
return result["final_response"]
# run_conversation has early-return / error paths (interrupted, retries
# exhausted, policy/billing bail) that omit "final_response". Honor the
# documented `-> str` contract instead of crashing with KeyError, so a
# run that never produced a final turn is recorded as a clean empty
# result (treated as a failed run upstream) rather than a harness Python
# traceback that gets misclassified as a launcher/harness fault.
return result.get("final_response") or ""

def _run_codex_app_server_turn(
self,
Expand Down Expand Up @@ -5517,10 +5523,11 @@ def main(
print(f"📞 API Calls: {result['api_calls']}")
print(f"💬 Messages: {len(result['messages'])}")

if result['final_response']:
_final_response = result.get('final_response')
if _final_response:
print("\n🎯 FINAL RESPONSE:")
print("-" * 30)
print(result['final_response'])
print(_final_response)

# Save sample trajectory to UUID-named file if requested
if save_sample:
Expand Down
37 changes: 37 additions & 0 deletions tests/test_chat_final_response_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Tests for AIAgent.chat() final_response guard.

run_agent.py:chat() must honor its ``-> str`` contract even when
run_conversation returns an early-exit / error dict that omits
``"final_response"`` (interrupted, retries exhausted, policy/billing bail).
Before the guard this raised ``KeyError: 'final_response'``, which crashed the
dispatched ``hermes -z`` goal worker and was misclassified upstream as a
harness fault instead of a clean "no final response → failed run".
"""

from unittest.mock import patch

from run_agent import AIAgent


def _agent_without_init() -> AIAgent:
# Bypass __init__ (heavy: providers/config/IO). We only exercise chat()'s
# handling of the run_conversation return dict.
return AIAgent.__new__(AIAgent)


def test_chat_returns_empty_string_when_final_response_key_missing():
agent = _agent_without_init()
with patch.object(agent, "run_conversation", return_value={"completed": False, "api_calls": 1}):
assert agent.chat("hi") == ""


def test_chat_returns_empty_string_when_final_response_is_none():
agent = _agent_without_init()
with patch.object(agent, "run_conversation", return_value={"final_response": None}):
assert agent.chat("hi") == ""


def test_chat_returns_final_response_when_present():
agent = _agent_without_init()
with patch.object(agent, "run_conversation", return_value={"final_response": "the answer"}):
assert agent.chat("hi") == "the answer"
Loading