PromptisePromptise
Docs
GitHub
Promptise - AI Framework LogoPromptise

The foundation layer for agentic intelligence. Build, secure and operate autonomous AI systems with Promptise Foundry.

pip install promptise

[01] Foundry

  • MCPcast
  • The Promptise Agent
  • Reasoning Engine
  • MCP
  • Agent Runtime
  • Prompt Engineering
  • Execution Engine
  • Agent Identity

[02] Resources

  • Documentation
  • GitHub
  • Guides
  • Learning Paths
  • Questions

[03] Company

  • About
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Subprocessors

© 2026 Promptise by Manser Ventures. All rights reserved.

Open source · Python

← Guides> AI Engineering

Self-Modifying AI Agent in Python: Open Mode & Limits

Build a self-modifying AI agent in Python with Promptise open mode: meta-tools, hot reload, the guardrails that bound it, and honest measured limits.

Level
Advanced
Reading time
16 min
Published
Oct 11, 2026
By
Promptise Team
  • AI Agents
  • Self-Modifying Agents
  • Open Mode
  • AI Governance
  • Python
  • Promptise Foundry

A self-modifying AI agent is one that can change itself while it runs: rewrite its own instructions, write a new tool, connect to another server, or add a trigger, without a redeploy. Promptise Foundry calls this open mode, and it is off by default for good reason. This guide turns it on for one narrow power, writing tools, runs the mechanism end to end (an agent writes a Python tool with typed parameters, the runtime hot-reloads it, and the next run calls it), and fires every guardrail on purpose so you can see what stops the agent. Then it measures, on Promptise Foundry 1.3.3 with gpt-5-mini, the detail that decides whether this works at all: a tool the agent writes is usable from its next run, not the one that wrote it. Told so, the agent wrote one tool and used it correctly in 8 of 8 trials. Told to write the tool and call it at once, it wrote 2.9 tools per run on average and 6 of 8 runs failed.

[01]

What is a self-modifying AI agent in Promptise?

An agent that can edit its own configuration at runtime. In Promptise Foundry every long-running agent is an AgentProcess, and a process has an execution mode. In the default ExecutionMode.STRICT, the instructions, tools and servers you gave it are frozen for its whole life. In ExecutionMode.OPEN, the process hands the agent a set of meta-tools it can call to change itself. A normal tool acts on the world (query a database, send an email); a meta-tool acts on the agent. When the agent calls one, the process does a hot reload: it rebuilds the agent with the new configuration and keeps the conversation. The run that asked for the change finishes with the tools it started with; the next run gets the new ones.

This is not the same as a self-improving agent that learns from feedback. That agent keeps its tools and adjusts its strategy from what worked; see Self-Improving AI Agent in Python. A self-modifying agent changes its own code and configuration. Open mode is for a narrow set of cases: an ops agent that writes a helper it will need again, a research agent that attaches a data source it just discovered. For almost everything else, strict mode with the tools you chose is the right answer.


[02]

The meta-tools an open-mode agent gets

Open mode exposes up to 14 meta-tools, each gated by a flag in OpenModeConfig, plus three that appear only when the matching governance subsystem is on. Grouped by what they touch:

Meta-tool

Permission flag

What it changes

modify_instructions

allow_identity_change

Rewrites the system prompt.

create_tool

allow_tool_creation

Defines a new Python tool, with parameters declared as a JSON schema.

connect_mcp_server

allow_mcp_connect

Attaches another MCP server and its tools.

add_trigger, remove_trigger

allow_trigger_management

Adds or drops cron, event, message, webhook and file-watch triggers.

spawn_process, list_processes

allow_process_spawn (default off)

Creates and lists child processes in the runtime.

store_memory, search_memory, forget_memory

allow_memory_management

Writes, searches and deletes long-term memory.

get_secret, check_budget, check_mission

secrets / budget / mission enabled

Reads a scoped secret, remaining budget, mission progress.

list_capabilities

always on

Reports the agent's current tools, triggers and instructions.

The first three groups mutate the agent and trigger a hot reload; the rest read or manage state. list_capabilities is always available. Everything else appears only when its flag is on, so an operator grants exactly the powers a job needs. The full input schema for each is in the Meta-Tools reference.


[03]

What you need

  • Python 3.10 or newer.

  • Promptise Foundry 1.3.3 or newer. Open mode, the runtime and the meta-tools are all in the base install:

>_Terminal
pip install "promptise>=1.3.3"
export OPENAI_API_KEY="sk-..."
  • An API key for a model provider. The examples use openai:gpt-5-mini.


[04]

Build a self-modifying agent, step by step

Step 01

Turn on open mode with guardrails

A self-modifying agent is one line of execution mode and a block of guardrails. This config lets the agent write tools and nothing else: it can't rewrite its own prompt, reach new servers, or change its schedule, and the tools it writes run sandboxed. The caps bound how far it can go:

Pythonself_improving_agent.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"""An open-mode agent that authors one tool when it's missing one."""

from promptise.runtime import ExecutionMode, JournalConfig, OpenModeConfig, ProcessConfig

INSTRUCTIONS = """\
You are a support assistant. You start with no tools, and you must never guess
a number you could compute.

When a task needs an exact calculation:
1. If you have a tool for it, call it and reply with the result in one sentence.
2. If you don't, create ONE with create_tool. Declare its arguments in
   `parameters` as a JSON object schema, and define `def run(...) -> str` with
   exactly those arguments. The code runs without imports, so use plain Python
   (for money, work in integer cents). A new tool is available from your next
   run, not this one: after creating it, reply "Tool <name> is ready; ask me again."

If create_tool returns an error, do not retry it: explain the problem instead.
"""


def make_config() -> ProcessConfig:
    return ProcessConfig(
        model="openai:gpt-5-mini",
        instructions=INSTRUCTIONS,
        execution_mode=ExecutionMode.OPEN,
        open_mode=OpenModeConfig(
            allow_identity_change=False,   # may not rewrite its own prompt
            allow_tool_creation=True,      # may author new tools
            allow_mcp_connect=False,       # may not reach out to new servers
            allow_trigger_management=False,  # may not change its own schedule
            allow_memory_management=False,
            max_custom_tools=3,            # at most three tools in its lifetime
            sandbox_custom_tools=True,     # tools run with restricted builtins
            max_rebuilds=5,
        ),
        journal=JournalConfig(level="full", path=".promptise/journal"),
    )

Each flag that is False removes the matching meta-tool entirely, so the agent never even sees connect_mcp_server or add_trigger. max_custom_tools, max_instruction_length and max_rebuilds cap the rest, and sandbox_custom_tools decides how agent-written code runs. The instructions carry two facts the model can't guess: the sandbox allows no imports, and a new tool is usable from the next run, not the current one. The experiment below shows what happens without them.

Step 02

See the mechanism: write a tool and call it

Before trusting a model with it, drive the meta-tools yourself. This script turns on open mode (only allow_identity_change is off), then calls create_tool the way the agent would, with three typed parameters declared as a JSON object schema and a run function that takes them:

Pythonmechanism.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
"""Drive the meta-tools directly (no model) to show the self-modification mechanism."""

import asyncio

from promptise.runtime import AgentProcess, ExecutionMode, OpenModeConfig, ProcessConfig
from promptise.runtime.meta_tools import create_meta_tools

TOOL_CODE = '''
def run(instalment: float, count: int, final: float) -> str:
    """Total of `count` equal instalments plus a final one, exact to the cent."""
    cents = count * round(instalment * 100) + round(final * 100)
    return f"${cents / 100:.2f}"
'''

PARAMETERS = {
    "type": "object",
    "properties": {
        "instalment": {"type": "number", "description": "One regular instalment, in dollars."},
        "count": {"type": "integer", "description": "How many regular instalments."},
        "final": {"type": "number", "description": "The final instalment, in dollars."},
    },
    "required": ["instalment", "count", "final"],
}


async def main() -> None:
    process = AgentProcess(
        "support",
        ProcessConfig(
            model="openai:gpt-5-mini",
            instructions="ORIGINAL",
            execution_mode=ExecutionMode.OPEN,
            open_mode=OpenModeConfig(allow_identity_change=False, max_custom_tools=3),
        ),
    )
    await process.start()
    tools = {t.name: t for t in create_meta_tools(process)}
    print("meta-tools offered:", sorted(tools))

    created = await tools["create_tool"].ainvoke(
        {"tool_name": "total_paid", "tool_description": "Exact total of equal instalments plus a final one.",
         "parameters": PARAMETERS, "python_code": TOOL_CODE}
    )
    print("create_tool:", created)
    print("agent tools after rebuild:", [t.name for t in process._agent.tools])

    authored = next(t for t in process._custom_tools if t.name == "total_paid")
    print("the model is offered:", authored.tool_call_schema.model_json_schema()["properties"])
    print("authored tool call:", await authored.ainvoke({"instalment": 149.99, "count": 6, "final": 87.45}))
    try:
        await authored.ainvoke({"instalment": 149.99, "count": "six", "final": 87.45})
    except Exception as exc:
        print("bad arguments:", type(exc).__name__, "-", str(exc).splitlines()[1])
    await process.stop()


asyncio.run(main())
Output
meta-tools offered: ['add_trigger', 'connect_mcp_server', 'create_tool', 'forget_memory', 'list_capabilities', 'remove_trigger', 'search_memory', 'store_memory']
create_tool: Tool 'total_paid' created. Rebuild successful
agent tools after rebuild: ['total_paid', 'create_tool', 'connect_mcp_server', 'add_trigger', 'remove_trigger', 'store_memory', 'search_memory', 'forget_memory', 'list_capabilities']
the model is offered: {'instalment': {'description': 'One regular instalment, in dollars.', 'title': 'Instalment', 'type': 'number'}, 'count': {'description': 'How many regular instalments.', 'title': 'Count', 'type': 'integer'}, 'final': {'description': 'The final instalment, in dollars.', 'title': 'Final', 'type': 'number'}}
authored tool call: $987.39
bad arguments: ToolArgumentError - - count: Input should be a valid integer, unable to parse string as an integer (got "six")

Read it top to bottom. The agent was offered eight meta-tools: no modify_instructions, because allow_identity_change=False, and no spawn_process, which is off by default. After create_tool, the agent's tool list starts with total_paid: the hot reload rebuilt the agent. The model is offered the tool's three parameters with their types and descriptions, the tool summed the instalments to the exact cent, and a call with count="six" was refused with a ToolArgumentError before run was called, with a message the model can act on. Before 1.3.3, create_tool ignored parameters and every agent-written tool took one input string; with empty parameters that is still what you get.

Step 03

Give it to a real model

Now wrap the same config in a process and hand the agent a task it has no tool for, twice. Each task is one trigger event, one run; the script then reads both runs back from the journal:

Pythonrun_once.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
"""Give the open-mode agent a task it has no tool for, twice, and read back what it did."""

import asyncio

from promptise.runtime import AgentProcess, FileJournal, TriggerEvent

from self_improving_agent import make_config

TASK = (
    "A customer paid six monthly instalments of 149.99 and a final instalment "
    "of 87.45. What exact total did they pay, in dollars?"
)


async def main() -> None:
    process = AgentProcess("support", config=make_config())
    await process.start()
    print("custom tools at start:", [t.name for t in process._custom_tools])

    for run in (1, 2):  # the same task twice: the second run has the tool the first one wrote
        await process.inject(TriggerEvent(trigger_id="manual", trigger_type="manual", payload={"task": TASK}))
        while process.status()["invocation_count"] + process.status()["dead_letter_count"] < run:
            await asyncio.sleep(0.5)
        await asyncio.sleep(0.5)

    print("custom tools now:", [t.name for t in process._custom_tools], "| rebuilds:", process.status()["rebuild_count"])
    print()
    for entry in await FileJournal(".promptise/journal").read("support"):
        if entry.entry_type == "invocation_start":
            print("--- run", entry.data.get("invocation", ""))
        elif entry.entry_type == "tool_call" and entry.data["tool"] == "create_tool":
            args = entry.data["args"]
            print(f"-> create_tool({args['tool_name']}, parameters={list(args['parameters'].get('properties', {}))})")
            print("   " + "\n   ".join(args["python_code"].strip().splitlines()))
        elif entry.entry_type == "tool_call":
            print(f"-> {entry.data['tool']}({entry.data['args']})")
        elif entry.entry_type == "tool_result":
            print(f"   = {entry.data['result'][:90]}")
        elif entry.entry_type == "invocation_result":
            print(f"reply: {entry.data['response']}\n")
    await process.stop()


asyncio.run(main())
Output
custom tools at start: []
custom tools now: ['calculate_total_paid'] | rebuilds: 1

--- run 1
-> create_tool(calculate_total_paid, parameters=['monthly_cents', 'monthly_count', 'final_cents'])
   def run(monthly_cents, monthly_count, final_cents):
       # All amounts are integer cents
       total_cents = monthly_cents * monthly_count + final_cents
       dollars = total_cents // 100
       cents = total_cents % 100
       return f"${dollars}.{cents:02d}"
   = Tool 'calculate_total_paid' created. Rebuild successful
reply: Tool calculate_total_paid is ready; ask me again.

--- run 2
-> calculate_total_paid({'monthly_cents': 14999, 'monthly_count': 6, 'final_cents': 8745})
   = $987.39
reply: $987.39

Run 1 had no tool, so the agent wrote one: integer cents, no imports, three declared parameters. The hot reload made it a real tool, but not for run 1, which finished with the tools it started with, so the agent said the tool was ready and stopped. Run 2 started with calculate_total_paid in its tool list, called it with the right arguments and answered $987.39. Four model calls in all. That's the shape that works: a self-modifying agent learns in one run and benefits in the next.


[05]

The guardrails that limit what a self-modifying agent can change

Open mode without guardrails is a liability; the guardrails are the reason it is usable at all. This script fires each one on purpose, driving the meta-tools directly so there is no model in the loop:

Pythonguardrails.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
"""Every open-mode guardrail, fired on purpose. No model calls — the meta-tools are driven directly."""

import asyncio

from promptise.runtime import AgentProcess, ExecutionMode, OpenModeConfig, ProcessConfig
from promptise.runtime.meta_tools import create_meta_tools


def new_process(**open_kw):
    cfg = ProcessConfig(
        model="openai:gpt-5-mini",
        instructions="ORIGINAL",
        execution_mode=ExecutionMode.OPEN,
        open_mode=OpenModeConfig(**open_kw),
    )
    return AgentProcess("p", cfg)


async def main() -> None:
    # 1. Permission flags decide which meta-tools exist at all.
    locked = new_process(
        allow_identity_change=False,
        allow_tool_creation=True,
        allow_mcp_connect=False,
        allow_trigger_management=False,
        allow_memory_management=False,
    )
    await locked.start()
    print("locked-down agent's meta-tools:", sorted(t.name for t in create_meta_tools(locked)))
    await locked.stop()

    # 2. A tool that reaches for an import, with the default sandbox.
    sandboxed = new_process(sandbox_custom_tools=True, max_custom_tools=5)
    await sandboxed.start()
    ct = {t.name: t for t in create_meta_tools(sandboxed)}["create_tool"]
    await ct.ainvoke({"tool_name": "reader", "tool_description": "read a file",
                      "parameters": {}, "python_code": "def run(**kwargs):\n    return open('/etc/hostname').read()"})
    await ct.ainvoke({"tool_name": "importer", "tool_description": "use os",
                      "parameters": {}, "python_code": "def run(**kwargs):\n    import os\n    return os.getcwd()"})
    reader = next(t for t in sandboxed._custom_tools if t.name == "reader")
    importer = next(t for t in sandboxed._custom_tools if t.name == "importer")
    print("sandboxed file read:  ", await reader.ainvoke({"input": ""}))
    print("sandboxed import os:  ", await importer.ainvoke({"input": ""}))
    print("non-object parameters:", await ct.ainvoke({"tool_name": "bad", "tool_description": "bad schema",
                                                      "parameters": {"type": "string"},
                                                      "python_code": "def run(text):\n    return text"}))

    # 3. Caps: instruction length and number of tools.
    capped = new_process(max_instruction_length=200, max_custom_tools=2)
    await capped.start()
    tools = {t.name: t for t in create_meta_tools(capped)}
    print("long instructions:    ", await tools["modify_instructions"].ainvoke({"new_instructions": "x" * 500}))
    for n in ("a", "b", "c"):
        r = await tools["create_tool"].ainvoke({"tool_name": n, "tool_description": n,
                                                 "parameters": {}, "python_code": "def run(**kwargs):\n    return 'ok'"})
        print(f"create tool {n!r}:       ", r.split(".")[0])
    await capped.stop()
    await sandboxed.stop()


asyncio.run(main())
Output
locked-down agent's meta-tools: ['create_tool', 'list_capabilities']
sandboxed file read:   Error executing tool: name 'open' is not defined
sandboxed import os:   Error executing tool: Imports are not allowed in sandboxed tools. Use only built-in Python operations.
non-object parameters: Error creating tool: parameters must be a JSON object schema, such as {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"]}
long instructions:     Error: instructions exceed max length (500 > 200)
create tool 'a':        Tool 'a' created
create tool 'b':        Tool 'b' created
create tool 'c':        Error: max custom tools (2) reached

Line by line:

  • Permission flags gate the tools. The locked-down agent was offered only create_tool and list_capabilities. A capability the agent never receives is one it can't misuse.

  • The sandbox blocks I/O and imports. With sandbox_custom_tools=True (the default), an agent-written tool runs with a restricted set of builtins: no open, no imports. A tool that reads /etc/hostname fails with name 'open' is not defined, and import os fails outright. In our check with sandbox_custom_tools=False, open was still missing and imports were limited to an allowlist (json, math, re, datetime, …): os and decimal were refused.

  • A tool's parameters must be an object schema. parameters={"type": "string"} was refused with an example of the expected shape, and no tool was created.

  • `max_instruction_length` caps a prompt rewrite. A 500-character rewrite under a 200-character limit is rejected before anything changes.

  • `max_custom_tools` caps how many tools exist. The third tool was refused.

Warning

The sandbox is defense in depth, not a security boundary. Promptise's own docs say so: Python's object model lets getattr-based traversal reach dangerous functions. For untrusted code, run tools in a real sandbox (see Code Execution Sandbox for AI Agents), not in open mode's restricted builtins.

Two more guardrails bound the agent without a flag: allowed_mcp_urls whitelists which servers connect_mcp_server may reach (empty means any), and max_dynamic_triggers caps how many triggers the agent can add. And max_rebuilds caps how many times the agent can change itself:

Pythonrebuild_limit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"""max_rebuilds: a change over the limit is refused, and stays refused after a restart."""

import asyncio

from promptise.runtime import AgentProcess, ExecutionMode, OpenModeConfig, ProcessConfig
from promptise.runtime.meta_tools import create_meta_tools


async def main() -> None:
    process = AgentProcess(
        "p",
        ProcessConfig(
            model="openai:gpt-5-mini",
            instructions="ORIGINAL",
            execution_mode=ExecutionMode.OPEN,
            open_mode=OpenModeConfig(max_rebuilds=1),
        ),
    )
    await process.start()
    rewrite = {t.name: t for t in create_meta_tools(process)}["modify_instructions"]
    print("rewrite 1:", await rewrite.ainvoke({"new_instructions": "REWRITE_1"}))
    print("rewrite 2:", await rewrite.ainvoke({"new_instructions": "REWRITE_2"}))
    print("instructions now:", process._agent._raw_instructions)
    await process.stop()
    await process.start()  # a deploy, a crash recovery, a restart_policy restart
    print("after restart:", process._agent._raw_instructions, "| rebuilds:", process.status()["rebuild_count"])
    await process.stop()


asyncio.run(main())
Output
rewrite 1: Rebuild successful
AgentProcess p: hot reload refused (instructions modified): Error: max rebuilds (1) reached; the change was not applied
rewrite 2: Error: max rebuilds (1) reached; the change was not applied
instructions now: REWRITE_1
after restart: REWRITE_1 | rebuilds: 1

The second rewrite was refused because max_rebuilds=1 was reached, and it was not made at all: the agent kept REWRITE_1, and so did the restarted process, whose rebuild count stayed at 1. The limit counts over the life of the process object, so stop()/start() and restart-policy restarts don't reset it. (Before 1.3.3 the refused change stayed staged and the next start applied it.) An operator's process.rollback() isn't refused by the limit; it undoes the agent's changes and counts as a rebuild.


[06]

How often does a model use open mode well?

One good run could be luck, so we ran the Step 3 setup 8 times with fresh processes on gpt-5-mini, each given the task twice, and counted what happened from each run's journal. A second condition swapped in the instructions an earlier draft of this guide used: write one tool, then call it to get the answer. Same model, same guardrails, same declared parameters:

What happened

Tool usable from the next run (Step 1)

Write the tool, then call it

Run 1: create_tool calls, average

1.0

14.6

Run 1: tools created, average (cap 3)

1.0

2.9

Run 1: hit the engine's iteration limit and raised

0 of 8

6 of 8

Run 2: called a tool it had written

8 of 8

8 of 8

A tool it wrote returned $987.39

8 of 8

1 of 8

Run 2 answered $987.39

8 of 8

7 of 8

With the Step 1 instructions the agent behaved the same way every time: one tool in run 1, no imports, then one call in run 2 that returned the right total. Told to write the tool and call it at once, it never could: the new tool wasn't in run 1's tool list, so the model kept calling create_tool, 14.6 times per run on average, mostly refused by the three-tool cap, until 6 of 8 runs hit the engine's limit of 15 reasoning steps and raised GraphIterationLimitError. The 2 run-1 answers that were right were computed by the model in its head. In run 2 it did call its tools, but 7 of the 8 trials had written from decimal import Decimal, which the sandbox refuses, so only 1 tool returned the total; the model then did the arithmetic itself, and got it right 7 times out of 8.

So the model wasn't the main problem; the instructions were. An open-mode agent needs to be told how its runtime works: when a new capability appears, and what agent-written code may do. The caps are what kept the bad condition a bounded waste rather than an incident: 6 failed runs and at most three useless tools per process. For actions with real consequences, put a person in the loop: Human-in-the-Loop Approval for AI Agents.


[07]

Honest limits

Open mode's mechanism works: tools are written with typed parameters, prompts rewritten, the agent hot-reloaded, the limits enforced. Plan around these:

  • A new tool is usable from the next run. The run that wrote it keeps its old tool list, and create_tool answers only Rebuild successful, so tell the model in its instructions. Without that, 6 of 8 runs above looped on create_tool and failed.

  • The model has to know the sandbox. Agent-written code gets no imports (and no open) by default; 7 of 8 trials of the create-then-call condition wrote tools that couldn't run.

  • The sandbox is not a security boundary. Restricted builtins can be escaped. Don't run untrusted agent code in-process; use a real sandbox.

  • Self-made changes live in memory. They survive stop()/start() and restart-policy restarts of the same process object, but a crash or redeploy builds the process from its config again. Persist anything you want to keep.

  • `max_rebuilds` is a lifetime count per process object. Once reached, every change is refused until an operator rolls back or a new process object starts.

  • Keep open mode out of multi-tenant and internet-facing agents. An agent that can write code and attach servers is a large surface; reserve it for trusted, single-purpose processes.


[08]

Frequently asked questions

What is a self-modifying AI agent?

An agent that changes its own configuration while running (its instructions, tools, connected servers or triggers) rather than only its outputs. In Promptise Foundry you get one by running an AgentProcess in ExecutionMode.OPEN, which gives the agent meta-tools that change the process and trigger a hot reload.

Can a self-modifying agent use a tool it just wrote?

From its next run. The hot reload builds a new agent with the tool, but the run that wrote it finishes with the tools it started with. Write that into the instructions, as Step 1 does, and let the next trigger use the tool.

Is a self-modifying AI agent safe to run in production?

Only with guardrails, and only for a narrow job. Disable every meta-tool the agent doesn't need, cap max_custom_tools and max_rebuilds, whitelist allowed_mcp_urls, and never run agent-written code in-process for anything untrusted. Keep it out of multi-tenant and public-facing systems. For most agents, strict mode is the right default.

How is open mode different from a self-improving agent?

A self-improving agent keeps a fixed set of tools and adjusts its strategy from feedback: it gets better at using what it has. An open-mode agent changes what it has: it writes new tools and rewrites its own prompt. Different mechanisms, different risks.

What happens to the conversation when the agent modifies itself?

The hot reload keeps it. The process snapshots the conversation, rebuilds the agent with the new instructions, tools or servers, and restores the conversation, so the next run continues the exchange with the new configuration.

Can I undo a change a self-modifying agent made?

Yes. process.rollback() clears the agent's own changes (instructions, written tools, added servers and triggers) and rebuilds it from its config. It counts as a rebuild but is never refused by max_rebuilds.


[09]

Where to go next

  • Meta-Tools and Configuration: every meta-tool, and ExecutionMode and OpenModeConfig in full.

  • Agent Runtime and Agent Processes: the process lifecycle open mode builds on.

  • Agent Manifests: declaring execution_mode and open_mode in a .agent YAML file.

  • AI Agent Guardrails in Python: input and output guards around any agent.

  • Code Execution Sandbox for AI Agents: running agent-written code safely.

  • Human-in-the-Loop Approval for AI Agents: a person on the actions that matter.

  • Deploy AI Agents in Production: running the process open mode lives inside.

Learning paths

Want more structure? Paths put guides in order, like a short course.

See the paths →

Keep going.

Browse every guide by topic and level, or follow a learning path that puts them in order.

All guidesLearning paths