Skip to content

feat: enhance tool call lifecycle management with call_id - #42

Merged
Emiyaaaaa merged 1 commit into
Emiyaaaaa:mainfrom
tutudouzi12:feat/toolcall
Aug 5, 2026
Merged

feat: enhance tool call lifecycle management with call_id#42
Emiyaaaaa merged 1 commit into
Emiyaaaaa:mainfrom
tutudouzi12:feat/toolcall

Conversation

@tutudouzi12

Copy link
Copy Markdown
Contributor
  • Introduced a stable call_id for tool_call.started and tool_call.completed events to associate parallel or same-name invocations within a step.
  • Updated the emit_tool_call_started and emit_tool_call_completed methods to accept and return call_id.
  • Modified adapters to utilize call_id for better tracking of tool call results and errors.
  • Enhanced documentation to reflect changes in the tool call lifecycle and the usage of call_id.

- Introduced a stable `call_id` for `tool_call.started` and `tool_call.completed` events to associate parallel or same-name invocations within a step.
- Updated the `emit_tool_call_started` and `emit_tool_call_completed` methods to accept and return `call_id`.
- Modified adapters to utilize `call_id` for better tracking of tool call results and errors.
- Enhanced documentation to reflect changes in the tool call lifecycle and the usage of `call_id`.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a stable call_id (persisted as ToolCall.id) to reliably correlate tool_call.started and tool_call.completed events, especially when tools run in parallel or when the same tool name is invoked multiple times in the same step. It updates both frontend event application logic and backend persistence/emission paths, and adds tests + documentation to lock in the new lifecycle contract.

Changes:

  • Add stable call_id generation/propagation through adapter emit APIs, DB persistence, and SSE payloads (with legacy fallback behavior when call_id is omitted).
  • Update frontend SSE reducer logic to associate tool-call started/completed events by call_id (or oldest pending same-name call when missing).
  • Add regression tests covering out-of-order completion and parallel same-name tool calls; update docs describing the contract.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
frontend/lib/run-events.ts Track tool-call lifecycle by call_id, with fallback to oldest pending same-name call.
docs/data-model.md Document ToolCall.id as the lifecycle association key for SSE events.
docs/api-contract.md Document call_id usage and adapter expectations for started/completed pairing.
backend/tests/test_tool_call_lifecycle.py New tests for call_id matching, fallback behavior, and parallel emissions via executor.
backend/tests/test_adapter_tools.py Assert call_id presence and propagation plus latency_ms typing.
backend/app/worker/executor.py Serialize event handling from concurrent adapter tasks to protect shared AsyncSession.
backend/app/services/run_service.py Persist tool calls using provided call_id, resolve completion by call_id with legacy fallback, and rebroadcast persisted call_id.
backend/app/adapters/langgraph_adapter.py Execute tool calls in parallel within a step to exercise/benefit from call_id association.
backend/app/adapters/echo_adapter.py Pass call_id from started → completed to ensure correct association.
backend/app/adapters/base.py Add ULID-based new_tool_call_id() and return call_id from emit_tool_call_started.
backend/app/adapters/adapter_tools.py Use call_id end-to-end and emit latency_ms based on monotonic timing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +565 to 567
for tc, result in pairs:
name = str(tc.get("name") or "")
tool_results[name] = result
Comment on lines +94 to +101
# Serialize DB writes: adapters may emit from concurrent tasks
# (e.g. parallel tool calls) while sharing one AsyncSession.
emit_lock = asyncio.Lock()

async def _emit(event_type: EventType, data: dict[str, Any]) -> None:
async with emit_lock:
await service._handle_event(run.id, event_type, data)

@Emiyaaaaa
Emiyaaaaa merged commit 4baa9d1 into Emiyaaaaa:main Aug 5, 2026
2 of 4 checks passed
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.

3 participants