coreouto 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 (63) hide show
  1. coreouto-0.1.0/.gitignore +42 -0
  2. coreouto-0.1.0/AGENTS.md +65 -0
  3. coreouto-0.1.0/LICENSE +21 -0
  4. coreouto-0.1.0/PKG-INFO +101 -0
  5. coreouto-0.1.0/README.md +60 -0
  6. coreouto-0.1.0/docs/agent.md +214 -0
  7. coreouto-0.1.0/docs/hooks.md +203 -0
  8. coreouto-0.1.0/docs/index.md +59 -0
  9. coreouto-0.1.0/docs/multi-agent.md +185 -0
  10. coreouto-0.1.0/docs/philosophy.md +99 -0
  11. coreouto-0.1.0/docs/presets.md +147 -0
  12. coreouto-0.1.0/docs/providers.md +366 -0
  13. coreouto-0.1.0/docs/quickstart.md +165 -0
  14. coreouto-0.1.0/docs/tools.md +190 -0
  15. coreouto-0.1.0/examples/01_simple.py +43 -0
  16. coreouto-0.1.0/examples/02_tools.py +61 -0
  17. coreouto-0.1.0/examples/03_presets.py +76 -0
  18. coreouto-0.1.0/examples/04_hooks.py +60 -0
  19. coreouto-0.1.0/examples/05_multi_agent.py +89 -0
  20. coreouto-0.1.0/examples/06_custom_provider.py +81 -0
  21. coreouto-0.1.0/examples/07_provider_settings.py +58 -0
  22. coreouto-0.1.0/examples/08_force_finish.py +77 -0
  23. coreouto-0.1.0/examples/09_agent_delegation.py +79 -0
  24. coreouto-0.1.0/examples/10_custom_endpoints.py +89 -0
  25. coreouto-0.1.0/examples/12_history.py +88 -0
  26. coreouto-0.1.0/examples/13_inject.py +94 -0
  27. coreouto-0.1.0/pyproject.toml +92 -0
  28. coreouto-0.1.0/src/coreouto/__init__.py +95 -0
  29. coreouto-0.1.0/src/coreouto/_types.py +82 -0
  30. coreouto-0.1.0/src/coreouto/_version.py +3 -0
  31. coreouto-0.1.0/src/coreouto/agent.py +213 -0
  32. coreouto-0.1.0/src/coreouto/contrib/__init__.py +0 -0
  33. coreouto-0.1.0/src/coreouto/contrib/hooks.py +97 -0
  34. coreouto-0.1.0/src/coreouto/hooks.py +40 -0
  35. coreouto-0.1.0/src/coreouto/multi_agent.py +108 -0
  36. coreouto-0.1.0/src/coreouto/presets.py +85 -0
  37. coreouto-0.1.0/src/coreouto/providers/__init__.py +42 -0
  38. coreouto-0.1.0/src/coreouto/providers/anthropic.py +161 -0
  39. coreouto-0.1.0/src/coreouto/providers/base.py +23 -0
  40. coreouto-0.1.0/src/coreouto/providers/google.py +183 -0
  41. coreouto-0.1.0/src/coreouto/providers/openai.py +157 -0
  42. coreouto-0.1.0/src/coreouto/providers/openai_response.py +188 -0
  43. coreouto-0.1.0/src/coreouto/settings.py +119 -0
  44. coreouto-0.1.0/src/coreouto/sync.py +27 -0
  45. coreouto-0.1.0/src/coreouto/tools.py +157 -0
  46. coreouto-0.1.0/tests/__init__.py +0 -0
  47. coreouto-0.1.0/tests/conftest.py +126 -0
  48. coreouto-0.1.0/tests/test_agent.py +738 -0
  49. coreouto-0.1.0/tests/test_contrib_hooks.py +332 -0
  50. coreouto-0.1.0/tests/test_hooks.py +201 -0
  51. coreouto-0.1.0/tests/test_multi_agent.py +377 -0
  52. coreouto-0.1.0/tests/test_presets.py +175 -0
  53. coreouto-0.1.0/tests/test_providers_anthropic.py +402 -0
  54. coreouto-0.1.0/tests/test_providers_base.py +94 -0
  55. coreouto-0.1.0/tests/test_providers_google.py +662 -0
  56. coreouto-0.1.0/tests/test_providers_openai.py +242 -0
  57. coreouto-0.1.0/tests/test_providers_openai_response.py +452 -0
  58. coreouto-0.1.0/tests/test_providers_registry.py +85 -0
  59. coreouto-0.1.0/tests/test_public_api.py +94 -0
  60. coreouto-0.1.0/tests/test_settings.py +128 -0
  61. coreouto-0.1.0/tests/test_sync.py +111 -0
  62. coreouto-0.1.0/tests/test_tools.py +252 -0
  63. coreouto-0.1.0/tests/test_types.py +350 -0
@@ -0,0 +1,42 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ .Python
8
+ build/
9
+ dist/
10
+ *.egg-info/
11
+ *.egg
12
+ .eggs/
13
+ install/
14
+
15
+ # Unit test / coverage reports
16
+ htmlcov/
17
+ .coverage
18
+ .coverage.*
19
+ .cache
20
+ .pytest_cache/
21
+ .ruff_cache/
22
+ .mypy_cache/
23
+
24
+ # Environments
25
+ .env
26
+ .envrc
27
+ .venv/
28
+ venv/
29
+ env/
30
+
31
+ # IDE
32
+ .idea/
33
+ .vscode/
34
+ *.swp
35
+ *.swo
36
+
37
+ # OS
38
+ .DS_Store
39
+ Thumbs.db
40
+
41
+ # Logs
42
+ *.log
@@ -0,0 +1,65 @@
1
+ # AGENTS.md
2
+
3
+ This file guides AI coding agents working on the coreouto codebase.
4
+
5
+ ## What coreouto is
6
+
7
+ A minimal Python agent library for PyPI. The entire core is one loop:
8
+ **call → internal loop → `Response` with extracted <finish> tag content**.
9
+
10
+ ## The five philosophies (NON-NEGOTIABLE)
11
+
12
+ 1. **Minimalism** — implement only the minimum for an agent system. Let the user extend.
13
+ 2. **Extensibility** — almost everything must be customizable.
14
+ 3. **Explicitness** — the user declares everything. No auto-features (no auto-agent-to-agent, no built-in auto-summarization). Built-in hooks live in `coreouto/contrib/hooks.py` and are opt-in.
15
+ 4. **Fragmentation** — features are broken into independent pieces. One feature changing doesn't break others.
16
+ 5. **Conciseness** — code using coreouto should be obvious to read.
17
+
18
+ ## Layout
19
+
20
+ ```
21
+ src/coreouto/
22
+ __init__.py # public API
23
+ _types.py # Pydantic v2 message/response models
24
+ agent.py # Agent class and the core loop
25
+ tools.py # @register_tool decorator and Tool class
26
+ presets.py # agent preset registration
27
+ hooks.py # hook event constants and ordered async dispatch
28
+ multi_agent.py # agent_as_tool() helper
29
+ sync.py # call_sync() — fails loudly if a loop is running
30
+ contrib/
31
+ hooks.py # 5 opt-in hook recipes
32
+ providers/
33
+ base.py # Provider protocol (3 methods)
34
+ __init__.py # string-keyed registry
35
+ openai.py
36
+ anthropic.py
37
+ google.py
38
+ openai_response.py
39
+ tests/
40
+ docs/
41
+ examples/
42
+ ```
43
+
44
+ ## Build / test commands
45
+
46
+ - `pip install -e ".[dev,all]"` — editable install
47
+ - `pytest -q` — full test suite (uses `MockProvider`, no real API calls)
48
+ - `ruff check src tests examples` — lint
49
+ - `ruff format --check src tests examples` — format check
50
+ - `python -m build` — build wheel + sdist
51
+ - `twine check dist/*` — verify metadata
52
+
53
+ ## Critical invariants
54
+
55
+ - **No real API calls in tests.** Use the `MockProvider` seam from `tests/conftest.py`.
56
+ - **Async-first.** `Agent.call()` is `async def`. `call_sync()` raises `RuntimeError` if an event loop is running.
57
+ - **No `nest_asyncio` anywhere.**
58
+ - **Provider protocol has exactly 3 methods**: `create`, `format_assistant_message`, `format_tool_result`.
59
+
60
+ ## Adding code
61
+
62
+ - New provider → implement the 3-method protocol and `register_provider(name, instance)`.
63
+ - New tool → `@register_tool("name")` over a sync/async callable. Type hints are extracted to JSON Schema.
64
+ - New hook event → add a string constant in `hooks.py`; fire via `await trigger(EVENT, ctx)`.
65
+ - New built-in hook recipe → add to `coreouto/contrib/hooks.py` as a factory returning a hook function.
coreouto-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 coreouto authors
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,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: coreouto
3
+ Version: 0.1.0
4
+ Summary: A minimal, extensible agent library for Python. Five philosophies: minimalism, extensibility, explicitness, fragmentation, conciseness.
5
+ Project-URL: Homepage, https://github.com/llaa33219/coreouto
6
+ Project-URL: Documentation, https://github.com/llaa33219/coreouto/tree/main/docs
7
+ Project-URL: Repository, https://github.com/llaa33219/coreouto
8
+ Project-URL: Issues, https://github.com/llaa33219/coreouto/issues
9
+ Author: coreouto authors
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,ai,llm,provider,tool-use
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: pydantic>=2.0
25
+ Provides-Extra: all
26
+ Requires-Dist: anthropic>=0.18; extra == 'all'
27
+ Requires-Dist: google-generativeai>=0.3; extra == 'all'
28
+ Requires-Dist: openai>=1.0; extra == 'all'
29
+ Provides-Extra: anthropic
30
+ Requires-Dist: anthropic>=0.18; extra == 'anthropic'
31
+ Provides-Extra: dev
32
+ Requires-Dist: mypy>=1.0; extra == 'dev'
33
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
34
+ Requires-Dist: pytest>=7.0; extra == 'dev'
35
+ Requires-Dist: ruff>=0.1; extra == 'dev'
36
+ Provides-Extra: google
37
+ Requires-Dist: google-generativeai>=0.3; extra == 'google'
38
+ Provides-Extra: openai
39
+ Requires-Dist: openai>=1.0; extra == 'openai'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # coreouto
43
+
44
+ **A minimal, extensible agent library for Python.**
45
+
46
+ Built on five philosophies: **minimalism, extensibility, explicitness, fragmentation, conciseness.**
47
+
48
+ The whole library reduces to one idea: an agent is called with a message, runs an internal loop, and returns its response when the model wraps its final answer in `<finish>...</finish>` tags. Everything else — providers, tools, presets, hooks, multi-agent — is an opt-in extension.
49
+
50
+ ```python
51
+ import os
52
+
53
+ import coreouto as co
54
+ from coreouto.providers.openai import OpenAIProvider
55
+
56
+ @co.register_tool("search")
57
+ def search(query: str) -> str:
58
+ """Search the web for `query`."""
59
+ return f"<results for {query}>"
60
+
61
+ co.register_provider("minimax", OpenAIProvider(
62
+ api_key=os.environ["MINIMAX_API_KEY"],
63
+ base_url="https://api.minimax.io/v1",
64
+ ))
65
+
66
+ preset = co.register_agent_preset(
67
+ "researcher", model="MiniMax-M3", provider="minimax",
68
+ system_prompt="You are a research assistant.",
69
+ tools=["search"],
70
+ )
71
+ response = co.Agent(preset.to_config()).call_sync("Find me recent news about fusion energy.")
72
+ print(response.content)
73
+ ```
74
+
75
+ ## Install
76
+
77
+ ```bash
78
+ pip install coreouto
79
+ # with providers
80
+ pip install coreouto[openai]
81
+ pip install coreouto[anthropic]
82
+ pip install coreouto[google]
83
+ pip install coreouto[all]
84
+ ```
85
+
86
+ ## Documentation
87
+
88
+ See [`docs/`](./docs/) for the full documentation set:
89
+
90
+ - [Philosophy](./docs/philosophy.md) — the five principles
91
+ - [Quickstart](./docs/quickstart.md)
92
+ - [Agent](./docs/agent.md)
93
+ - [Providers](./docs/providers.md)
94
+ - [Tools](./docs/tools.md)
95
+ - [Presets](./docs/presets.md)
96
+ - [Hooks](./docs/hooks.md)
97
+ - [Multi-agent](./docs/multi-agent.md)
98
+
99
+ ## License
100
+
101
+ MIT
@@ -0,0 +1,60 @@
1
+ # coreouto
2
+
3
+ **A minimal, extensible agent library for Python.**
4
+
5
+ Built on five philosophies: **minimalism, extensibility, explicitness, fragmentation, conciseness.**
6
+
7
+ The whole library reduces to one idea: an agent is called with a message, runs an internal loop, and returns its response when the model wraps its final answer in `<finish>...</finish>` tags. Everything else — providers, tools, presets, hooks, multi-agent — is an opt-in extension.
8
+
9
+ ```python
10
+ import os
11
+
12
+ import coreouto as co
13
+ from coreouto.providers.openai import OpenAIProvider
14
+
15
+ @co.register_tool("search")
16
+ def search(query: str) -> str:
17
+ """Search the web for `query`."""
18
+ return f"<results for {query}>"
19
+
20
+ co.register_provider("minimax", OpenAIProvider(
21
+ api_key=os.environ["MINIMAX_API_KEY"],
22
+ base_url="https://api.minimax.io/v1",
23
+ ))
24
+
25
+ preset = co.register_agent_preset(
26
+ "researcher", model="MiniMax-M3", provider="minimax",
27
+ system_prompt="You are a research assistant.",
28
+ tools=["search"],
29
+ )
30
+ response = co.Agent(preset.to_config()).call_sync("Find me recent news about fusion energy.")
31
+ print(response.content)
32
+ ```
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install coreouto
38
+ # with providers
39
+ pip install coreouto[openai]
40
+ pip install coreouto[anthropic]
41
+ pip install coreouto[google]
42
+ pip install coreouto[all]
43
+ ```
44
+
45
+ ## Documentation
46
+
47
+ See [`docs/`](./docs/) for the full documentation set:
48
+
49
+ - [Philosophy](./docs/philosophy.md) — the five principles
50
+ - [Quickstart](./docs/quickstart.md)
51
+ - [Agent](./docs/agent.md)
52
+ - [Providers](./docs/providers.md)
53
+ - [Tools](./docs/tools.md)
54
+ - [Presets](./docs/presets.md)
55
+ - [Hooks](./docs/hooks.md)
56
+ - [Multi-agent](./docs/multi-agent.md)
57
+
58
+ ## License
59
+
60
+ MIT
@@ -0,0 +1,214 @@
1
+ # Agent
2
+
3
+ The `Agent` class is the core of coreouto. It takes an `AgentConfig`, runs an internal loop calling the LLM and executing tools, and returns a `Response` when the model wraps its final answer in `<finish>...</finish>` tags.
4
+
5
+ ## Creating an agent
6
+
7
+ An agent needs an `AgentConfig`, which you can build directly or get from a preset:
8
+
9
+ ```python
10
+ import coreouto as co
11
+
12
+ # From a preset:
13
+ preset = co.register_agent_preset(
14
+ "writer", model="claude-opus-4-8", provider="anthropic",
15
+ system_prompt="You write clearly and concisely.",
16
+ )
17
+ agent = co.Agent(preset.to_config())
18
+
19
+ # From config directly:
20
+ config = co.AgentConfig(
21
+ name="writer",
22
+ model="claude-opus-4-8",
23
+ provider="anthropic",
24
+ system_prompt="You write clearly and concisely.",
25
+ tools=[],
26
+ max_iterations=50,
27
+ )
28
+ agent = co.Agent(config)
29
+ ```
30
+
31
+ ## `AgentConfig` fields
32
+
33
+ | Field | Type | Default | Description |
34
+ |------------------------|---------------------|---------|------------------------------------------|
35
+ | `name` | `str` | -- | Agent identifier |
36
+ | `model` | `str` | -- | Model name passed to the provider |
37
+ | `provider` | `str` | -- | Key of a registered provider |
38
+ | `system_prompt` | `str \| None` | `None` | System message prepended to the conversation |
39
+ | `tools` | `list[str]` | `[]` | Names of registered tools available to this agent |
40
+ | `max_iterations` | `int` | `50` | Max loop iterations before raising `MaxIterationsError` |
41
+ | `provider_config` | `dict[str, Any]` | `{}` | Canonical settings (see [Normalized settings](providers.md#normalized-settings)); translated to provider-specific kwargs |
42
+ | `provider_passthrough` | `dict[str, Any]` | `{}` | Non-canonical settings sent through to the SDK unchanged |
43
+
44
+ If `system_prompt` is `None`, a default system prompt is injected automatically explaining the `<finish>...</finish>` termination protocol.
45
+
46
+ ## Calling the agent
47
+
48
+ ### Async: `call()`
49
+
50
+ The primary interface. Returns a `Response`:
51
+
52
+ ```python
53
+ response = await agent.call("Write a haiku about Python.")
54
+ print(response.content)
55
+ ```
56
+
57
+ You can pass an `override` config to change settings for a single call:
58
+
59
+ ```python
60
+ override = co.AgentConfig(
61
+ name="writer", model="claude-sonnet-4-6", provider="anthropic",
62
+ system_prompt="Be poetic.",
63
+ )
64
+ response = await agent.call("Write something.", override=override)
65
+ ```
66
+
67
+ ### Sync: `call_sync()`
68
+
69
+ Wraps `call()` with `asyncio.run()`. Raises `RuntimeError` if an event loop is already running:
70
+
71
+ ```python
72
+ response = agent.call_sync("Write a haiku about Python.")
73
+ print(response.content)
74
+ ```
75
+
76
+ Use `call_sync()` from scripts and CLI tools. Use `call()` inside async code, web frameworks, or anywhere an event loop exists.
77
+
78
+ ### Conversation history
79
+
80
+ `call()` and `call_sync()` accept an optional `history: list[Message]` parameter. When provided, the history is prepended to the message list (after the system prompt, before the new user message). coreouto does not store conversation state between calls — that is the caller's responsibility.
81
+
82
+ Two patterns are common:
83
+
84
+ **Accumulate** — pass the messages from a previous `Response` to continue the conversation:
85
+
86
+ ```python
87
+ r1 = await agent.call("My name is Alice.")
88
+ r2 = await agent.call("What is my name?", history=r1.messages)
89
+ ```
90
+
91
+ **Fabricate** — hand-craft a list of `Message` objects to seed the agent with any context you want:
92
+
93
+ ```python
94
+ from coreouto._types import Message
95
+
96
+ fake = [
97
+ Message(role="user", content="What is 2 + 2?"),
98
+ Message(role="assistant", content="4"),
99
+ Message(role="user", content="And 3 + 3?"),
100
+ Message(role="assistant", content="6"),
101
+ ]
102
+ response = await agent.call("Continue the pattern.", history=fake)
103
+ ```
104
+
105
+ The history is prepended as-is — no implicit processing. If your `cfg.system_prompt` is set AND your history's first message is `role="system"`, you will get two system messages. Slice `history[1:]` if you want to avoid that, or use `override=AgentConfig(system_prompt=...)` to set a different one for the new call.
106
+
107
+ ### Injecting user messages into a running loop
108
+
109
+ For long-running agents that need external input mid-execution (human-in-the-loop, streaming input, tool-triggered re-prompting), use `Agent.inject_user_message(content)`:
110
+
111
+ ```python
112
+ agent = co.Agent(config)
113
+
114
+ async def push_message_later():
115
+ await asyncio.sleep(1)
116
+ agent.inject_user_message("stop and reconsider")
117
+
118
+ asyncio.create_task(push_message_later())
119
+ response = await agent.call("start the task")
120
+ ```
121
+
122
+ The message is queued (thread-safe via `asyncio.Queue`) and drained at the start of the next iteration. It fires the `on_user_injection` hook with `message` and `messages` kwargs for observability.
123
+
124
+ You can call `inject_user_message()` from anywhere: another thread, another async task, a hook callback, or even before `call()` starts (the queue persists across calls). The loop yields to the scheduler at the start of each iteration, so concurrent tasks get a chance to run.
125
+
126
+ ## The `Response` object
127
+
128
+ `call()` and `call_sync()` both return a `Response`:
129
+
130
+ | Field | Type | Description |
131
+ |------------------|-------------------|------------------------------------------------------|
132
+ | `content` | `str` | The final text extracted from `<finish>...</finish>` tags |
133
+ | `messages` | `list[Message]` | Full message history (system, user, assistant, tool) |
134
+ | `iterations` | `int` | How many LLM calls were made |
135
+ | `usage` | `list[Usage]` | Token usage per LLM call |
136
+ | `finish_called` | `bool` | Always `True` when the agent finishes normally |
137
+
138
+ Each `Usage` entry has `prompt_tokens`, `completion_tokens`, and `total_tokens`.
139
+
140
+ ## `MaxIterationsError`
141
+
142
+ If the agent loops more than `max_iterations` times without producing a `<finish>...</finish>` tag, it raises `MaxIterationsError`:
143
+
144
+ ```python
145
+ try:
146
+ response = await agent.call("Do something complex.")
147
+ except co.MaxIterationsError as e:
148
+ print(f"Agent didn't finish: {e}")
149
+ ```
150
+
151
+ Increase `max_iterations` in the config if your tasks need more steps:
152
+
153
+ ```python
154
+ preset = co.register_agent_preset(
155
+ "deep-researcher",
156
+ model="claude-opus-4-8",
157
+ provider="anthropic",
158
+ tools=["search"],
159
+ max_iterations=200,
160
+ )
161
+ ```
162
+
163
+ ## How the loop works
164
+
165
+ 1. Build the message list: system prompt (default or configured) + history (if any) + user message.
166
+ 2. Call the LLM via the registered provider.
167
+ 3. If the response's `content` contains `<finish>...</finish>` tags, extract the inner text and return a `Response`.
168
+ 4. If the response has tool calls, execute each one, append the results to the message list, and go to step 2.
169
+ 5. If the response has neither a `<finish>` tag nor tool calls, inject a reminder user message and go to step 2.
170
+ 6. If `max_iterations` is exceeded, raise `MaxIterationsError`.
171
+
172
+ ## Hooks during the loop
173
+
174
+ Six hook events fire during the loop. See [Hooks](hooks.md) for details:
175
+
176
+ - `before_llm_call` -- before each LLM request
177
+ - `after_llm_call` -- after each LLM response
178
+ - `before_tool_call` -- before each tool execution
179
+ - `after_tool_call` -- after each tool result
180
+ - `on_iteration` -- at the end of each iteration
181
+ - `on_finish` -- when the agent detects `<finish>...</finish>` tags
182
+ - `on_user_injection` -- when a user message is injected via `Agent.inject_user_message`
183
+
184
+ ## Provider config and the `<finish>` reminder
185
+
186
+ ### `provider_config`
187
+
188
+ `AgentConfig.provider_config` is a `dict[str, Any]` of **canonical settings** that coreouto normalizes to each provider's native kwarg names (e.g., `max_tokens` automatically becomes `max_output_tokens` for OpenAI Responses and Google). Use it for the 8 cross-provider settings: `temperature`, `top_p`, `max_tokens`, `top_k`, `stop`, `seed`, `metadata`, `reasoning_effort`. See [Normalized settings](providers.md#normalized-settings) for the full mapping table.
189
+
190
+ For non-canonical, provider-specific settings (like OpenAI's `response_format` or Anthropic's `thinking`), use `AgentConfig.provider_passthrough` instead -- it is sent through to the SDK unchanged.
191
+
192
+ ```python
193
+ config = co.AgentConfig(
194
+ name="writer",
195
+ model="claude-opus-4-8",
196
+ provider="anthropic",
197
+ provider_config={"temperature": 0.3, "max_tokens": 1024},
198
+ )
199
+ ```
200
+
201
+ ### Missing `<finish>` tag reminder
202
+
203
+ Sometimes a model returns plain text without wrapping it in `<finish>...</finish>` tags. The agent handles this gracefully by injecting a user message that reminds the model to use the tags, then continues the loop. This prevents the agent from silently losing output when the model forgets the termination protocol.
204
+
205
+ ### Tracking finish events with hooks
206
+
207
+ ```python
208
+ import coreouto as co
209
+
210
+ def log_finish(*, content, raw_content, messages, iterations, **kwargs):
211
+ print(f"Agent finished after {iterations} iterations with: {content}")
212
+
213
+ co.register_hook(co.ON_FINISH, log_finish)
214
+ ```