The LLM Agent Wrote it’s Own Agent Loop

In the previous chapter, I partially made an LLM Agent. One that is missing the agent loop.

In this chapter, I use ipython to manually tell the LLM Agent to write it’s own agent loop. It succeeded (GPT-4.1).

There are two sections:

  • Write Your Own Agent Loop – contains the ipython session, and
  • Example Use.

Write Your Own Agent Loop

In [1]: load "inferscript.py"

This is where I define the task.

In [3]: c.push_easy_input_message("The CWD is a Python project of an LLM Agent.
      ⋮  Ignore the md file. Look only at the PY files in the CWD. Determine
      ⋮  what is missing to make an LLM agent.")

Note: each response is labelled in order.

In [4]: r1 = infer_iter(c.to_list(), tk, e)
In [5]: r1
Out[5]: 
[{'id': 'omitted',
  'type': 'message',
  'status': 'completed',
  'content': [{'type': 'output_text',
    'annotations': [],
    'logprobs': [],
    'text': "To determine what is missing from your Python project to make a functional LLM (
Large Language Model) agent, I will need to analyze the actual content of your current workin
g directory (CWD) and inspect the `.py` files.\n\n**Next Steps:**\n1. List the `.py` files in
 your directory.\n2. Review their contents to see what components exist and what's missing fo
r a complete LLM agent.\n\nLet me start by listing the `.py` files in your CWD."}],
  'role': 'assistant'}]

Note: each response is manually added to the context.

In [6]: for r in r1:
   ...:     c.push_custom(r)
   ...: 
In [7]: r2 = infer_iter(c.to_list(), tk, e)
Permission needed to use tool call.
{'arguments': '{"code":"import os\\n[filename for filename in os.listdir() if '
              'filename.endswith(\'.py\')]"}',
 'call_id': 'omitted',
 'id': 'omitted',
 'name': 'py_runsource_exec',
 'status': 'completed',
 'type': 'function_call'}
Grant permission? (y/n): y
Proceeding...
In [8]: for r in r2:
   ...:     c.push_custom(r)
   ...: 
In [9]: r3 = infer_iter(c.to_list(), tk, e)

Note: that failed. Why?

In [10]: r3
Out[10]: 
[{'id': 'omitted',
  'type': 'message',
  'status': 'completed',
  'content': [{'type': 'output_text',
    'annotations': [],
    'logprobs': [],
    'text': "It looks like there was an issue retrieving the list of `.py` files in your proj
ect directory (CWD). I need at least the filenames (and ideally the content or structure) of 
your Python files to analyze what's present and what's missing for an LLM agent.\n\nCould you
 please:\n- Provide the list of `.py` files in your project directory  \nOR\n- Share the main
 code structure or key `.py` files' contents?\n\nOnce I have that information, I can identify
 what is missing for a working LLM agent."}],
  'role': 'assistant'}]

Let me see why it failed.

In [11]: c.to_list()
Out[11]: 
[{'content': 'The CWD is a Python project of an LLM Agent. Ignore the md file. Look only at t
he PY files in the CWD. Determine what is missing to make an LLM agent.',
  'role': 'user',
  'type': 'message'},
 {'id': 'omitted',
  'type': 'message',
  'status': 'completed',
  'content': [{'type': 'output_text',
    'annotations': [],
    'logprobs': [],
    'text': "To determine what is missing from your Python project to make a functional LLM (
Large Language Model) agent, I will need to analyze the actual content of your current workin
g directory (CWD) and inspect the `.py` files.\n\n**Next Steps:**\n1. List the `.py` files in
 your directory.\n2. Review their contents to see what components exist and what's missing fo
r a complete LLM agent.\n\nLet me start by listing the `.py` files in your CWD."}],
  'role': 'assistant'},
 {'id': 'omitted',
  'type': 'function_call',
  'status': 'completed',
  'arguments': '{"code":"import os\\n[filename for filename in os.listdir() if filename.endsw
ith(\'.py\')]"}',
  'call_id': 'omitted',
  'name': 'py_runsource_exec'},
 {'call_id': 'omitted',
  'output': '[Function tool call returned empty result.]',
  'type': 'function_call_output'}]

Of course, the Python interpreter does not return the result. The LLM must print it out manually.

In [12]: c.push_easy_input_message("The Function tool call returned an empty result because y
       ⋮ result because you did not print the result of the python script you
       ⋮ provided.")

Try again.

In [13]: r3 = infer_iter(c.to_list(), tk, e)
Permission needed to use tool call.
{'arguments': '{"code":"import os\\nprint([filename for filename in '
              'os.listdir() if filename.endswith(\'.py\')])"}',
 'call_id': 'omitted',
 'id': 'omitted',
 'name': 'py_runsource_exec',
 'status': 'completed',
 'type': 'function_call'}
Grant permission? (y/n): y
Proceeding...

That looks good. I can see the print function now.

In [14]: for r in r3:
    ...:     c.push_custom(r)
    ...: 
In [15]: r4 = infer_iter(c.to_list(), tk, e)
Permission needed to use tool call.
{'arguments': '{"code":"with open(\'toolkit.py\') as f:\\n    '
              'print(f.read())"}',
 'call_id': 'omitted',
 'id': 'omitted',
 'name': 'py_runsource_exec',
 'status': 'completed',
 'type': 'function_call'}
Grant permission? (y/n): y
Proceeding...
Permission needed to use tool call.
{'arguments': '{"code":"with open(\'evaluator.py\') as f:\\n    '
              'print(f.read())"}',
 'call_id': 'omitted',
 'id': 'omitted',
 'name': 'py_runsource_exec',
 'status': 'completed',
 'type': 'function_call'}
Grant permission? (y/n): y
Proceeding...
Permission needed to use tool call.
{'arguments': '{"code":"with open(\'inferscript.py\') as f:\\n    '
              'print(f.read())"}',
 'call_id': 'omitted',
 'id': 'omitted',
 'name': 'py_runsource_exec',
 'status': 'completed',
 'type': 'function_call'}
Grant permission? (y/n): y
Proceeding...
Permission needed to use tool call.
{'arguments': '{"code":"with open(\'replscript.py\') as f:\\n    '
              'print(f.read())"}',
 'call_id': 'omitted',
 'id': 'omitted',
 'name': 'py_runsource_exec',
 'status': 'completed',
 'type': 'function_call'}
Grant permission? (y/n): y
Proceeding...
Permission needed to use tool call.
{'arguments': '{"code":"with open(\'circularcontext.py\') as f:\\n    '
              'print(f.read())"}',
 'call_id': 'omitted',
 'id': 'omitted',
 'name': 'py_runsource_exec',
 'status': 'completed',
 'type': 'function_call'}
Grant permission? (y/n): y
Proceeding...

Sanity check the context size. How many LLM API JSON objects we have in the context.

In [16]: len(c.to_list())
Out[16]: 8
In [17]: for r in r4:
    ...:     c.push_custom(r)
    ...: 
In [18]: len(r4)
Out[18]: 11

In [19]: r5 = infer_iter(c.to_list(), tk, e)
In [22]: print(r5[0]['content'][0]['text'])

Note that the following text is the LLM Response.


Here is a summary of what’s present in your project and what’s missing to make a full LLM agent:


What You Have

toolkit.py

  • Defines a Toolkit class for registering and managing tools (e.g., for tool-augmented LLM agents).
  • Includes a function to add a Python REPL tool, allowing the agent to execute Python code.

evaluator.py

  • Evaluator class launches a subprocess with a persistent REPL, sending code to replscript.py for execution; handles chunked code input/output via base64.
  • Used to execute dynamic Python code safely in isolation from the main process.

inferscript.py

  • Sets up toolkit, evaluator, and context.
  • Implements permission system for tool calls.
  • Main entry point for “inference iterations” with a function infer_iter, using the toolkit and evaluator based on context and tool predictions.
  • Relies on predict from the context module for next actions.

replscript.py

  • Implements a custom REPL that can safely evaluate Python code and return output in a chunked, encoded form.
  • Used by Evaluator for persistent code execution.

circularcontext.py

  • Implements CircularBuffer and CircularContext for limited-size message/context history.
  • Has a function called predict that (as coded) simply wraps an openai_request (but this is not a locally defined model interface).
  • Defines OpenAI API request logic and a skeletal interface for LLM use, with tool support.

What’s MISSING for a complete LLM Agent

1. Missing: LLM Prompt Cycle and Agent Logic

  • No main agent loop that accepts user input, pushes messages, and coordinates iterative reasoning (i.e., input → LLM → tool use → next step → output).
  • No clear decision logic for running the agent interactively or as a service.

2. Model Results Handling

  • The circularcontext.py file has a predict() function that expects the OpenAI API to return a final result directly. However, the actual OpenAI API endpoint used and its formatting m ay be incorrect/outdated (/v1/responses is not a standard endpoint—should be /v1/chat/completions for chat models).
  • Not enough error handling or model configuration.

3. Function/Tool Call Parsing and Integration

  • No general handler for parsing LLM output (e.g., function/tool calls as OpenAI-format JSON or similar).
  • No routine for converting raw model outputs into tool calls and iteratively feeding results back.

4. No User Interface or Entrypoint

  • No CLI, Web, or API interface to interact with the agent.
  • No main application (e.g., main() function) that wires up message input, tool results, and chat context.

5. Missing Authentication/Secrets Management

  • The OpenAI key is fetched from the environment, but no handling is included for missing or invalid keys.

6. LLM Output-Driven Memory/Context Feedback

  • The agent lacks a mechanism for using conversation history to drive LLM output over multiple rounds.
  • The CircularContext is present, but there’s no function stitching all pieces into a complete prompt-tool/result-feedback loop.

Summary Table

Component Present? Missing?
OpenAI LLM Integration Partial Endpoint outdated/incomplete
Tooling (Python REPL) Present
Context Management Present Used, but not in full agent loop
Agent Loop (main logic) No Core interactive/iterative loop
Function Call Parsing Partial Needs full OpenAI format support
User Interface No CLI/webapp or API
Error Handling Minimal Needs improvement

What to Add

  1. Main Agent Loop: A driver script that:

    • Accepts user input
    • Builds prompt with history/context
    • Sends to LLM
    • Parses model/tool call outputs
    • Executes tools when needed
    • Stores results in context, repeats as needed
  2. Fix/Improve OpenAI API Calls: Switch to /v1/chat/completions with correct format (see OpenAI docs).

  3. Function Call Handling: Add parsing for function/tool calls in the format returned by OpenAI/other LLMs.

  4. Better Error/Edge Case Handling: For missing keys, invalid tool calls, API failures, etc.

  5. Entry Point: A main() or CLI loop for end-to-end interaction.


Would you like:

  • Example code wiring this together?
  • More details on a specific piece?
  • Recommendations for libraries/frameworks to simplify your agent logic?

END OF LLM RESPONSE

In [23]: for r in r5:
    ...:     c.push_custom(r)
    ...: 

I understand what you have in mind. The first proposed task is just what I want.

In [24]: c.push_easy_input_message("I want you to create the file agent.py and
       ⋮ add into it the main agent loop.")
In [25]: r6 = infer_iter(c.to_list(), tk, e)
In [26]: print(r6[0]['content'][0]['text'])

The following text is the LLM Response.


Here is a design for agent.py that wires up your tool/system components into a simple main agent loop. This implementation will:

  • Accept user input via CLI
  • Add it to the circular context buffer
  • Send the context/history to the LLM (using your API/client)
  • Check the response for function/tool calls
  • If present, execute the tool and append the result to the context
  • Otherwise, print/display the LLM output as the agent’s reply
  • Continue until stopped (Ctrl+C or empty input)

I’ll add clear comments for the key parts.
Note: This code assumes that the function call output from the LLM follows the OpenAI format.


# agent.py

import toolkit
import evaluator
import inferscript
import circularcontext as cc

def display(msg):
    print(f"\n=== Agent ===\n{msg}\n")

def main():
    # Initialize toolkit, evaluator, and context
    tk = toolkit.Toolkit()
    evaluator_inst = evaluator.Evaluator()
    context = cc.CircularContext()

    # Register tools for the agent (e.g., Python REPL)
    toolkit.add_py_repl_tools(tk)

    print("Welcome to your LLM agent. Type 'exit' to quit.\n")

    while True:
        try:
            user_input = input("You: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nExiting agent.")
            break

        if user_input.lower() in ("exit", "quit"):
            break

        if not user_input:
            continue

        # Add user input to context buffer
        context.push_easy_input_message(content=user_input, role="user")

        # Main reasoning and tool-use step
        outputs = inferscript.infer_iter(context.to_list(), tk, evaluator_inst)

        # Handle agent outputs (could be message(s) and/or function call output(s))
        agent_reply = ""
        for out in outputs:
            if out.get("type") == "message":
                # Standard agent message
                content = out.get("content", "")
                agent_reply = content
                context.push_custom(out)
            elif out.get("type") == "function_call_output":
                # Result from running a tool call
                output = out.get("output", "")
                # Optionally, print tool result or just show final answer
                agent_reply += f"\n[Tool Output]: {output}"
                context.push_custom(out)

        # Display the agent response(s)
        if agent_reply.strip():
            display(agent_reply)

    print("Goodbye.")

if __name__ == "__main__":
    main()

How it works: – The loop accepts user input and processes it. – It uses inferscript.infer_iter() as the core cycle for reasoning and tool-use, handling both message and tool call outputs. – It prints out LLM messages and tool outputs, and adds all outputs to the shared context buf fer (“memory”). – The conversation/context history is kept short (up to the CircularContext capacity).

To use: 1. Save the above to agent.py. 2. In your terminal, run: python agent.py 3. You can interact with your agent and use Python tool calls! 4. To exit, type exit or press Ctrl+C.

If you want improvements (e.g. more advanced output parsing, formatted output, streaming, etc .), let me know!


END OF LLM RESPONSE

In [27]: for r in r6:
    ...:     c.push_custom(r)
    ...: 
In [28]: c.push_easy_input_message("I approve of your agent.py proposal. I want
       ⋮  you to create and write the agent.py directly to the CWD.")
In [29]: r7 = infer_iter(c.to_list(), tk, e)
Permission needed to use tool call.
{'arguments': '{"code":"AGENT_PY = \'\'\'\\n# agent.py\\n\\nimport '
              'toolkit\\nimport evaluator\\nimport inferscript\\nimport '
              'circularcontext as cc\\n\\ndef display(msg):\\n    '
              'print(f\\"\\\\n=== Agent ===\\\\n{msg}\\\\n\\")\\n\\ndef '
              'main():\\n    # Initialize toolkit, evaluator, and '
              'context\\n    tk = toolkit.Toolkit()\\n    evaluator_inst = '
              'evaluator.Evaluator()\\n    context = '
              'cc.CircularContext()\\n\\n    # Register tools for the agent '
              '(e.g., Python REPL)\\n    '
              'toolkit.add_py_repl_tools(tk)\\n\\n    print(\\"Welcome to your '
              'LLM agent. Type \'exit\' to quit.\\\\n\\")\\n\\n    while '
              'True:\\n        try:\\n            user_input = input(\\"You: '
              '\\").strip()\\n        except (EOFError, '
              'KeyboardInterrupt):\\n            print(\\"\\\\nExiting '
              'agent.\\")\\n            break\\n\\n        if '
              'user_input.lower() in (\\"exit\\", \\"quit\\"):\\n            '
              'break\\n\\n        if not user_input:\\n            '
              'continue\\n\\n        # Add user input to context '
              'buffer\\n        '
              'context.push_easy_input_message(content=user_input, '
              'role=\\"user\\")\\n\\n        # Main reasoning and tool-use '
              'step\\n        outputs = '
              'inferscript.infer_iter(context.to_list(), tk, '
              'evaluator_inst)\\n\\n        # Handle agent outputs (could be '
              'message(s) and/or function call output(s))\\n        '
              'agent_reply = \\"\\"\\n        for out in '
              'outputs:\\n            if out.get(\\"type\\") == '
              '\\"message\\":\\n                # Standard agent '
              'message\\n                content = out.get(\\"content\\", '
              '\\"\\")\\n                agent_reply = '
              'content\\n                '
              'context.push_custom(out)\\n            elif out.get(\\"type\\") '
              '== \\"function_call_output\\":\\n                # Result from '
              'running a tool call\\n                output = '
              'out.get(\\"output\\", \\"\\")\\n                # Optionally, '
              'print tool result or just show final answer\\n                '
              'agent_reply += f\\"\\\\n[Tool Output]: '
              '{output}\\"\\n                '
              'context.push_custom(out)\\n\\n        # Display the agent '
              'response(s)\\n        if agent_reply.strip():\\n            '
              'display(agent_reply)\\n\\n    print(\\"Goodbye.\\")\\n\\nif '
              '__name__ == \\"__main__\\":\\n    main()\\n\'\'\'\\n\\nwith '
              'open(\'agent.py\', \'w\') as f:\\n    f.write(AGENT_PY)"}',
 'call_id': 'omitted',
 'id': 'omitted',
 'name': 'py_runsource_exec',
 'status': 'completed',
 'type': 'function_call'}
Grant permission? (y/n): y
Proceeding...

Example Use

Setup

Note that in the final agent.py file, I had to manually correct the newlines which python generated becaues the LLM Agent used \n but it had to use \\n as the code goes through a Python interpreter.

I also had to do something I forgot to do. That is change inferscript.py into an actual infer.py file.

So, here is the infer.py file I forgot.

import json

import toolkit
import evaluator 
import circularcontext as cc

from pprint import pprint

def make_functioncalloutput(call_id, content):
    if content == "":
        content = "[Function tool call returned empty result.]"
    return {
        "call_id": call_id,
        "output": content,
        "type": "function_call_output"
    }

def make_functioncalloutput_denied(call_id):
    return {
        "call_id": call_id,
        "output": "[Function tool call permission denied.]",
        "type": "function_call_output"
    }

def ask_for_permission(toolcall):
    print("Permission needed to use tool call.")
    pprint(toolcall)
    while True:
        answer = input("Grant permission? (y/n): ").strip().lower()
        if answer in ('y', 'yes'):
            print("Proceeding...")
            return True
        elif answer in ('n', 'no'):
            print("Aborting function tool call.")
            return False
        else:
            print("Please enter 'y' or 'n'.")

def infer_iter(context, toolkit, evaluator):
    outputs = cc.predict(context=context, tools=toolkit.tools())
    r = outputs.copy()
    for o in outputs:
        if o['type'] == "function_call":
            if toolkit.match(o['name']) == "py_runsource_exec":
                if ask_for_permission(o):
                    args = json.loads(o['arguments'])
                    result = evaluator.runsource_exec(args['code'])
                    r.append(make_functioncalloutput(o['call_id'], result))
                else:
                    r.append(make_functioncalloutput_denied(o['call_id']))
    return r

Problems

First, I had to manually change inferscript to infer in the agent.py.

Then I had to change

        # Display the agent response(s)
        if agent_reply.strip():
            display(agent_reply)

to

        # Display the agent response(s)
        if agent_reply:
            display(agent_reply)

Trying agent.py

python3 agent.py 
Welcome to your LLM agent. Type 'exit' to quit.

You: Use Python to calculate the gravitational acceleration at the ISS.
Permission needed to use tool call.
{'arguments': '{"code":"# Constants\\nG = 6.67430e-11  # Gravitational '
              'constant, m^3 kg^-1 s^-2\\nM_earth = 5.972e24  # Mass of the '
              'Earth, kg\\nR_earth = 6371e3     # Radius of the Earth, '
              'm\\nh_iss = 420e3        # Average altitude of ISS, m\\n\\n# '
              'Distance from the center of the Earth to the ISS\\nr_iss = '
              'R_earth + h_iss\\n\\ng_iss = G * M_earth / '
              'r_iss**2\\nprint(f\\"Gravitational acceleration at the ISS '
              'altitude: {g_iss:.2f} m/s^2\\")"}',
 'call_id': 'omitted',
 'id': 'omitted',
 'name': 'py_runsource_exec',
 'status': 'completed',
 'type': 'function_call'}
Grant permission? (y/n): y
Proceeding...

=== Agent ===

[Tool Output]: Gravitational acceleration at the ISS altitude: 8.64 m/s^2


You: exit
Goodbye.