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
Toolkitclass 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
Evaluatorclass launches a subprocess with a persistent REPL, sending code toreplscript.pyfor 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
predictfrom 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
Evaluatorfor persistent code execution.
circularcontext.py
- Implements
CircularBufferandCircularContextfor limited-size message/context history. - Has a function called
predictthat (as coded) simply wraps anopenai_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.pyfile has apredict()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/responsesis not a standard endpoint—should be/v1/chat/completionsfor 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
CircularContextis 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
-
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
-
Fix/Improve OpenAI API Calls: Switch to
/v1/chat/completionswith correct format (see OpenAI docs). -
Function Call Handling: Add parsing for function/tool calls in the format returned by OpenAI/other LLMs.
-
Better Error/Edge Case Handling: For missing keys, invalid tool calls, API failures, etc.
-
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.