Phase 3 Implementation Plan: Human-in-the-Loop (HITL)
This document outlines the development tasks required to implement the Human-in-the-Loop (HITL) functionality as described in 03-02-hitl.md
, building upon the foundational architecture established in Phases 1 and 2.
Task 1: Extend State Models for HITL
Objective: Update the core data models to support paused states, pending tool calls, and a new response type for HITL interventions.
- File:
src/sk_agents/ska_types.py
-
Changes:
1.Modify
AgentTaskItem
: Add an optional field to store tool calls that are pending approval.2.Modify# In AgentTaskItem class pending_tool_calls: list[dict] | None = None # Store serialized FunctionCallContent
AgentTask
: Add a "Canceled" status to thestatus
literal to handle rejections. 3.CreateHitlResponse
Model: Define a new Pydantic model for the response sent to the client when an intervention is required.python class HitlResponse(BaseModel): task_id: str session_id: str request_id: str message: str = "Human intervention required." approval_url: str rejection_url: str tool_calls: list[dict] # Serialized FunctionCallContent
Task 2: Implement Persistence Layer Extension
Objective: Enable the retrieval of tasks using a request_id
to support the resume endpoint.
- Files:
src/sk_agents/persistence/task_persistence_manager.py
src/sk_agents/persistence/in_memory_persistence_manager.py
- Changes:
- Update
TaskPersistenceManager
ABC: Add a new abstract method to find a task byrequest_id
. - Implement in
InMemoryPersistenceManager
: Implement the new method. This will likely require creating a new index (dictionary) to maprequest_id
totask_id
. Ensure this index is properly maintained with thread-safe locks.
- Update
Task 3: Implement HITL Trigger and Pause Logic
Objective: Modify the agent and handler to detect when an intervention is needed, pause the task, and persist the state.
- Files:
src/sk_agents/hitl/hitl_manager.py
src/sk_agents/tealagents/v1alpha1/agent.py
src/sk_agents/tealagents/v1alpha1/handler.py
- Changes:
- Update
hitl_manager.py
: For now, hardcodecheck_for_intervention
to returnTrue
if a tool call's name matches a predefined "high-risk" tool (e.g.,ShellCommand
). - Create Custom Exception: Define a custom exception, e.g.,
HitlInterventionRequired(Exception)
, to signal the need for HITL from the agent to the handler. - Modify
agent.py
: Ininvoke()
andinvoke_stream()
, after extracting tool calls, iterate through them. Ifcheck_for_intervention()
returnsTrue
for any of them, raiseHitlInterventionRequired
with the list of all tool calls from the LLM's response. - Modify
handler.py
: - Wrap theagent.invoke()
call in atry...except HitlInterventionRequired
block. - In theexcept
block: - SetAgentTask.status
to"Paused"
. - Create a newAgentTaskItem
for the assistant's turn, storing the pending tool calls from the exception into the newpending_tool_calls
field. - Persist the updatedAgentTask
using thepersistence_manager
. - Construct and return theHitlResponse
, generating the appropriate approval and rejection URLs (e.g.,/tealagents/v1alpha1/resume/{request_id}
).
- Update
Task 4: Create the HITL Resume Endpoint
Objective: Create a new API endpoint that the client can call to approve or reject a paused tool call.
- Files:
src/sk_agents/routes.py
src/sk_agents/app.py
src/sk_agents/tealagents/v1alpha1/handler.py
(or a newresume_handler.py
)
- Changes:
- Define Route in
routes.py
: Add a newPOST
route, e.g.,/tealagents/v1alpha1/resume/{request_id}
. - Create Resume Handler: Implement a new handler function for this route. This function will accept the
request_id
from the URL and a simple JSON body like{"action": "approve"}
or{"action": "reject"}
. - Integrate in
app.py
: Wire the new route to the new handler function in theAppV3
class.
- Define Route in
Task 5: Implement Resume and Rejection Logic
Objective: Implement the core logic within the resume handler to either continue execution or cancel the task.
- File:
src/sk_agents/tealagents/v1alpha1/handler.py
(or the new resume handler file) - Changes:
- Authorization: The request must go through the
RequestAuthorizer
to get theuser_id
. - Load Task: Use the new
persistence_manager.load_by_request_id()
method to fetch theAgentTask
. - Validation:
- If the task is not found, return 404.
- Verify the
user_id
from the authorizer matches thetask.user_id
. If not, return 403 Forbidden. - Verify the task status is "Paused". If not, return 409 Conflict.
- Handle Rejection (
action == "reject"
):- Update the
AgentTask.status
to"Canceled"
. - Add an
AgentTaskItem
to the history logging the rejection. - Persist the task.
- Return a confirmation response to the client.
- Update the
- Handle Approval (
action == "approve"
):- This is the most complex part, creating a new execution path.
- Retrieve the
pending_tool_calls
from the lastAgentTaskItem
. - Execute the tool calls using
asyncio.gather()
, just as the agent would have. - Create
ToolContent
objects from the results. - Add the
AgentTaskItem
with thepending_tool_calls
and a newAgentTaskItem
with theToolContent
results to the chat history. - Update the
AgentTask.status
to"Running"
. - Invoke the agent again with the updated chat history to get the final response from the LLM.
- Persist the final state (
AgentTask
and newAgentTaskItem
s). - Return the final
TealAgentsResponse
to the client.
- Authorization: The request must go through the
Task 6: Record Approval/Rejection
Objective: Ensure that the approval or rejection action is explicitly recorded in the task's history.
- File:
src/sk_agents/tealagents/v1alpha1/handler.py
(or the new resume handler file) - Changes:
- When handling an approval or rejection, create a new
AgentTaskItem
that explicitly records the action. - For approvals, this could be an item with
role: "user"
and content like{"action": "approve", "request_id": "..."}
. - For rejections, a similar item should be added before setting the state to "Canceled".
- This provides a clear audit trail within the conversation history.
- When handling an approval or rejection, create a new