Skip to main content

Code Agent

An IDE-based agent that can write and edit code and request the user to execute code.

Dashboard

Recreating Cursor-style 'Waiting for approval...' tools

Dashboard

When we want the agent to do something that requires user approval we want to:

  1. Prompt the user of the action we want to take
  2. Put the agent in a pending idle state
  3. Wait for user rejection / approval
  4. Continue

1. Create the agent

We want to give the agent 3 tools, the ability to think/make a plan, the ability to write code, and the ability to request the user to execute code.

Note that edit_code is the only tool that does any real work in this example, think is simply a way to let the agent log its thoughts and request_code_execution will simply put the agent in the idle pending state until the code has been executed (in the client) or rejected and the result submitted with the approve_or_reject endpoint.

agent.py

import os
from typing import Any
from dotenv import load_dotenv
from factorial import (
BaseAgent,
AgentContext,
ExecutionContext,
gpt_41_mini,
deferred_result,
)


load_dotenv()


class IdeAgentContext(AgentContext):
code: str


def think(thoughts: str) -> str:
"""Think deeply about the task and plan your next steps before executing"""
return thoughts


def edit_code(
find: str,
find_start_line: int,
find_end_line: int,
replace: str,
agent_ctx: IdeAgentContext,
) -> tuple[str, dict[str, Any]]:
"""
Edit code in a file

Arguments:
find: The text to find and replace
find_start_line: The start line number where the 'find' text is located
find_end_line: The end line number where the 'find' text is located
replace: The text to replace the 'find' text with
"""
lines = agent_ctx.code.split("\n")
start_idx = find_start_line - 1
end_idx = find_end_line - 1
if start_idx < 0 or end_idx >= len(lines) or start_idx > end_idx:
return "Error: Invalid line numbers", {
"error": "Line numbers out of range or invalid",
"total_lines": len(lines),
}

existing_text = "\n".join(lines[start_idx : end_idx + 1])

# Check if the find text matches what's at those line numbers
if find not in existing_text:
return (
f"Error: Text '{find}' not found at lines {find_start_line}-{find_end_line}",
{
"error": "Find text not found at specified lines",
"existing_text": existing_text,
},
)

new_text = existing_text.replace(find, replace)
new_lines = lines[:start_idx] + new_text.split("\n") + lines[end_idx + 1 :]

# Update the agent context with the modified code
agent_ctx.code = "\n".join(new_lines)

return (
f"Code successfully edited: replaced '{find}' with '{replace}' at lines {find_start_line}-{find_end_line}",
{
"find": find,
"find_start_line": find_start_line,
"find_end_line": find_end_line,
"replace": replace,
"new_code": agent_ctx.code,
},
)


@deferred_result(timeout=300.0) # 5-minute timeout waiting for user decision
def request_code_execution(
response_on_reject: str, agent_ctx: AgentContext, execution_ctx: ExecutionContext
) -> None:
"""
Request the code to be run. The use must approve this request before the code is run.

Parameters
----------
response_on_reject : str
A message the agent should send if the user rejects the execution request.
"""
pass


instructions = """
You are an IDE assistant that helps with coding tasks. You can write, read, analyze, and execute code.
For anything non-trivial, always start by making a plan for the coding task.

The code will be displayed with line numbers in the following format. The code you write should not
contain line numbers.

[1]def hello_world():
[2] print("Hello, world!")

In your final response, just clearly and consisely explain what you did without writing any code.
The code changes will be shown to the user in a diff editor.
"""


class IDEAgent(BaseAgent[IdeAgentContext]):
def __init__(self):
super().__init__(
context_class=IdeAgentContext,
instructions=instructions,
tools=[think, edit_code, request_code_execution],
model=gpt_41_mini,
)

def prepare_messages(self, agent_ctx: IdeAgentContext) -> list[dict[str, Any]]:
if agent_ctx.turn == 0:
messages = [{"role": "system", "content": self.instructions}]
if agent_ctx.messages:
messages.extend(
[message for message in agent_ctx.messages if message["content"]]
)

code = "\n".join(
[f"[{i + 1}]{line}" for i, line in enumerate(agent_ctx.code.split("\n"))]
)
messages.append(
{
"role": "user",
"content": f"Code file with line numbers:\n{code}\n---\nQuery: {agent_ctx.query}",
}
)
else:
messages = agent_ctx.messages

return messages


ide_agent = IDEAgent()

The agent now has the ability to think, write code, and request the user to execute the code.

2. Register the runner

orchestrator.py

from factorial import Orchestrator, AgentWorkerConfig
from agent import ide_agent

orchestrator = Orchestrator(openai_api_key=os.getenv("OPENAI_API_KEY"))

orchestrator.register_runner(
agent=ide_agent,
agent_worker_config=AgentWorkerConfig(workers=50, turn_timeout=120),
)

if __name__ == "__main__":
orchestrator.run()

register_runner spins up a pool of workers that pull tasks from Redis and drive the agent.

3. Expose an API & WebSocket

server.py

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import HTTPException
from pydantic import BaseModel
import json
from starlette.websockets import WebSocket, WebSocketDisconnect
from agent import basic_agent
from orchestrator import orchestrator

app = FastAPI()

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)


@app.websocket("/ws/{user_id}")
async def websocket_updates(websocket: WebSocket, user_id: str):
await websocket.accept()

try:
async for update in orchestrator.subscribe_to_updates(owner_id=user_id):
await websocket.send_text(json.dumps(update))
except WebSocketDisconnect:
print(f"WebSocket disconnected for user_id={user_id}")


class EnqueueRequest(BaseModel):
user_id: str
message_history: list[dict[str, str]]
query: str
code: str

@app.post("/api/enqueue")
async def enqueue(request: EnqueueRequest):
task = ide_agent.create_task(
owner_id=request.user_id,
payload=IdeAgentContext(
messages=request.message_history,
query=request.query,
turn=0,
code=request.code,
),
)

await orchestrator.enqueue_task(agent=ide_agent, task=task)
return {"task_id": task.id}


class CancelRequest(BaseModel):
user_id: str
task_id: str

@app.post("/api/cancel")
async def cancel_task_endpoint(request: CancelRequest):
try:
await orchestrator.cancel_task(task_id=request.task_id)
return {
"success": True,
"message": f"Task {request.task_id} marked for cancellation",
}
except Exception as e:
return {"success": False, "error": str(e)}


class ApproveOrRejectRequest(BaseModel):
user_id: str
task_id: str
tool_call_id: str
result: str


@app.post("/api/approve_or_reject")
async def approve_or_reject_endpoint(request: CompleteToolRequest):
"""Complete a deferred tool call with the provided result."""
try:
success = await orchestrator.complete_deferred_tool(
task_id=request.task_id,
tool_call_id=request.tool_call_id,
result=request.result,
)
if success:
return {"success": True}
raise HTTPException(status_code=500, detail="Unable to complete deferred tool.")
except Exception as e:
print(f"Failed to complete deferred tool call {request.tool_call_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))