simple-agent-loop 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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(uv build:*)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1 @@
1
+ ANTHROPIC_API_KEY=
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Secrets
13
+ .env
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,195 @@
1
+ # How to Implement an Agent Loop
2
+
3
+ An agent loop lets a language model use tools repeatedly until it finishes a task. The model decides what to do, calls tools, sees results, and keeps going until it has an answer. This guide covers the core concepts and data structures needed to build one from scratch in any language.
4
+
5
+ ## Core Data Structure: The Session
6
+
7
+ A session is a list of messages. Each message has a role or type and some content:
8
+
9
+ ```
10
+ { role: "system", content: "..." } -- system prompt
11
+ { role: "user", content: "..." } -- user input
12
+ { role: "assistant", content: "..." } -- model text output
13
+ { type: "thinking", content: "..." } -- model reasoning (if supported)
14
+ { type: "tool_call", name: "...", id: "...", input: {...} }
15
+ { type: "tool_result", id: "...", output: "..." }
16
+ ```
17
+
18
+ This is a generic format. It does not match any specific API directly -- you translate to/from the API format at the boundary. This keeps your session representation clean and portable.
19
+
20
+ ## 1. Initialize a Session
21
+
22
+ Create a session with a system prompt and the user's message:
23
+
24
+ ```
25
+ function init_session(system_prompt, user_prompt):
26
+ return {
27
+ messages: [
28
+ { role: "system", content: system_prompt },
29
+ { role: "user", content: user_prompt },
30
+ ]
31
+ }
32
+ ```
33
+
34
+ ## 2. Convert to API Format
35
+
36
+ Most model APIs expect messages grouped by role, with tool calls and tool results packed into specific block structures. Write a conversion function that:
37
+
38
+ - Skips system messages (pass them separately)
39
+ - Groups consecutive `thinking`, `assistant`, and `tool_call` messages into a single assistant message with multiple content blocks
40
+ - Groups consecutive `tool_result` messages into a single user message with multiple content blocks
41
+
42
+ ```
43
+ function to_api_messages(messages):
44
+ api_messages = []
45
+ assistant_blocks = []
46
+ tool_result_blocks = []
47
+
48
+ -- When hitting a boundary (user message, or switching between
49
+ -- assistant-side and tool-result-side), flush the accumulated
50
+ -- blocks into a single message.
51
+
52
+ for each message:
53
+ if system: skip
54
+ if user: flush both buffers, add user message
55
+ if assistant: flush tool results, add text block to assistant buffer
56
+ if thinking: flush tool results, add thinking block to assistant buffer
57
+ if tool_call: flush tool results, add tool_use block to assistant buffer
58
+ if tool_result: flush assistant buffer, add to tool result buffer
59
+
60
+ flush remaining buffers
61
+ return api_messages
62
+ ```
63
+
64
+ The key insight is that what your session stores as separate messages may need to be combined into a single API message with multiple content blocks.
65
+
66
+ ## 3. Parse the Response
67
+
68
+ The model returns a response with content blocks. Parse each block back into your generic message format:
69
+
70
+ ```
71
+ function parse_response(api_response):
72
+ messages = []
73
+ for each block in api_response.content:
74
+ if block is thinking: append { type: "thinking", content: block.thinking }
75
+ if block is text: append { role: "assistant", content: block.text }
76
+ if block is tool_use: append { type: "tool_call", name: ..., id: ..., input: ... }
77
+ return messages
78
+ ```
79
+
80
+ ## 4. Execute Tool Calls
81
+
82
+ Tool handlers are just functions. Map tool names to handler functions, then execute them:
83
+
84
+ ```
85
+ function execute_tool_calls(tool_calls, tool_handlers):
86
+ results = []
87
+ for each tool_call:
88
+ handler = tool_handlers[tool_call.name]
89
+ try:
90
+ output = handler(tool_call.input)
91
+ catch error:
92
+ output = "Error: " + error.message
93
+ results.append({ type: "tool_result", id: tool_call.id, output: output })
94
+ return results
95
+ ```
96
+
97
+ Tool calls within a single response are independent of each other, so you can execute them in parallel (threads, promises, goroutines, etc).
98
+
99
+ ## 5. The Loop
100
+
101
+ The agent loop ties everything together. Each iteration: call the model, add its messages to the session, check for tool calls, execute them, add results, repeat.
102
+
103
+ ```
104
+ function agent_loop(invoke_model, tools, session, tool_handlers):
105
+ loop:
106
+ api_messages = to_api_messages(session.messages)
107
+ api_response = invoke_model(tools, api_messages)
108
+
109
+ new_messages = parse_response(api_response)
110
+ append new_messages to session
111
+
112
+ tool_calls = filter new_messages for type "tool_call"
113
+ if no tool_calls: break -- model is done
114
+
115
+ results = execute_tool_calls(tool_calls, tool_handlers)
116
+ append results to session
117
+
118
+ return session
119
+ ```
120
+
121
+ That's the entire loop. The model drives the control flow -- it decides when to call tools and when to stop (by responding with just text and no tool calls).
122
+
123
+ ## 6. Defining Tools
124
+
125
+ Tools are defined as schemas that tell the model what's available. Each tool has a name, description, and input schema:
126
+
127
+ ```
128
+ {
129
+ name: "read_file",
130
+ description: "Read the contents of a file at the given path.",
131
+ input_schema: {
132
+ type: "object",
133
+ properties: {
134
+ path: { type: "string", description: "File path to read" }
135
+ },
136
+ required: ["path"]
137
+ }
138
+ }
139
+ ```
140
+
141
+ Pass the tool definitions to the model on each call. The corresponding handler is just a function that takes the input and returns a string:
142
+
143
+ ```
144
+ function read_file(input):
145
+ return file_system.read(input.path)
146
+ ```
147
+
148
+ ## 7. Subagents
149
+
150
+ An agent can use other agents as tools. A subagent is just a tool handler that runs its own agent loop:
151
+
152
+ ```
153
+ function summarize(input):
154
+ sub_session = init_session(
155
+ "You are a summarizer. Output only the summary.",
156
+ input.text
157
+ )
158
+ result = agent_loop(invoke_model, [], sub_session, {})
159
+ return response(result).content
160
+ ```
161
+
162
+ Register it as a tool handler like any other function. The outer agent calls "summarize" as a tool, the handler spins up an inner agent loop, and returns the result. The outer agent sees a simple string tool result and has no idea a whole agent loop ran inside.
163
+
164
+ This composes naturally:
165
+ - A coordinator agent can dispatch to multiple specialist subagents
166
+ - Subagents can have their own tools (or no tools at all)
167
+ - Subagents can have different system prompts, models, or iteration limits
168
+ - You can run multiple subagents in parallel when the tool calls are independent
169
+
170
+ ## 8. Context Management
171
+
172
+ Sessions grow over time. Old thinking blocks and tool call inputs can be large and become less relevant as the conversation progresses. You can compact them:
173
+
174
+ - Walk the message list backwards, counting assistant responses
175
+ - Once N assistant responses have appeared after a message, truncate its content to a short prefix
176
+ - Only compact thinking and tool_call messages -- keep user messages, assistant text, and tool results intact
177
+ - Mark compacted messages so you don't re-process them
178
+
179
+ This keeps the session under control during long-running agent loops without losing the structural flow of the conversation.
180
+
181
+ ## Putting It Together
182
+
183
+ The full implementation is around 200 lines in most languages. The essential pieces:
184
+
185
+ | Component | Purpose |
186
+ |---|---|
187
+ | Session | Ordered list of generic messages |
188
+ | `to_api_messages` | Translate generic messages to API format |
189
+ | `parse_response` | Translate API response back to generic messages |
190
+ | `execute_tool_calls` | Run tool handlers, collect results |
191
+ | `agent_loop` | The loop: call model, execute tools, repeat |
192
+
193
+ Everything else -- logging, session forking, compaction, subagents -- is layered on top of these five pieces.
194
+
195
+ The model is the control flow. Your code just provides the loop scaffold and tool execution. The model decides what to do, when to use tools, and when to stop.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tom MacWright
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,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: simple-agent-loop
3
+ Version: 0.1.0
4
+ Summary: A minimal agent loop for tool-using language models
5
+ Project-URL: Homepage, https://github.com/tmcw/agent-loop
6
+ Project-URL: Repository, https://github.com/tmcw/agent-loop
7
+ Author: Tom MacWright
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.10
20
+ Provides-Extra: anthropic
21
+ Requires-Dist: anthropic>=0.79.0; extra == 'anthropic'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # simple_agent_loop
25
+
26
+ A minimal agent loop for tool-using language models. ~250 lines. Handles
27
+ message format conversion, parallel tool execution, session compaction,
28
+ and subagent composition.
29
+
30
+ ## Install
31
+
32
+ ```
33
+ pip install simple-agent-loop
34
+ ```
35
+
36
+ ## Setup
37
+
38
+ ```python
39
+ import anthropic
40
+ import json
41
+ import simple_agent_loop as sal
42
+
43
+ client = anthropic.Anthropic() # uses ANTHROPIC_API_KEY env var
44
+
45
+ def invoke_model(tools, session):
46
+ kwargs = dict(
47
+ model="claude-sonnet-4-5",
48
+ max_tokens=16000,
49
+ messages=session["messages"],
50
+ )
51
+ if "system" in session:
52
+ kwargs["system"] = session["system"]
53
+ if tools:
54
+ kwargs["tools"] = tools
55
+ return client.messages.create(**kwargs).to_dict()
56
+ ```
57
+
58
+ ## Hello World
59
+
60
+ No tools, single turn -- the model just responds:
61
+
62
+ ```python
63
+ session = init_session(
64
+ system_prompt="You are a helpful assistant.",
65
+ user_prompt="Say hello in three languages.",
66
+ )
67
+ result = agent_loop(invoke_model, [], session, max_iterations=1)
68
+ print(response(result)["content"])
69
+ ```
70
+
71
+ ## Tool-Using Agent
72
+
73
+ Define tools as Anthropic tool schemas and provide handler functions. The
74
+ handler receives tool input as keyword arguments and returns a string.
75
+
76
+ ```python
77
+ import requests
78
+
79
+ tools = [
80
+ {
81
+ "name": "get_weather",
82
+ "description": "Get the current weather for a city.",
83
+ "input_schema": {
84
+ "type": "object",
85
+ "properties": {
86
+ "city": {"type": "string", "description": "City name"},
87
+ },
88
+ "required": ["city"],
89
+ },
90
+ }
91
+ ]
92
+
93
+ def get_weather(city):
94
+ resp = requests.get(f"https://wttr.in/{city}?format=j1")
95
+ data = resp.json()["current_condition"][0]
96
+ return json.dumps({
97
+ "city": city,
98
+ "temp_c": data["temp_C"],
99
+ "description": data["weatherDesc"][0]["value"],
100
+ })
101
+
102
+ session = init_session(
103
+ system_prompt="You answer weather questions. Use the get_weather tool.",
104
+ user_prompt="What's the weather in Tokyo and Paris?",
105
+ )
106
+ result = agent_loop(
107
+ invoke_model, tools, session,
108
+ tool_handlers={"get_weather": get_weather},
109
+ )
110
+ print(response(result)["content"])
111
+ ```
112
+
113
+ The model will call get_weather twice (in parallel), see the results, and
114
+ respond with a summary. The loop runs until the model responds without
115
+ making any tool calls.
116
+
117
+ ## Subagents
118
+
119
+ A subagent is a tool handler that runs its own agent loop. The outer agent
120
+ calls it like any tool and gets back a string result.
121
+
122
+ ### Example: Text Compressor (compressor.py)
123
+
124
+ A coordinator agent iteratively compresses text using two subagents:
125
+ a shortener and a quality judge.
126
+
127
+ ```python
128
+ # Subagent: compresses text
129
+ def shorten(text):
130
+ session = init_session(
131
+ system_prompt="Rewrite the text to half its length. Output ONLY the result.",
132
+ user_prompt=text,
133
+ )
134
+ result = agent_loop(invoke_model, [], session, name="shortener", max_iterations=1)
135
+ shortened = response(result)["content"]
136
+ ratio = len(shortened) / len(text)
137
+ return json.dumps({"compression_ratio": round(ratio, 3), "shortened_text": shortened})
138
+
139
+ # Subagent: judges compression quality
140
+ def judge(original, shortened):
141
+ session = init_session(
142
+ system_prompt=(
143
+ "Compare original and shortened text. Return ONLY JSON: "
144
+ '{"verdict": "acceptable", "reason": "..."} or '
145
+ '{"verdict": "too_lossy", "reason": "..."}'
146
+ ),
147
+ user_prompt=f"ORIGINAL:\n{original}\n\nSHORTENED:\n{shortened}",
148
+ )
149
+ result = agent_loop(invoke_model, [], session, name="judge", max_iterations=1)
150
+ return response(result)["content"]
151
+ ```
152
+
153
+ The coordinator has tools for `shorten` and `judge`, and its system prompt
154
+ tells it to loop: shorten, judge, stop if too_lossy or diminishing returns,
155
+ otherwise shorten again. Each subagent is a one-shot agent loop
156
+ (max_iterations=1) with no tools of its own.
157
+
158
+ ### Example: Transform Rule Derivation (derive_transform.py)
159
+
160
+ A more complex example with four subagents and a coordinator. Given a
161
+ source text and target text, it derives general transformation rules and
162
+ specific info that together reproduce the target from the source.
163
+
164
+ ```python
165
+ # Subagent: applies rules + specific info to source text
166
+ def edit(text, rules, specific_info):
167
+ session = init_session(
168
+ system_prompt="Apply the rules to the text using the specific info. Output ONLY the result.",
169
+ user_prompt=f"SOURCE TEXT:\n{text}\n\nRULES:\n{rules}\n\nSPECIFIC INFO:\n{specific_info}",
170
+ )
171
+ result = agent_loop(invoke_model, [], session, name="editor", max_iterations=1)
172
+ return response(result)["content"]
173
+
174
+ # Subagent: scores how close the output is to the target
175
+ def judge_similarity(editor_output, target):
176
+ session = init_session(
177
+ system_prompt='Compare texts. Return JSON: {"score": 0-100, "differences": "..."}',
178
+ user_prompt=f"EDITOR OUTPUT:\n{editor_output}\n\nTARGET:\n{target}",
179
+ )
180
+ result = agent_loop(invoke_model, [], session, name="similarity-judge", max_iterations=1)
181
+ return response(result)["content"]
182
+
183
+ # Subagent: checks rules are abstract (no specific content leaked in)
184
+ def judge_generality(rules):
185
+ ...
186
+
187
+ # Subagent: checks specific_info is a flat fact list
188
+ def judge_specific_info(specific_info):
189
+ ...
190
+ ```
191
+
192
+ The coordinator calls `edit`, then calls all three judges in parallel,
193
+ refines based on scores, and repeats until all judges score above 90.
194
+ Tool calls within a single model response execute in parallel automatically.
195
+
196
+ ## API Reference
197
+
198
+ ### Session Management
199
+
200
+ - `init_session(system_prompt, user_prompt)` - Create a new session
201
+ - `extend_session(session, message)` - Append a message to the session
202
+ - `send(session, user_message)` - Add a user message to the session
203
+ - `fork_session(session)` - Deep copy a session for branching
204
+ - `response(session)` - Get the last assistant message, or None
205
+
206
+ ### Agent Loop
207
+
208
+ - `agent_loop(invoke_model, tools, session, tool_handlers=None, name=None, max_iterations=None)`
209
+ - `invoke_model(tools, session)` - Function that calls the model API
210
+ - `tools` - List of Anthropic tool schemas ([] for no tools)
211
+ - `session` - Session dict from init_session
212
+ - `tool_handlers` - Dict mapping tool names to handler functions
213
+ - `name` - Agent name for log output
214
+ - `max_iterations` - Max model calls before stopping (None = unlimited)
215
+ - Returns the session with all messages appended
216
+
217
+ ### Message Format
218
+
219
+ Messages use a generic format independent of any API:
220
+
221
+ {"role": "system", "content": "..."}
222
+ {"role": "user", "content": "..."}
223
+ {"role": "assistant", "content": "..."}
224
+ {"type": "thinking", "content": "..."}
225
+ {"type": "tool_call", "name": "...", "id": "...", "input": {...}}
226
+ {"type": "tool_result", "id": "...", "output": "..."}
227
+
228
+ Conversion to/from Anthropic API format is handled by `to_api_messages()`
229
+ and `parse_response()`.
@@ -0,0 +1,206 @@
1
+ # simple_agent_loop
2
+
3
+ A minimal agent loop for tool-using language models. ~250 lines. Handles
4
+ message format conversion, parallel tool execution, session compaction,
5
+ and subagent composition.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ pip install simple-agent-loop
11
+ ```
12
+
13
+ ## Setup
14
+
15
+ ```python
16
+ import anthropic
17
+ import json
18
+ import simple_agent_loop as sal
19
+
20
+ client = anthropic.Anthropic() # uses ANTHROPIC_API_KEY env var
21
+
22
+ def invoke_model(tools, session):
23
+ kwargs = dict(
24
+ model="claude-sonnet-4-5",
25
+ max_tokens=16000,
26
+ messages=session["messages"],
27
+ )
28
+ if "system" in session:
29
+ kwargs["system"] = session["system"]
30
+ if tools:
31
+ kwargs["tools"] = tools
32
+ return client.messages.create(**kwargs).to_dict()
33
+ ```
34
+
35
+ ## Hello World
36
+
37
+ No tools, single turn -- the model just responds:
38
+
39
+ ```python
40
+ session = init_session(
41
+ system_prompt="You are a helpful assistant.",
42
+ user_prompt="Say hello in three languages.",
43
+ )
44
+ result = agent_loop(invoke_model, [], session, max_iterations=1)
45
+ print(response(result)["content"])
46
+ ```
47
+
48
+ ## Tool-Using Agent
49
+
50
+ Define tools as Anthropic tool schemas and provide handler functions. The
51
+ handler receives tool input as keyword arguments and returns a string.
52
+
53
+ ```python
54
+ import requests
55
+
56
+ tools = [
57
+ {
58
+ "name": "get_weather",
59
+ "description": "Get the current weather for a city.",
60
+ "input_schema": {
61
+ "type": "object",
62
+ "properties": {
63
+ "city": {"type": "string", "description": "City name"},
64
+ },
65
+ "required": ["city"],
66
+ },
67
+ }
68
+ ]
69
+
70
+ def get_weather(city):
71
+ resp = requests.get(f"https://wttr.in/{city}?format=j1")
72
+ data = resp.json()["current_condition"][0]
73
+ return json.dumps({
74
+ "city": city,
75
+ "temp_c": data["temp_C"],
76
+ "description": data["weatherDesc"][0]["value"],
77
+ })
78
+
79
+ session = init_session(
80
+ system_prompt="You answer weather questions. Use the get_weather tool.",
81
+ user_prompt="What's the weather in Tokyo and Paris?",
82
+ )
83
+ result = agent_loop(
84
+ invoke_model, tools, session,
85
+ tool_handlers={"get_weather": get_weather},
86
+ )
87
+ print(response(result)["content"])
88
+ ```
89
+
90
+ The model will call get_weather twice (in parallel), see the results, and
91
+ respond with a summary. The loop runs until the model responds without
92
+ making any tool calls.
93
+
94
+ ## Subagents
95
+
96
+ A subagent is a tool handler that runs its own agent loop. The outer agent
97
+ calls it like any tool and gets back a string result.
98
+
99
+ ### Example: Text Compressor (compressor.py)
100
+
101
+ A coordinator agent iteratively compresses text using two subagents:
102
+ a shortener and a quality judge.
103
+
104
+ ```python
105
+ # Subagent: compresses text
106
+ def shorten(text):
107
+ session = init_session(
108
+ system_prompt="Rewrite the text to half its length. Output ONLY the result.",
109
+ user_prompt=text,
110
+ )
111
+ result = agent_loop(invoke_model, [], session, name="shortener", max_iterations=1)
112
+ shortened = response(result)["content"]
113
+ ratio = len(shortened) / len(text)
114
+ return json.dumps({"compression_ratio": round(ratio, 3), "shortened_text": shortened})
115
+
116
+ # Subagent: judges compression quality
117
+ def judge(original, shortened):
118
+ session = init_session(
119
+ system_prompt=(
120
+ "Compare original and shortened text. Return ONLY JSON: "
121
+ '{"verdict": "acceptable", "reason": "..."} or '
122
+ '{"verdict": "too_lossy", "reason": "..."}'
123
+ ),
124
+ user_prompt=f"ORIGINAL:\n{original}\n\nSHORTENED:\n{shortened}",
125
+ )
126
+ result = agent_loop(invoke_model, [], session, name="judge", max_iterations=1)
127
+ return response(result)["content"]
128
+ ```
129
+
130
+ The coordinator has tools for `shorten` and `judge`, and its system prompt
131
+ tells it to loop: shorten, judge, stop if too_lossy or diminishing returns,
132
+ otherwise shorten again. Each subagent is a one-shot agent loop
133
+ (max_iterations=1) with no tools of its own.
134
+
135
+ ### Example: Transform Rule Derivation (derive_transform.py)
136
+
137
+ A more complex example with four subagents and a coordinator. Given a
138
+ source text and target text, it derives general transformation rules and
139
+ specific info that together reproduce the target from the source.
140
+
141
+ ```python
142
+ # Subagent: applies rules + specific info to source text
143
+ def edit(text, rules, specific_info):
144
+ session = init_session(
145
+ system_prompt="Apply the rules to the text using the specific info. Output ONLY the result.",
146
+ user_prompt=f"SOURCE TEXT:\n{text}\n\nRULES:\n{rules}\n\nSPECIFIC INFO:\n{specific_info}",
147
+ )
148
+ result = agent_loop(invoke_model, [], session, name="editor", max_iterations=1)
149
+ return response(result)["content"]
150
+
151
+ # Subagent: scores how close the output is to the target
152
+ def judge_similarity(editor_output, target):
153
+ session = init_session(
154
+ system_prompt='Compare texts. Return JSON: {"score": 0-100, "differences": "..."}',
155
+ user_prompt=f"EDITOR OUTPUT:\n{editor_output}\n\nTARGET:\n{target}",
156
+ )
157
+ result = agent_loop(invoke_model, [], session, name="similarity-judge", max_iterations=1)
158
+ return response(result)["content"]
159
+
160
+ # Subagent: checks rules are abstract (no specific content leaked in)
161
+ def judge_generality(rules):
162
+ ...
163
+
164
+ # Subagent: checks specific_info is a flat fact list
165
+ def judge_specific_info(specific_info):
166
+ ...
167
+ ```
168
+
169
+ The coordinator calls `edit`, then calls all three judges in parallel,
170
+ refines based on scores, and repeats until all judges score above 90.
171
+ Tool calls within a single model response execute in parallel automatically.
172
+
173
+ ## API Reference
174
+
175
+ ### Session Management
176
+
177
+ - `init_session(system_prompt, user_prompt)` - Create a new session
178
+ - `extend_session(session, message)` - Append a message to the session
179
+ - `send(session, user_message)` - Add a user message to the session
180
+ - `fork_session(session)` - Deep copy a session for branching
181
+ - `response(session)` - Get the last assistant message, or None
182
+
183
+ ### Agent Loop
184
+
185
+ - `agent_loop(invoke_model, tools, session, tool_handlers=None, name=None, max_iterations=None)`
186
+ - `invoke_model(tools, session)` - Function that calls the model API
187
+ - `tools` - List of Anthropic tool schemas ([] for no tools)
188
+ - `session` - Session dict from init_session
189
+ - `tool_handlers` - Dict mapping tool names to handler functions
190
+ - `name` - Agent name for log output
191
+ - `max_iterations` - Max model calls before stopping (None = unlimited)
192
+ - Returns the session with all messages appended
193
+
194
+ ### Message Format
195
+
196
+ Messages use a generic format independent of any API:
197
+
198
+ {"role": "system", "content": "..."}
199
+ {"role": "user", "content": "..."}
200
+ {"role": "assistant", "content": "..."}
201
+ {"type": "thinking", "content": "..."}
202
+ {"type": "tool_call", "name": "...", "id": "...", "input": {...}}
203
+ {"type": "tool_result", "id": "...", "output": "..."}
204
+
205
+ Conversion to/from Anthropic API format is handled by `to_api_messages()`
206
+ and `parse_response()`.