Skip to content

feat(api-server): support approval and stop on session chat stream - #58856

Open
baleian wants to merge 1 commit into
NousResearch:mainfrom
baleian:feat/session-chat-stream-approval-stop
Open

feat(api-server): support approval and stop on session chat stream#58856
baleian wants to merge 1 commit into
NousResearch:mainfrom
baleian:feat/session-chat-stream-approval-stop

Conversation

@baleian

@baleian baleian commented Jul 5, 2026

Copy link
Copy Markdown

What does this PR do?

Wires interactive approval and stop into the streaming session
endpoint POST /api/sessions/{id}/chat/stream.

That endpoint reaches the agent through _run_agent, which — unlike the
/v1/runs path — registered nothing in the shared run maps. As a result:

  • Approval failed closed silently. No gateway approval notifier was
    registered for the turn, so a guarded tool hit the "no callback" fallback in
    tools/approval.py and returned approval_pending to the agent while the
    SSE stream emitted no approval.request event and offered no way to
    resolve it. This is the session-endpoint sibling of Legacy chat-completions stream does not emit approval events #51871 (same root cause:
    the notifier is only wired on the runs path) and complements fix(api-server): emit approval events on legacy chat-completions SSE stream #51878, which
    fixes it for /v1/chat/completions.
  • Turns could not be stopped. The internal run_id was never published in
    _active_run_agents / _active_run_tasks, so POST /v1/runs/{run_id}/stop
    could not find the agent to interrupt.

Rather than add new endpoints, this reuses the existing runs-API control
endpoints (/v1/runs/{run_id}/approval, /v1/runs/{run_id}/stop) unchanged —
they key purely off shared run_id maps and are agnostic to which handler
populated them. The streaming session turn now registers itself in those maps
and surfaces approval.request on its SSE stream. Full session-store history
fidelity (tool results, tool_calls, reasoning) comes for free because the
session path already replays the native conversation.

Only the streaming endpoint is wired; the synchronous /api/sessions/{id}/chat
cannot do interactive approval (single blocking response, no event channel) and
is left unchanged.

Related Issue

Fixes #58853

Related: #51871 (chat-completions sibling), #51878.

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix

Changes Made

  • gateway/platforms/api_server.py:
    • _run_agent: add two optional, backward-compatible params —
      approval_notify_callback (register/unregister the gateway approval
      notifier around run_conversation, mirroring the /v1/runs inline
      wiring) and run_id (publish the live AIAgent in
      _active_run_agents[run_id] and clean it up in finally so
      /v1/runs/{run_id}/stop can interrupt it). Existing callers
      (/v1/chat/completions) pass neither and are unaffected.
    • _handle_session_chat_stream: define an _approval_notify closure that
      redacts credentials (_redact_approval_command) and emits
      approval.request on the SSE stream; register the run in
      _run_approval_sessions / _run_statuses / _active_run_tasks (and
      _run_streams_created for the orphan sweeper); set terminal run status;
      tear all registrations down on turn completion.
  • tests/gateway/test_session_chat_approval_stop.py: new — 7 tests covering
    _run_agent run_id publish/cleanup, approval callback register/unregister,
    the _approval_notify closure (redaction + approval.request shape), and an
    end-to-end streaming turn that emits approval.request and populates the
    shared control maps, then cleans them up.
  • tests/gateway/test_approval_prompt_redaction.py: harden the redact-before-
    sink guard to check every _approval_notify in the module (not just the
    first), so additional approval-notify transports can't skip redaction.

How to Test

  1. pytest tests/gateway/test_session_chat_approval_stop.py tests/gateway/test_approval_prompt_redaction.py -q — 15 pass.
  2. Regression: pytest tests/gateway/test_api_server_runs.py tests/gateway/test_api_server.py tests/run_agent/ -q — all pass (2111 passed, 3 skipped locally).
  3. Manual:
    • Start a gateway with API_SERVER_ENABLED=1 and a tool that requires approval.
    • POST /api/sessions/{id}/chat/stream with a prompt that triggers the guarded tool → the SSE stream now emits event: approval.request.
    • POST /v1/runs/{run_id}/approval {"choice":"once"} (run_id from the run.started event) → the tool proceeds.
    • Mid-turn, POST /v1/runs/{run_id}/stop → the agent is interrupted.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat(api-server): …)
  • I searched for existing PRs to make sure this isn't a duplicate (closest: fix(api-server): emit approval events on legacy chat-completions SSE stream #51878, different endpoint)
  • My PR contains only changes related to this feature
  • I've run the tests and all pass
  • I've added tests for my changes
  • I've tested on my platform: Ubuntu (WSL2), Python 3.11

Documentation & Housekeeping

  • Docstrings updated (_run_agent) — no user-facing docs needed
  • N/A — no config keys changed
  • N/A — no architecture/workflow change
  • N/A — pure Python asyncio, no platform-specific I/O
  • N/A — no tool schema/behavior change

@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists labels Jul 5, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Implements #58853. Session-endpoint sibling of #51871 (chat-completions approval gap, fixed by #51878) and companion to #45888 (Responses API path). Keeping the issue open as the feature spec; this PR is the fix.

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

Code Review Summary

Verdict: Comment (not blocking)

New feature: API server support for approval and stop on session chat stream. This is a 3-file PR with moderate additions (~24K diff). The feature adds meaningful new API surface to the session streaming layer.

Suggestion: The PR description is minimal. Please confirm:

  1. Whether this feature has corresponding test coverage
  2. Whether any new endpoints are added to the OpenAPI/API schema

Surface area note: With 3 files changed and new stream control behavior, this warrants a human reviewer to confirm the session stop semantics are correct before merge.

@baleian

baleian commented Jul 5, 2026

Copy link
Copy Markdown
Author

Tests

$ pytest tests/gateway/test_session_chat_approval_stop.py \
         tests/gateway/test_approval_prompt_redaction.py -q
15 passed in 1.03s

Line coverage over the changed hunks in api_server.py is 36/42 executable statements (85.7%); the 6 uncovered lines are all except Exception: pass cleanup guards. The tests cover the run_id publish/cleanup, the approval-callback register/unregister, the _approval_notify redaction + approval.request shape, and a full streaming turn end-to-end.

API schema

No new endpoints — grep over the diff shows zero route registrations. It reuses /v1/runs/{run_id}/approval and /v1/runs/{run_id}/stop unchanged; the session stream just registers into the shared run_id maps those endpoints already read from, so the OpenAPI surface is unchanged.

@100yenadmin 100yenadmin 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.

Reviewed against #58853's accepted design ("reuse the existing runs-API control endpoints, no new endpoints") — this implements it faithfully: approval.request emitted on the session SSE stream with the same redaction shaping as the runs path, the run registered in _active_run_agents/_active_run_tasks/_run_approval_sessions so POST /v1/runs/{run_id}/approval and /stop just work, and cleanup bound to the task future. The test that introspects the source to assert the redaction line sits before the sink is a nice guard against silent regressions during future refactors.

One question and one follow-up thought:

  • _cleanup_run pops _run_statuses[run_id] — after the stream closes, can a client still query the terminal status (completed/failed/stopped) of a session-stream run, or does it 404 immediately? If the classic runs path retains a terminal status for some window, mirroring that here would keep the two control surfaces symmetric; if it doesn't, ignore me.
  • clarify prompts park the agent thread the same way exec-approvals do (tools/clarify_gateway.py, same blocking-queue shape) — a session-stream turn that hits a clarify still has no path to answer. The seam this PR builds (approval.request event + runs-API resolution) extends to clarify almost mechanically (clarify.request event + a resolve route to resolve_gateway_clarify). Natural follow-up rather than scope for this PR — flagging it because we're actively working the clarify surfaces on other channels (#61940, #61943) and would happily build that follow-up on top of this once it lands.

Would like to see this merge — it closes the sharpest gap in #58853.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for targeting a real session-stream control gap: current main's handler calls _run_agent without any run-control registration (gateway/platforms/api_server.py:2150-2158), while approval and stop resolve through the shared run maps (gateway/platforms/api_server.py:4754-4766, 4848-4864).

Problems

  • The proposed approval_session_key = gateway_session_key or session_id or run_id reintroduces cross-run approval sharing. Current main deliberately changed API approvals to use run_id in commit 66325a770; its regression test at tests/gateway/test_api_server_runs.py:380-408 covers concurrent runs with the same client session.
  • The proposed cleanup removes _run_statuses immediately. Main retains terminal run status for _RUN_STATUS_TTL (gateway/platforms/api_server.py:4280-4281, 4905-4912), so session-stream runs should follow that lifecycle.
  • The changed redaction test reads production source via inspect.getsource; this conflicts with the repository's explicit behavioral-test rule (AGENTS.md:1358-1362).

Suggested changes

  • Scope the approval queue to run_id, retain terminal status through the existing sweeper, and add a behavioral concurrent-run/SSE redaction test.

Automated hermes-sweeper review.

@alt-glitch alt-glitch added needs-decision Awaiting maintainer decision before any implementation sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 15, 2026
@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/sessions Session lifecycle, resume, persistence, history area/streaming Streaming responses: gateway delivery, provider wire labels Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history area/streaming Streaming responses: gateway delivery, provider wire comp/gateway Gateway runner, session dispatch, delivery needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Interactive approval + stop on POST /api/sessions/{id}/chat/stream

5 participants