Skip to content

feat: pass client tool results back to engine - #54

Open
sr-anam wants to merge 3 commits into
feat/handle-tool-lifecycle-eventsfrom
feat/handle-client-tool-passback
Open

feat: pass client tool results back to engine#54
sr-anam wants to merge 3 commits into
feat/handle-tool-lifecycle-eventsfrom
feat/handle-client-tool-passback

Conversation

@sr-anam

@sr-anam sr-anam commented Apr 17, 2026

Copy link
Copy Markdown

Summary by cubic

Send client tool results back to the engine so the LLM can use them in its reply. Adds session-scoped event filtering and tool config options for awaiting results and timeouts.

  • New Features

    • Client tool handlers that return a string from on_start now send a tool_result over the data channel and emit a local TOOL_CALL_COMPLETED; exceptions send an error and emit TOOL_CALL_FAILED.
    • Added send_tool_result to the streaming client and wired it via ToolCallManager when a session connects; manager clears session state on close or failed connect.
    • ClientToolConfig supports await_result (maps to awaitResult) and tool_timeout_seconds (maps to toolTimeoutSeconds).
    • Tool call payloads include session_id; the manager filters by active session and ignores stale completes after a failure.
    • Example updated to show await_result=True and tool_timeout_seconds; tests cover passback, no-result behavior, session filtering, and failure handling.
  • Migration

    • To have the LLM wait for and use a client tool’s output:
      • Set await_result=True (and optionally tool_timeout_seconds) in ClientToolConfig.
      • Return a result string from on_start; return None to acknowledge without sending a result.

Written for commit b1da5d0. Summary will update on new commits.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/anam/_tool_call_manager.py">

<violation number="1" location="src/anam/_tool_call_manager.py:153">
P1: Client tool handlers that return `None` are now incorrectly treated as completed results, which breaks the documented `on_start` contract and can prematurely complete fire-and-forget tool calls.</violation>
</file>

<file name="src/anam/client.py">

<violation number="1" location="src/anam/client.py:274">
P2: `connect_async`'s new failure cleanup is partial; it should also tear down/reset streaming/session state when `connect()` raises.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment on lines +153 to +168
self._send_client_tool_result(event, result=result, error_message=None)
# Auto-complete with the returned result
completed_payload = self._build_completed_payload(event, result=result)
await self._emit(AnamEvent.TOOL_CALL_COMPLETED, completed_payload)
if tool_call_id:
self._pending_calls.pop(tool_call_id, None)
# Dispatch to handler's on_complete callback
if hasattr(handler, "on_complete") and handler.on_complete is not None:
try:
await handler.on_complete(completed_payload)
except Exception as cb_err:
logger.error(
"Error in on_complete handler for tool '%s': %s",
tool_name,
cb_err,
)

@cubic-dev-ai cubic-dev-ai Bot Apr 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Client tool handlers that return None are now incorrectly treated as completed results, which breaks the documented on_start contract and can prematurely complete fire-and-forget tool calls.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/anam/_tool_call_manager.py, line 153:

<comment>Client tool handlers that return `None` are now incorrectly treated as completed results, which breaks the documented `on_start` contract and can prematurely complete fire-and-forget tool calls.</comment>

<file context>
@@ -108,31 +147,36 @@ async def process_started_event(self, event: dict[str, Any]) -> None:
-                                    tool_name,
-                                    cb_err,
-                                )
+                    self._send_client_tool_result(event, result=result, error_message=None)
+                    # Auto-complete with the returned result
+                    completed_payload = self._build_completed_payload(event, result=result)
</file context>
Suggested change
self._send_client_tool_result(event, result=result, error_message=None)
# Auto-complete with the returned result
completed_payload = self._build_completed_payload(event, result=result)
await self._emit(AnamEvent.TOOL_CALL_COMPLETED, completed_payload)
if tool_call_id:
self._pending_calls.pop(tool_call_id, None)
# Dispatch to handler's on_complete callback
if hasattr(handler, "on_complete") and handler.on_complete is not None:
try:
await handler.on_complete(completed_payload)
except Exception as cb_err:
logger.error(
"Error in on_complete handler for tool '%s': %s",
tool_name,
cb_err,
)
if result is not None:
self._send_client_tool_result(event, result=result, error_message=None)
# Auto-complete with the returned result
completed_payload = self._build_completed_payload(event, result=result)
await self._emit(AnamEvent.TOOL_CALL_COMPLETED, completed_payload)
if tool_call_id:
self._pending_calls.pop(tool_call_id, None)
# Dispatch to handler's on_complete callback
if hasattr(handler, "on_complete") and handler.on_complete is not None:
try:
await handler.on_complete(completed_payload)
except Exception as cb_err:
logger.error(
"Error in on_complete handler for tool '%s': %s",
tool_name,
cb_err,
)
Fix with Cubic

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ok, for fire and forget tools this result passback get dropped on the engine side, if await_result is true we always need to send this event (even if there is no returned value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! I've saved this as a new learning to improve future reviews.

Comment thread src/anam/client.py
try:
await self._streaming_client.connect()
except Exception:
self._tool_call_manager.clear_session_state()

@cubic-dev-ai cubic-dev-ai Bot Apr 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: connect_async's new failure cleanup is partial; it should also tear down/reset streaming/session state when connect() raises.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/anam/client.py, line 274:

<comment>`connect_async`'s new failure cleanup is partial; it should also tear down/reset streaming/session state when `connect()` raises.</comment>

<file context>
@@ -262,8 +262,17 @@ async def connect_async(self, session_options: SessionOptions = SessionOptions()
+        try:
+            await self._streaming_client.connect()
+        except Exception:
+            self._tool_call_manager.clear_session_state()
+            raise
         self._is_streaming = True
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant