Chatbot Interruption
Scope
This document describes the current implementation of the STOP button in the chatbot session. It covers the path from the Razor UI through the C# session/container layer to the Python plan executor.
STOP combines targeted de²generator request cancellation with the existing cooperative plan interruption. It is not the same operation as Terminate.
End-to-end flow
sequenceDiagram
participant User
participant Razor as ChatTopButtons.razor
participant Session as C# Session
participant Program as AiProgramInstance
participant Container as AI container
participant API as Python POST /stopplan
participant Plan as Active Plan
participant Agents as IN_PROGRESS goal agents
participant DE2 as de²generator /v1/cancel
participant Flag as InputOutput.plan_is_stopped
participant Goal as Goal.run()
participant Chatbot as ChatBot2 loop
User->>Razor: Click STOP icon
Razor->>Session: StopPlan()
Session->>Program: StopPlan()
Program->>Container: StopPlan()
Container->>API: POST /stopplan
API->>Plan: cancel_llm_requests()
Plan->>Agents: Cancel requests concurrently
Agents->>DE2: POST /v1/cancel with request token
DE2-->>Agents: cancelled/already_cancelled
Agents-->>Plan: cancellation results
API->>Flag: set_stopped(True)
API-->>Container: 200 OK
Container-->>Program: completed
Program-->>Session: completed
Session-->>Razor: OnDataChanged + StateHasChanged
Note over Goal,Flag: Current work continues until it reaches a stop check
Goal->>Flag: is_stopped()
Flag-->>Goal: True
Goal-->>Chatbot: Stop scheduling / return
Chatbot->>Flag: set_stopped(False)
Chatbot-->>User: Return to input loop
1. Razor entry point
The STOP icon is rendered by ThalabusWeb/RazorComponents/ChatRazorComponents/ChatTopButtons.razor.
The click handler is:
private async Task StopSession() {
if (SessionData != null) {
await SessionData.StopPlan();
StateHasChanged();
}
}
This handler does not set IsStopping. It only requests that the current plan stop and then refreshes the component.
The neighboring TerminateSession() handler is different: it sets SessionData.IsStopping, calls aiProgram.Stop(...), clears chat state, removes the AI program, removes the session, and navigates away. The STOP button does none of those operations.
2. C# session and program delegation
The call chain is:
ChatTopButtons.razor
-> Session.StopPlan()
-> AiProgramInstance.StopPlan()
-> IAiContainer.StopPlan()
Session.Partial.PlanHandling.cs obtains the current AI program and delegates to it. AiProgramInstance.StopPlan() delegates to its configured AiContainer.
Local/container-backed sessions
AiContainer.StopPlan() sends an authenticated HTTP request to:
POST {ai-container-url}/stopplan
The request has an empty JSON object as its body. It uses PythonApiHttpClientFactory.CreateUserSessionClient(...), so the request carries the user-session bearer token when one is available.
LocalAiContainer.StopPlan() uses the same endpoint through its local base URL.
Remote sessions
RemoteSession.StopPlan() uses the remote session API instead:
POST {session-endpoint}/sessions/session/{session-id}/stop
It sends the remote session API token as a bearer token. This is a different transport boundary from the direct Python container /stopplan route; the remote session service is responsible for forwarding or applying the stop request.
3. Python HTTP endpoint
The direct container route is registered in PythonServer/src/api/v1/session.py:
@app.route('/stopplan', methods=['POST'])
def post_stopplan():
success, _, error_message, status_code = post_stopplan_data()
The route is included in the authenticated user-session endpoint allowlist in PythonServer/src/api/auth/jwt_middleware.py.
post_stopplan_data() in PythonServer/src/core/base/Main.py first asks the current global plan to cancel active LLM requests, then sets the cooperative stop flag:
plan = get_global_plan()
if plan is not None:
plan.cancel_llm_requests()
set_stopped(True)
On success it returns HTTP 200 with Plan stopped successfully. If either operation raises an exception, it returns an error response.
No plan ID, goal ID, agent ID, or cancellation token is supplied by C# to this endpoint. The endpoint resolves the current global plan and obtains request tokens from its active goals.
4. Python stop state
PythonServer/src/core/base/InputOutput.py defines:
plan_is_stopped = [False]
The list is used as a mutable container so imported references observe updates. The public helpers are:
def set_stopped(value: bool) -> None:
plan_is_stopped[0] = value
def is_stopped() -> bool:
return plan_is_stopped[0]
This is process-global state within the Python service/container. It is not scoped by plan, goal, agent, or request token.
5. Where execution checks the flag
Plan.run() delegates to the root goal:
response = self.plan_goal.run()
Plan.cancel_llm_requests() recursively obtains active goals and dispatches their cancellation through a ThreadPoolExecutor. Each goal is submitted as an independent cancellation task, so multiple active requests are cancelled concurrently.
Goal.get_active_goals() selects only goals whose status is GoalStatus.IN_PROGRESS. GoalStatus.CHILDREN_IN_PROGRESS is a scheduling/container state and is excluded because the child goals own the LLM requests. Not-started, completed, and failed goals are also excluded.
Each selected goal calls its target agent's cancel_llm_request(). The agent sends:
POST {de2generator-uri}/v1/cancel
with the request token in the JSON body:
{"cancellation_token": "<request-token>"}
The token is created and registered when provider_http routes an LLM call through de²generator, and is removed when that call finishes.
If the de²generator-backed request returns HTTP 499, provider_http raises GoalCancelledException immediately from the HTTP response path. The exception is a cancellation control-flow sentinel that bypasses generic except Exception handlers. It propagates through the agent boundary until _run_parallel(), which marks the goal GoalStatus.CANCELLED through set_goal_status().
Setting any goal to GoalStatus.CANCELLED through set_goal_status() automatically propagates cancellation to goals that depend on it, recursively. Dependents are changed only while they are still GoalStatus.NOT_STARTED (the codebase's equivalent of OPEN). Goals already in progress are not changed by dependency propagation; their own request cancellation is handled independently. Callers therefore do not invoke dependency propagation separately.
Goal.run() checks is_stopped():
- at the beginning of a goal run;
- before entering child-goal execution;
- in the loop that schedules/restarts child goals.
When the flag becomes true, the goal stops scheduling additional work after the current control point. This prevents later goals from starting, but it does not interrupt arbitrary Python code that is already running.
The plan may be executing child goals concurrently. The executor waits on each submitted future with future.result(). Therefore, if STOP arrives while a child goal is active, the parent can remain blocked until that child returns. The stop flag is then observed by the surrounding scheduling loop.
6. Chatbot loop behavior after STOP
In PythonServer/src/core/extensions/skillsets/ChatBot/ChatBot2.py, the main chatbot loop does this:
execution_response = plan.run()
set_stopped(False)
The stop flag is reset immediately after plan.run() returns. The chatbot then continues its normal response/input-loop handling and sends the updated plan to Blazor.
This means STOP is a one-shot cooperative interruption signal. It does not leave the plan permanently stopped, and it does not shut down the chatbot process.
The initial chatbot setup also clears the flag before starting normal plan execution. A newly created chatbot plan therefore starts with the stop flag false.
What STOP currently does
- Sends a stop request from the Razor session UI to the active AI runtime.
- Attempts to cancel registered de²generator LLM requests belonging to
IN_PROGRESSgoals, concurrently. - Sets the Python plan stop flag to
True. - Prevents additional goals from being scheduled once the current execution reaches a stop check.
- Resets the flag to
Falseafterplan.run()returns. - Leaves the chatbot session and plan available for further input.
What STOP currently does not do
- It does not set
SessionData.IsStopping. - It does not terminate or remove the AI program/container.
- It does not clear the chat or plan.
- It does not remove the session or navigate away.
- It does not cancel arbitrary non-de²generator LLM/network requests.
- It does not interrupt arbitrary synchronous Python work immediately.
- It does not target a specific plan or goal when multiple execution contexts share the same Python process.
- It does not cancel requests belonging to not-started, child-scheduling, completed, or failed goals.
Relation to the agent cancellation token
Agents maintain an active request registry. For each de²generator LLM call, provider_http creates a request token, adds it to the de²generator metadata, and registers the token and endpoint on the owning agent. Agent.cancel_llm_request() uses that registry to call de²generator /v1/cancel with the token in the JSON body. The request is removed from the registry in the provider's finally block.
The STOP path now invokes Plan.cancel_llm_requests() from the Python /stopplan handler before setting the global stop flag. The C# STOP handler remains unchanged; it still calls /stopplan and does not carry an individual request token.
Practical timing
The observed timing is therefore:
- The user clicks STOP.
- The Python stop handler concurrently sends de²generator cancellation requests for active
IN_PROGRESSgoal agents. - The handler sets the global stop flag.
- De²generator-backed LLM calls can be interrupted by de²generator; other active LLM calls, tool calls, or blocking futures may continue until they return or reach their own timeout/error boundary.
- The next
Goal.run()stop check prevents further scheduling. - The chatbot loop resets the flag and waits for/handles the next input.
The UI request itself is awaited by the Razor handler, but this only waits for the stop endpoint response. It does not mean the active plan execution has already finished when the handler returns.