Skip to content

Fix/fire and forget#723

Open
DebanKsahu wants to merge 7 commits into
Agent-Field:mainfrom
DebanKsahu:fix/fire_and_forget
Open

Fix/fire and forget#723
DebanKsahu wants to merge 7 commits into
Agent-Field:mainfrom
DebanKsahu:fix/fire_and_forget

Conversation

@DebanKsahu

Copy link
Copy Markdown
Contributor

Summary

  • Fixes the blocking fire and forget issue by wrapping the async functions into asyncio.Task.
  • Left the sync log_execution part as it is as it seems lightweight no, needless to delegate it to a thread.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Docs only
  • Tests only
  • CI / tooling
  • Breaking change

Test plan

  • ./scripts/test-all.sh
  • Verified the branch rebases cleanly on latest upstream/main.

Test coverage

I ran the Python SDK coverage test locally because this PR only changes Python-side code.

Command used:

cd sdk/python
uv run pytest --cov=agentfield --cov-report=term-missing

Result:

1661 passed, 4 skipped, 36 deselected, 27 warnings
TOTAL coverage: 81%

Before asking for review, please confirm:

  • I ran tests for the surface(s) I changed locally.
  • New code paths are covered by tests in this PR (no bare additions).
  • If I removed code, I updated coverage-baseline.json in
    this PR only if the removal caused a legitimate regression and I
    called it out in the summary above.
  • The coverage gate check is green in CI before requesting review.

Checklist

Related issues / PRs

Fixes #622

@DebanKsahu DebanKsahu requested review from a team and AbirAbbas as code owners July 6, 2026 11:15
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Performance

SDK Memory Δ Latency Δ Tests Status
Python 9.5 KB +6% 0.31 µs -11%

✓ No regressions detected

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage gate

Thresholds from .coverage-gate.toml: per-surface ≥ 84%, aggregate ≥ 85%, max per-surface regression ≤ 1.0 pp, max aggregate regression ≤ 0.50 pp.

Surface Current Baseline Δ
control-plane 86.90% 87.40% ↓ -0.50 pp 🟡
sdk-go 91.90% 92.00% ↓ -0.10 pp 🟢
sdk-python 93.76% 93.73% ↑ +0.03 pp 🟢
sdk-typescript 90.42% 90.42% → +0.00 pp 🟢
web-ui 84.76% 84.79% ↓ -0.03 pp 🟡
aggregate 85.55% 85.75% ↓ -0.20 pp 🟡

✅ Gate passed

No surface regressed past the allowed threshold and the aggregate stayed above the floor.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📐 Patch coverage gate

Threshold: 80% on lines this PR touches vs origin/main (from .coverage-gate.toml:thresholds.min_patch).

Surface Touched lines Patch coverage Status
control-plane 0 ➖ no changes
sdk-go 0 ➖ no changes
sdk-python 22 86.00%
sdk-typescript 0 ➖ no changes
web-ui 0 ➖ no changes

✅ Patch gate passed

Every surface whose lines were touched by this PR has patch coverage at or above the threshold.

@AbirAbbas AbirAbbas 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.

Good direction — detaching the telemetry POST out of the reasoner's critical path is the right fix for #622, and the GC-safe idiom (_background_tasks.add + add_done_callback(discard)) is done correctly at all the sites. Two things holding back an approval, both inline.

Also: there are no tests for the new non-blocking behavior (the "covered by tests" box is unchecked), and this only fixes half of #622log_execution is still called synchronously on the event loop. Fine to scope that out, but say so in the PR/issue rather than leaving it silent. A ruff format pass would also clear the trailing-whitespace/indentation noise.

reasoner_id,
payload_dict,
parent_execution_id=execution_context.parent_execution_id,
task = asyncio.create_task(

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.

Detaching start/complete/error into independent tasks means the events can now hit the endpoint out of order — previously notify_call_start was awaited before the reasoner body ran, so "started" was guaranteed to post before "completed". If the control-plane telemetry orders by timestamp/status this is fine, but it's a real weakening of the prior contract — worth confirming the CP tolerates out-of-order start/complete and noting it in the PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Internally task queue follow the FIFO property so if start pushed first then it will execute first unless and await called will be executed inside the first task which leads it to sleep and task after the first task will be executed. Here the only async code inside the all notify functions is fire_and_forget_update() method which might cause issue.

I have 2 approach to this:

  1. Convert fire_and_forget_update() to a task also so that start, complete and error execute in order then their fire_and_forget_update also queued in order.
  2. Instead of converting the entire notify function into a task we should only convert the respective fire_and_forget_update() calls into task.

Please review these 2 approaches.

parent_execution_id=execution_context.parent_execution_id,
)
)
self._background_tasks.add(task)

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.

These background tasks are never drained on shutdown — stop()_cleanup_async_resources() calls self.client.aclose() while notification POSTs may still be in flight, so the final complete/error event can be dropped if the reasoner returns right before teardown. Previously that terminal event was awaited, i.e. guaranteed delivered.

Suggest draining before the client closes: await asyncio.gather(*self._background_tasks, return_exceptions=True) (with a short timeout) in _cleanup_async_resources, ahead of aclose().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry for my mistake, I will work on it in the next commits.

@DebanKsahu

Copy link
Copy Markdown
Contributor Author

Good direction — detaching the telemetry POST out of the reasoner's critical path is the right fix for #622, and the GC-safe idiom (_background_tasks.add + add_done_callback(discard)) is done correctly at all the sites. Two things holding back an approval, both inline.

Also: there are no tests for the new non-blocking behavior (the "covered by tests" box is unchecked), and this only fixes half of #622log_execution is still called synchronously on the event loop. Fine to scope that out, but say so in the PR/issue rather than leaving it silent. A ruff format pass would also clear the trailing-whitespace/indentation noise.

Regarding the sync execution of log_execution, I left out because If I execute that into a separate thread then there are out of order problems occuring in the existing tests, also log_execution is lightweight so it's might don't effect the async execution. That's why I left out like this.

- Wrapped all functions (`notify_call_start`, `notify_call_complete` and `notify_call_error`) into `asyncio.Task` so parent function don't have to await them.
- Converted the `_emit_execution_transition_log` to sync as underlying code is sync and execute it to a separate thread.
…method using `asyncio.to_thread()` because of mismatch order.

- Considering the `log_execution` inside the function is a lightweight function which don't takeover the `event_loop` for long time.
@DebanKsahu DebanKsahu force-pushed the fix/fire_and_forget branch from f9f2a41 to f3e53c8 Compare July 11, 2026 04:40
- Initially mock `Agent` is created through __new__ which by pass __init__ and it doesn't contain `_background_tasks` property.
- I added `_background_tasks` property and added new assert in `test_cleanup_async_resources` test.
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.

[Python SDK] fire_and_forget_update isn't F&F

2 participants