agent-patterns-py 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. agent_patterns_py-0.1.0/.env.example +6 -0
  2. agent_patterns_py-0.1.0/.gitignore +45 -0
  3. agent_patterns_py-0.1.0/LICENSE +21 -0
  4. agent_patterns_py-0.1.0/PKG-INFO +92 -0
  5. agent_patterns_py-0.1.0/README.md +76 -0
  6. agent_patterns_py-0.1.0/docs/agent-language.md +74 -0
  7. agent_patterns_py-0.1.0/docs/capabilities.md +79 -0
  8. agent_patterns_py-0.1.0/docs/dependency-injection.md +86 -0
  9. agent_patterns_py-0.1.0/docs/models.md +57 -0
  10. agent_patterns_py-0.1.0/docs/multi-agent.md +77 -0
  11. agent_patterns_py-0.1.0/docs/safety.md +85 -0
  12. agent_patterns_py-0.1.0/examples/specialist_personas.py +167 -0
  13. agent_patterns_py-0.1.0/examples/time_aware_capability.py +74 -0
  14. agent_patterns_py-0.1.0/pyproject.toml +42 -0
  15. agent_patterns_py-0.1.0/src/agent_patterns/__init__.py +98 -0
  16. agent_patterns_py-0.1.0/src/agent_patterns/capabilities/__init__.py +9 -0
  17. agent_patterns_py-0.1.0/src/agent_patterns/capabilities/base.py +109 -0
  18. agent_patterns_py-0.1.0/src/agent_patterns/capabilities/plan_first.py +62 -0
  19. agent_patterns_py-0.1.0/src/agent_patterns/capabilities/progress_tracking.py +64 -0
  20. agent_patterns_py-0.1.0/src/agent_patterns/core/__init__.py +17 -0
  21. agent_patterns_py-0.1.0/src/agent_patterns/core/action.py +62 -0
  22. agent_patterns_py-0.1.0/src/agent_patterns/core/agent.py +240 -0
  23. agent_patterns_py-0.1.0/src/agent_patterns/core/context.py +54 -0
  24. agent_patterns_py-0.1.0/src/agent_patterns/core/environment.py +99 -0
  25. agent_patterns_py-0.1.0/src/agent_patterns/core/goal.py +22 -0
  26. agent_patterns_py-0.1.0/src/agent_patterns/core/memory.py +34 -0
  27. agent_patterns_py-0.1.0/src/agent_patterns/language/__init__.py +10 -0
  28. agent_patterns_py-0.1.0/src/agent_patterns/language/base.py +81 -0
  29. agent_patterns_py-0.1.0/src/agent_patterns/language/function_calling.py +92 -0
  30. agent_patterns_py-0.1.0/src/agent_patterns/language/json_action.py +103 -0
  31. agent_patterns_py-0.1.0/src/agent_patterns/multi_agent/__init__.py +3 -0
  32. agent_patterns_py-0.1.0/src/agent_patterns/multi_agent/registry.py +32 -0
  33. agent_patterns_py-0.1.0/src/agent_patterns/safety/__init__.py +8 -0
  34. agent_patterns_py-0.1.0/src/agent_patterns/safety/reversible.py +111 -0
  35. agent_patterns_py-0.1.0/src/agent_patterns/safety/staged.py +95 -0
  36. agent_patterns_py-0.1.0/src/agent_patterns/tools/__init__.py +15 -0
  37. agent_patterns_py-0.1.0/src/agent_patterns/tools/agent_tools.py +72 -0
  38. agent_patterns_py-0.1.0/src/agent_patterns/tools/persona.py +100 -0
  39. agent_patterns_py-0.1.0/src/agent_patterns/tools/planning.py +116 -0
  40. agent_patterns_py-0.1.0/src/agent_patterns/tools/registry.py +230 -0
  41. agent_patterns_py-0.1.0/src/agent_patterns/utils/__init__.py +8 -0
  42. agent_patterns_py-0.1.0/src/agent_patterns/utils/llm.py +100 -0
  43. agent_patterns_py-0.1.0/tests/__init__.py +0 -0
  44. agent_patterns_py-0.1.0/uv.lock +2556 -0
@@ -0,0 +1,6 @@
1
+ # Copy this file to .env and fill in your API keys
2
+ # Never commit .env to version control
3
+
4
+ # LLM providers
5
+ OPENAI_API_KEY=
6
+ ANTHROPIC_API_KEY=
@@ -0,0 +1,45 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .eggs/
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # uv
19
+ .uv/
20
+
21
+ # Environment variables
22
+ .env
23
+ .env.*
24
+ !.env.example
25
+
26
+ # IDE
27
+ .vscode/
28
+ .idea/
29
+ *.swp
30
+ *.swo
31
+
32
+ # Testing & coverage
33
+ .pytest_cache/
34
+ .coverage
35
+ htmlcov/
36
+ .mypy_cache/
37
+ .ruff_cache/
38
+
39
+ # Jupyter
40
+ .ipynb_checkpoints/
41
+ *.ipynb_checkpoints
42
+
43
+ # OS
44
+ .DS_Store
45
+ Thumbs.db
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Moiz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-patterns-py
3
+ Version: 0.1.0
4
+ Summary: Production-ready boilerplate for common agentic AI patterns in Python
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: litellm>=1.0
8
+ Requires-Dist: python-dotenv>=1.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: ipykernel>=6.0; extra == 'dev'
11
+ Requires-Dist: mypy>=1.10; extra == 'dev'
12
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
13
+ Requires-Dist: pytest>=8.0; extra == 'dev'
14
+ Requires-Dist: ruff>=0.4; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # agent-patterns
18
+
19
+ A Python framework for building production-ready AI agents using the GAME pattern (Goals, Actions, Memory, Environment).
20
+
21
+ ---
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ git clone https://github.com/your-username/agent-patterns.git
27
+ cd agent-patterns
28
+ uv sync
29
+ cp .env.example .env # add your API keys
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Quick start
35
+
36
+ ```python
37
+ import os
38
+ from agent_patterns import (
39
+ Agent, Goal,
40
+ PythonEnvironment,
41
+ AgentFunctionCallingActionLanguage,
42
+ PythonActionRegistry,
43
+ register_tool,
44
+ make_generate_response,
45
+ )
46
+
47
+ @register_tool(tags=["files"])
48
+ def list_files() -> list:
49
+ """List files in the current directory."""
50
+ return os.listdir(".")
51
+
52
+ @register_tool(tags=["files"])
53
+ def read_file(name: str) -> str:
54
+ """Read a file and return its contents."""
55
+ with open(name) as f:
56
+ return f.read()
57
+
58
+ @register_tool(tags=["system"], terminal=True)
59
+ def terminate(message: str) -> str:
60
+ """End the session with a final message."""
61
+ return message
62
+
63
+ agent = Agent(
64
+ goals=[
65
+ Goal(1, "Explore", "List and read files in the current directory."),
66
+ Goal(2, "Terminate", "Call terminate with a summary when done."),
67
+ ],
68
+ agent_language=AgentFunctionCallingActionLanguage(),
69
+ action_registry=PythonActionRegistry(tags=["files", "system"]),
70
+ generate_response=make_generate_response("openai/gpt-4o"),
71
+ environment=PythonEnvironment(),
72
+ )
73
+
74
+ memory = agent.run("What Python files are here and what do they do?")
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Docs
80
+
81
+ - [Switching models](docs/models.md)
82
+ - [Agent language strategies](docs/agent-language.md)
83
+ - [Capabilities](docs/capabilities.md)
84
+ - [Dependency injection](docs/dependency-injection.md)
85
+ - [Multi-agent](docs/multi-agent.md)
86
+ - [Safety patterns](docs/safety.md)
87
+
88
+ ---
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,76 @@
1
+ # agent-patterns
2
+
3
+ A Python framework for building production-ready AI agents using the GAME pattern (Goals, Actions, Memory, Environment).
4
+
5
+ ---
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ git clone https://github.com/your-username/agent-patterns.git
11
+ cd agent-patterns
12
+ uv sync
13
+ cp .env.example .env # add your API keys
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Quick start
19
+
20
+ ```python
21
+ import os
22
+ from agent_patterns import (
23
+ Agent, Goal,
24
+ PythonEnvironment,
25
+ AgentFunctionCallingActionLanguage,
26
+ PythonActionRegistry,
27
+ register_tool,
28
+ make_generate_response,
29
+ )
30
+
31
+ @register_tool(tags=["files"])
32
+ def list_files() -> list:
33
+ """List files in the current directory."""
34
+ return os.listdir(".")
35
+
36
+ @register_tool(tags=["files"])
37
+ def read_file(name: str) -> str:
38
+ """Read a file and return its contents."""
39
+ with open(name) as f:
40
+ return f.read()
41
+
42
+ @register_tool(tags=["system"], terminal=True)
43
+ def terminate(message: str) -> str:
44
+ """End the session with a final message."""
45
+ return message
46
+
47
+ agent = Agent(
48
+ goals=[
49
+ Goal(1, "Explore", "List and read files in the current directory."),
50
+ Goal(2, "Terminate", "Call terminate with a summary when done."),
51
+ ],
52
+ agent_language=AgentFunctionCallingActionLanguage(),
53
+ action_registry=PythonActionRegistry(tags=["files", "system"]),
54
+ generate_response=make_generate_response("openai/gpt-4o"),
55
+ environment=PythonEnvironment(),
56
+ )
57
+
58
+ memory = agent.run("What Python files are here and what do they do?")
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Docs
64
+
65
+ - [Switching models](docs/models.md)
66
+ - [Agent language strategies](docs/agent-language.md)
67
+ - [Capabilities](docs/capabilities.md)
68
+ - [Dependency injection](docs/dependency-injection.md)
69
+ - [Multi-agent](docs/multi-agent.md)
70
+ - [Safety patterns](docs/safety.md)
71
+
72
+ ---
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,74 @@
1
+ # Agent language strategies
2
+
3
+ `AgentLanguage` controls two things:
4
+
5
+ 1. **How GAME components are formatted into a prompt** sent to the LLM
6
+ 2. **How the LLM's response is parsed** back into an action
7
+
8
+ Swapping the language changes the prompting strategy without touching the agent loop.
9
+
10
+ ---
11
+
12
+ ## Function calling (recommended)
13
+
14
+ Uses the LLM's native tool/function calling API. The LLM returns a structured tool call — no text parsing needed.
15
+
16
+ ```python
17
+ from agent_patterns import AgentFunctionCallingActionLanguage
18
+
19
+ agent = Agent(
20
+ ...
21
+ agent_language=AgentFunctionCallingActionLanguage(),
22
+ )
23
+ ```
24
+
25
+ **When to use:** Almost always. More reliable, no prompt engineering needed to get structured output.
26
+
27
+ ---
28
+
29
+ ## JSON action blocks
30
+
31
+ The LLM outputs free-form reasoning text and embeds its action in a fenced ` ```action ``` ` block:
32
+
33
+ ```
34
+ I'll start by listing the files.
35
+
36
+ ```action
37
+ {
38
+ "tool": "list_files",
39
+ "args": {}
40
+ }
41
+ ```
42
+
43
+ ```python
44
+ from agent_patterns import AgentJsonActionLanguage
45
+
46
+ agent = Agent(
47
+ ...
48
+ agent_language=AgentJsonActionLanguage(),
49
+ )
50
+ ```
51
+
52
+ **When to use:**
53
+ - The model doesn't support native function calling
54
+ - You want the agent's reasoning visible in memory
55
+ - You need a model-agnostic fallback
56
+
57
+ ---
58
+
59
+ ## Custom language
60
+
61
+ Subclass `AgentLanguage` and implement two methods:
62
+
63
+ ```python
64
+ from agent_patterns import AgentLanguage, Prompt
65
+
66
+ class MyLanguage(AgentLanguage):
67
+ def construct_prompt(self, actions, environment, goals, memory) -> Prompt:
68
+ # build and return a Prompt however you like
69
+ ...
70
+
71
+ def parse_response(self, response: str) -> dict:
72
+ # return {"tool": "...", "args": {...}}
73
+ ...
74
+ ```
@@ -0,0 +1,79 @@
1
+ # Capabilities
2
+
3
+ Capabilities extend the agent loop at specific lifecycle points without modifying the `Agent` class. Pass a list of them when creating an agent:
4
+
5
+ ```python
6
+ from agent_patterns import Agent, PlanFirstCapability, ProgressTrackingCapability
7
+
8
+ agent = Agent(
9
+ ...
10
+ capabilities=[
11
+ PlanFirstCapability(),
12
+ ProgressTrackingCapability(track_frequency=3),
13
+ ],
14
+ )
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Built-in capabilities
20
+
21
+ ### PlanFirstCapability
22
+
23
+ Before the agent takes its first action, generates a detailed step-by-step execution plan and stores it in memory. Every subsequent prompt includes the plan, keeping the agent on track for multi-step tasks.
24
+
25
+ ```python
26
+ from agent_patterns import PlanFirstCapability
27
+
28
+ PlanFirstCapability(
29
+ plan_memory_type="system", # memory type for the plan entry (default: "system")
30
+ )
31
+ ```
32
+
33
+ ---
34
+
35
+ ### ProgressTrackingCapability
36
+
37
+ At the end of every N iterations, generates a progress report (what's done, any blockers, recommended next steps) and adds it to memory. Useful for long-running tasks where the agent needs to self-correct.
38
+
39
+ ```python
40
+ from agent_patterns import ProgressTrackingCapability
41
+
42
+ ProgressTrackingCapability(
43
+ memory_type="system", # memory type for report entries (default: "system")
44
+ track_frequency=1, # generate a report every N iterations (default: 1)
45
+ )
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Writing your own
51
+
52
+ Subclass `Capability` and override only the hooks you need:
53
+
54
+ ```python
55
+ from agent_patterns import Capability
56
+
57
+ class LoggingCapability(Capability):
58
+ def __init__(self):
59
+ super().__init__(name="Logging", description="Logs every action taken.")
60
+
61
+ def process_action(self, agent, action_context, action):
62
+ print(f"[LOG] action={action['tool']} args={action.get('args')}")
63
+ return action # always return the (optionally modified) value
64
+ ```
65
+
66
+ ### Available hooks
67
+
68
+ | Method | Called when | Return |
69
+ |--------|------------|--------|
70
+ | `init` | Once, before the loop starts | — |
71
+ | `start_agent_loop` | Start of each iteration | `False` to stop the loop |
72
+ | `process_prompt` | Before the prompt is sent to the LLM | modified `Prompt` |
73
+ | `process_response` | After the LLM responds, before parsing | modified response `str` |
74
+ | `process_action` | After parsing, before execution | modified action `dict` |
75
+ | `process_result` | After execution | modified result |
76
+ | `process_new_memories` | Before memory entries are committed | modified `list[dict]` |
77
+ | `end_agent_loop` | End of each iteration | — |
78
+ | `should_terminate` | After memory update | `True` to stop the loop |
79
+ | `terminate` | Once, when the agent stops | — |
@@ -0,0 +1,86 @@
1
+ # Dependency injection
2
+
3
+ Tools often need access to things beyond their explicit arguments — a database connection, an auth token, the agent's memory. Rather than hardcoding these or passing them through the LLM, the framework injects them automatically based on parameter naming conventions.
4
+
5
+ ---
6
+
7
+ ## How it works
8
+
9
+ When `PythonEnvironment` executes a tool, it inspects the function's parameters and injects matching values before calling it. Three conventions are supported:
10
+
11
+ | Parameter name | What gets injected |
12
+ |---------------|-------------------|
13
+ | `action_context` | The live `ActionContext` for this run |
14
+ | `action_agent` | The `Agent` instance |
15
+ | `_<key>` | `action_context.properties[key]` |
16
+
17
+ These parameters are **hidden from the LLM** — they never appear in the tool's JSON schema.
18
+
19
+ ---
20
+
21
+ ## action_context
22
+
23
+ Use this when you need direct access to the context object:
24
+
25
+ ```python
26
+ from agent_patterns import register_tool, ActionContext
27
+
28
+ @register_tool(tags=["llm"])
29
+ def summarise(action_context: ActionContext, text: str) -> str:
30
+ """Summarise the given text."""
31
+ generate = action_context.get("llm")
32
+ return generate(Prompt(messages=[
33
+ {"role": "user", "content": f"Summarise: {text}"}
34
+ ]))
35
+ ```
36
+
37
+ ---
38
+
39
+ ## _prefixed parameters
40
+
41
+ For injecting specific values from the context, prefix the parameter name with `_`:
42
+
43
+ ```python
44
+ @register_tool(tags=["db"])
45
+ def query(action_context: ActionContext, sql: str, _db_conn) -> list:
46
+ """Run a SQL query."""
47
+ return _db_conn.execute(sql).fetchall()
48
+ ```
49
+
50
+ Then pass `db_conn` (without the underscore) when running the agent:
51
+
52
+ ```python
53
+ agent.run(
54
+ "Find all users created this month.",
55
+ action_context_props={"db_conn": my_db_connection},
56
+ )
57
+ ```
58
+
59
+ `_db_conn` maps to `action_context.properties["db_conn"]` automatically.
60
+
61
+ ---
62
+
63
+ ## Common injected values
64
+
65
+ ```python
66
+ agent.run(
67
+ "Process the request.",
68
+ action_context_props={
69
+ "auth_token": "Bearer abc123", # → _auth_token
70
+ "db_conn": db, # → _db_conn
71
+ "user_config": config, # → _user_config
72
+ "agent_registry": registry, # → used by call_agent
73
+ },
74
+ )
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Using base Environment
80
+
81
+ If you use the base `Environment` instead of `PythonEnvironment`, no injection happens — tools receive only the arguments the LLM explicitly provided.
82
+
83
+ ```python
84
+ from agent_patterns import Environment # no DI
85
+ from agent_patterns import PythonEnvironment # with DI
86
+ ```
@@ -0,0 +1,57 @@
1
+ # Switching models
2
+
3
+ The framework uses [LiteLLM](https://docs.litellm.ai) under the hood, so any model LiteLLM supports works out of the box.
4
+
5
+ Use `make_generate_response` to create a callable bound to a specific model and pass it to your agent:
6
+
7
+ ```python
8
+ from agent_patterns import make_generate_response, Agent
9
+
10
+ agent = Agent(
11
+ ...
12
+ generate_response=make_generate_response("openai/gpt-4o"),
13
+ )
14
+ ```
15
+
16
+ ## Examples
17
+
18
+ ```python
19
+ # OpenAI
20
+ make_generate_response("openai/gpt-4o")
21
+ make_generate_response("openai/gpt-4o-mini")
22
+
23
+ # Anthropic
24
+ make_generate_response("anthropic/claude-opus-4-5")
25
+ make_generate_response("anthropic/claude-sonnet-4-5")
26
+
27
+ # Any other LiteLLM-supported provider
28
+ make_generate_response("groq/llama-3.1-70b-versatile")
29
+ ```
30
+
31
+ ## Different models per agent
32
+
33
+ Each agent gets its own `generate_response` callable, so you can mix models freely:
34
+
35
+ ```python
36
+ fast_agent = Agent(
37
+ ...
38
+ generate_response=make_generate_response("openai/gpt-4o-mini"),
39
+ )
40
+
41
+ smart_agent = Agent(
42
+ ...
43
+ generate_response=make_generate_response("anthropic/claude-opus-4-5"),
44
+ )
45
+ ```
46
+
47
+ ## Using a custom LLM function
48
+
49
+ `generate_response` is just a callable with the signature `(Prompt) -> str`. You can pass anything that matches:
50
+
51
+ ```python
52
+ def my_llm(prompt: Prompt) -> str:
53
+ # call any API you want
54
+ ...
55
+
56
+ agent = Agent(..., generate_response=my_llm)
57
+ ```
@@ -0,0 +1,77 @@
1
+ # Multi-agent
2
+
3
+ One agent can delegate tasks to other agents using the built-in `call_agent` tool and `AgentRegistry`.
4
+
5
+ ---
6
+
7
+ ## Setup
8
+
9
+ ### 1. Create your agents
10
+
11
+ ```python
12
+ from agent_patterns import Agent, Goal, PythonEnvironment, AgentFunctionCallingActionLanguage, PythonActionRegistry, make_generate_response
13
+
14
+ summariser = Agent(
15
+ goals=[Goal(1, "Summarise", "Summarise the provided text concisely then terminate.")],
16
+ agent_language=AgentFunctionCallingActionLanguage(),
17
+ action_registry=PythonActionRegistry(tags=["summariser", "system"]),
18
+ generate_response=make_generate_response("openai/gpt-4o-mini"),
19
+ environment=PythonEnvironment(),
20
+ )
21
+ ```
22
+
23
+ ### 2. Register them
24
+
25
+ ```python
26
+ from agent_patterns import AgentRegistry
27
+
28
+ registry = AgentRegistry()
29
+ registry.register_agent("summariser", summariser.run)
30
+ ```
31
+
32
+ ### 3. Give the coordinator the call_agent tool
33
+
34
+ ```python
35
+ # Import to register it in the global tool registry
36
+ import agent_patterns.tools.agent_tools # noqa: F401
37
+
38
+ coordinator = Agent(
39
+ goals=[Goal(1, "Coordinate", "Delegate tasks to specialist agents as needed.")],
40
+ agent_language=AgentFunctionCallingActionLanguage(),
41
+ action_registry=PythonActionRegistry(tags=["coordinator", "system"]),
42
+ generate_response=make_generate_response("openai/gpt-4o"),
43
+ environment=PythonEnvironment(),
44
+ )
45
+ ```
46
+
47
+ ### 4. Pass the registry at runtime
48
+
49
+ ```python
50
+ memory = coordinator.run(
51
+ "Summarise this report: ...",
52
+ action_context_props={"agent_registry": registry},
53
+ )
54
+ ```
55
+
56
+ ---
57
+
58
+ ## How call_agent works
59
+
60
+ When the coordinator calls `call_agent`:
61
+
62
+ - A **fresh memory** is created for the sub-agent (no memory leakage between agents)
63
+ - Context properties are forwarded, except `agent_registry` and `memory` (prevents infinite recursion)
64
+ - The sub-agent's final memory item is returned as the result
65
+
66
+ ---
67
+
68
+ ## Memory sharing patterns
69
+
70
+ Beyond basic message passing, you can control how much context a sub-agent receives by what you pass in `action_context_props`. Four patterns, in order of context shared:
71
+
72
+ | Pattern | How |
73
+ |---------|-----|
74
+ | Message passing | Default — fresh memory, task only |
75
+ | Memory handoff | Pass `memory=existing_memory` to `agent.run()` |
76
+ | Memory reflection | Copy sub-agent memories back to caller after the call |
77
+ | Selective sharing | Filter memories before passing them to the sub-agent |
@@ -0,0 +1,85 @@
1
+ # Safety patterns
2
+
3
+ Three patterns for agents that take real-world actions (calendar events, emails, API calls, etc.).
4
+
5
+ ---
6
+
7
+ ## Reversible actions
8
+
9
+ Wrap any action so it can be undone:
10
+
11
+ ```python
12
+ from agent_patterns import ReversibleAction
13
+
14
+ create_event = ReversibleAction(
15
+ execute_func=calendar.create_event,
16
+ reverse_func=lambda **record: calendar.delete_event(record["result"]["event_id"]),
17
+ )
18
+
19
+ # Execute
20
+ result = create_event.run(title="Sync", attendees=["alice@example.com"])
21
+
22
+ # Undo if needed
23
+ create_event.undo()
24
+ ```
25
+
26
+ The `reverse_func` receives the full execution record as kwargs:
27
+
28
+ ```python
29
+ {
30
+ "args": {...}, # original arguments
31
+ "result": {...}, # return value from execute_func
32
+ "timestamp": "...", # ISO timestamp
33
+ }
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Action transactions
39
+
40
+ Group multiple reversible actions into an atomic unit — all succeed or all roll back:
41
+
42
+ ```python
43
+ from agent_patterns import ActionTransaction, ReversibleAction
44
+
45
+ tx = ActionTransaction()
46
+ tx.add(create_event, title="Sync", attendees=["alice@example.com"])
47
+ tx.add(send_invite, event_id="abc123")
48
+
49
+ try:
50
+ tx.execute() # runs both; rolls back automatically on failure
51
+ tx.commit() # marks as final
52
+ except Exception as e:
53
+ print(f"Transaction failed and was rolled back: {e}")
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Staged execution
59
+
60
+ Collect actions for review before executing them. Useful when a human or a more capable model should sign off before side-effects occur:
61
+
62
+ ```python
63
+ from agent_patterns import StagedActionEnvironment, ReversibleAction
64
+
65
+ env = StagedActionEnvironment()
66
+
67
+ # Register a reviewer — returns True to approve, False to reject
68
+ def my_reviewer(task_id, transaction):
69
+ print(f"Approving {len(transaction._pending)} actions for task {task_id}")
70
+ return True # or False to reject
71
+
72
+ env.set_reviewer(my_reviewer)
73
+
74
+ # Stage actions
75
+ tx = env.stage_actions(task_id="meeting-setup")
76
+ tx.add(create_event, title="Team sync")
77
+ tx.add(send_invite, attendees=["team@example.com"])
78
+
79
+ # Review, then execute
80
+ if env.review_transaction("meeting-setup"):
81
+ tx.execute()
82
+ tx.commit()
83
+ ```
84
+
85
+ The reviewer can be a human approval step, an LLM policy check, or any callable that returns a bool.