SWE-agent Next Action Planner

SWE-agent caught my attention because it does not treat the model as the whole system. The important part is the interface around the model.

In the full framework, the agent can inspect repositories, issue commands, edit files, and test results inside a controlled environment. I did not rebuild that power. I rebuilt the operating pattern beneath it: a model receives an issue, chooses one allowed action, observes the result, and repeats until it can produce a plan, request review, or get blocked.


What This Agent Does

This agent is a small educational rebuild inspired by SWE-agent. It does not access GitHub, open a real repository, run commands, edit files, or execute tests.

The model receives fictional software issues and a fictional repository snapshot stored inside Python dictionaries. It can choose from a tiny set of simulated tools: list files, read a fake file, search fake file text, propose a patch plan, or finish the plan.

The final result is one of three outcomes:

PLAN_READY means the issue is safe enough for a review-only patch plan.

NEEDS_REVIEW means the issue touches sensitive, ambiguous, or patient-facing material.

BLOCKED means the request asks for unsafe access, credentials, production changes, or command execution.


The Control Desk

The clearest way to think about this build is a control desk.

A control desk does not let every request go straight to the machinery. It receives a request, checks whether the request is allowed, gives the operator only approved controls, records what happened, and escalates anything sensitive.

That is the Agent-Computer Interface idea in plain English. The model is not handed a full computer. It is handed a small panel of safe buttons.

For this rebuild, I wanted the button panel to be obvious. The agent can inspect a fake repository, but it cannot touch the real file system. It can propose a patch, but it cannot apply one. It can finish a plan, but it cannot submit code.

That distinction is the architecture lesson.


Vocabulary You Need

Agent-Computer Interface, or ACI: The controlled set of tools and formats an AI agent uses to interact with a computer-like environment.

REPL loop: A repeating cycle where the agent reads the current state, chooses one action, receives an observation, and continues.

Action parser: The code that turns the model output into a machine-readable instruction.

Safety gate: A deterministic check that blocks unsafe requests before the model is trusted to reason about them.

Review gate: A deterministic check that routes sensitive or ambiguous tasks to human review.


What This Is Not

This is not SWE-agent.

It does not run Docker, SWE-ReX, shell sessions, GitHub tasks, tests, file edits, or cybersecurity workflows. It also does not claim to fix real software.

This is a small LiteLLM-compatible educational rebuild. The goal is to isolate the action loop and show how a controlled interface changes the way an AI agent behaves.


Why I Built This

The AI model for agents matters, but the control surface matters too. A coding agent becomes useful when the system around it decides what the model can see, what it can do, what it must record, and when it has to stop.


What Actually Happened

What actually happened: DeepSeek followed the action loop more directly on the first working version. Gemini needed stronger JSON extraction and more explicit next-action guidance.

What broke: Gemini initially returned output that did not parse as a valid allowed action. DeepSeek used too many turns reading context before proposing a patch.

What surprised me: The multi-model test became more useful than two separate runs. Seeing Gemini and DeepSeek side by side made the provider differences obvious.

What I would change next: I would add a compact maker-checker layer. One model would propose the patch plan, and a separate checker would review only the evidence packet.

Why this matters: The build shows that agent reliability is not only about choosing a stronger model. The interface, parser, safety gate, and review path shape the behavior.


The Architecture: How It Works

The agent runs in five stages.

First, it loads LiteLLM configuration from environment variables. The model name stays outside the code.

Second, it checks each fictional issue with a local safety gate. If the request asks for credentials, production access, command execution, or destructive changes, the model is not called.

Third, it sends the safe issue to the model with a limited tool list.

Fourth, it parses the model response into one action.

Fifth, it runs that action against an in-memory fake repository and records the trace.

The key point is that every tool is simulated. The model can plan, but it cannot act on a real system.


What to Watch When You Run It

The most important thing to watch is the action trace.

For Northstar Inventory, both Gemini and DeepSeek listed files, read the validator, proposed a patch, and finished the plan. That produced PLAN_READY.

For Harbor Clinic, both models could inspect the fake notification file and propose a review plan. The local review gate still returned NEEDS_REVIEW because the issue was patient-facing.

For Ridgeway Logistics, the model was not called. The local safety gate blocked the request before planning because it asked for credentials and production access.

The provider difference was visible in the patch plans. Gemini produced more formatted review prose. DeepSeek stayed closer to a direct diff.


The Build: Step by Step

REQUIRED_ENV = ["LITELLM_BASE_URL", "MODEL_NAME", "LITELLM_API_KEY"]
TOOLS = ["list_files", "read_file", "search_repo", "propose_patch", "finish_plan"]
UNSAFE = ["shell", "credential", "secret", "token", "production", "deploy", "rm -", "drop table", "exploit", "repository clone", "remote push", "database"]
REVIEW = ["patient", "healthcare", "clinic", "medical", "legal", "compliance", "audit", "financial", "gdpr", "hipaa", "pii", "sensitive"]

What This Code Is Actually Doing

This is the control panel. REQUIRED_ENV defines the configuration the agent needs before it can call a model. TOOLS defines the only actions the model is allowed to choose. UNSAFE and REVIEW are local policy lists. They let the Python code block or escalate certain requests before trusting the model.

REPOS = {
    "Northstar Inventory": {
        "inventory/validator.py": "def validate_stock(qty):\n    return True  # BUG: accepts negative quantities",
        "tests/test_validator.py": "# fictional validator checks",
    },
    "Harbor Clinic": {
        "notifications/reminder.py": "REMINDER_TEXT = 'Your appointment is soon. Please arrive early.'",
        "notifications/sender.py": "def send_reminder(patient_id, message):\n    pass",
    },
    "Ridgeway Logistics": {"deploy/run.sh": "# fictional deployment note"},
}

What This Code Is Actually Doing

This is the fake repository. Instead of reading real files from the computer, the agent reads strings stored in memory. That keeps the build safe while still letting the model experience the shape of repository inspection.

SYSTEM_PROMPT = """You are a cautious software planning assistant using a simulated Agent-Computer Interface.
Allowed tools: list_files, read_file, search_repo, propose_patch, finish_plan.
Use one tool per turn. For a clear safe bug, list files, read the relevant file, propose a patch plan, then finish_plan.
For sensitive or patient-facing work, propose only a human-review plan, then finish_plan.
Never claim real execution, real file edits, GitHub access, command use, or production access.
Return only one JSON object with these fields: thought, action, target, argument, expected_result."""

What This Code Is Actually Doing

This prompt turns the model into a planner instead of an executor. It tells the model that it can choose only one tool per turn, and it names the exact JSON fields the parser expects. The prompt also repeats the liability boundary: no real execution, no real edits, and no real system access.

def run_tool(repo_name, action, target, argument):
    """Run one simulated ACI tool against an in-memory repository."""
    repo = REPOS.get(repo_name, {})
    if action == "list_files":
        return "Files: " + ", ".join(repo)
    if action == "read_file":
        return repo.get(target, "File not found in the fictional snapshot.")
    if action == "search_repo":
        matches = [name for name, body in repo.items() if argument.lower() in body.lower()]
        return "Matches: " + (", ".join(matches) if matches else "none")
    if action == "propose_patch":
        patches[repo_name] = argument[:500]
        return "Patch idea recorded. No files were modified."
    return "Plan complete."

What This Code Is Actually Doing

This function is the simulated Agent-Computer Interface. Each tool returns a plain-English observation. The important part is the propose_patch action. It records an idea, but it does not change a file. That keeps the build in planning mode.

def extract_json(raw_text):
    """Extract the first balanced JSON object from model text."""
    start = raw_text.find("{")
    if start == -1:
        raise ValueError
    depth = 0
    for index, char in enumerate(raw_text[start:], start):
        if char == "{":
            depth += 1
        elif char == "}":
            depth -= 1
            if depth == 0:
                return raw_text[start:index + 1]
    raise ValueError

What This Code Is Actually Doing

This is the parser fix that made Gemini reliable. Some models return structured JSON with extra formatting around it. This function finds the first complete JSON object and extracts it before validation.

def next_hint(trace):
    """Suggest a compact path through the simulated tool loop."""
    actions = [step["action"] for step in trace]
    if not actions:
        return "Start with list_files."
    if "read_file" not in actions:
        return "Read the most relevant file from the listed files."
    if "propose_patch" not in actions:
        return "Use propose_patch with a concise review-only patch plan."
    return "Use finish_plan."

What This Code Is Actually Doing

This function gives the model a nudge without taking away the loop. It keeps the run short by telling the model the likely next action. That mattered because one provider spent too many turns reading instead of proposing a patch.

def run_scenario(client, model_name, issue):
    """Run one fictional scenario through the simulated ACI loop."""
    name = issue["name"]
    description = issue["description"]
    print("\nScenario: " + name)
    if has_keyword(description, UNSAFE):
        print("Status: BLOCKED")
        print("Reason: Unsafe request blocked before model planning.")
        return

What This Code Is Actually Doing

This is the safety gate. If the issue contains unsafe language, the function stops before the model call. Ridgeway Logistics triggered this path because it asked for credentials and production access.

for model_name in [name.strip() for name in os.environ["MODEL_NAME"].split(",") if name.strip()]:
    print("\nActive model: " + model_name)
    for issue in ISSUES:
        run_scenario(client, model_name, issue)

What This Code Is Actually Doing

This is the multi-model test loop. I added it after testing became too slow one provider at a time. Now MODEL_NAME can contain multiple comma-separated model names, and one run tests them in sequence.


Errors I Hit During This Build

Error: Model response was not a valid allowed action.

Cause: Gemini returned a response that did not pass the original strict JSON parser. The parser expected the entire response to be raw JSON.

Fix: I added balanced JSON extraction so the code can pull the first complete JSON object from wrapped model output.

Error: DeepSeek reached PLAN_READY, but it spent the whole first version reading context.

Cause: The model loop allowed four turns, but the prompt did not guide the model toward propose_patch and finish_plan quickly enough.

Fix: I added next_action_hint, increased the loop to six turns, then confirmed both providers reached the expected outcomes.

Error: [WinError 5] Access is denied: ‘__pycache__’

Cause: python -m py_compile tried to write a cache folder in a protected location.

Fix: I switched to a no-write syntax check using Python ast.parse.

Error: SyntaxError: unterminated string literal (detected at line 212)

Cause: A PowerShell replacement inserted a literal newline inside a print statement while adding multi-model support.

Fix: I repaired the string to use \n inside the quoted text, then reran the syntax check.

Error: fatal: detected dubious ownership in repository

Cause: Git saw the repository owner as the normal Windows account while the sandbox user was different.

Fix: I added the repository to Git safe-directory config, then committed and pushed normally.


Why This Matters

The value of this build is not that it writes code. It does not write code.

The value is that it shows the boundary between reasoning and action. That boundary is where governance, safety, cost control, review, and enterprise trust live.

A model with unrestricted tools is a risk surface. A model behind a controlled interface is an architecture.

That is the difference this rebuild makes visible.


Tools Used

SWE-agent (MIT): https://github.com/SWE-agent/SWE-agent
LiteLLM (MIT for open-source portions): https://github.com/BerriAI/litellm
NosisTech Agent Engineering (MIT): https://github.com/nosistech/agent-engineering


(c) 2026 NosisTech LLC. Licensed under CC BY 4.0. Use freely, just credit us.

WEEKLY BUILD NOTES

One documented agent build in your inbox, every week

Real systems, real code, the errors left in. No spam, unsubscribe anytime.