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.
- coreouto-0.1.0/.gitignore +42 -0
- coreouto-0.1.0/AGENTS.md +65 -0
- coreouto-0.1.0/LICENSE +21 -0
- coreouto-0.1.0/PKG-INFO +101 -0
- coreouto-0.1.0/README.md +60 -0
- coreouto-0.1.0/docs/agent.md +214 -0
- coreouto-0.1.0/docs/hooks.md +203 -0
- coreouto-0.1.0/docs/index.md +59 -0
- coreouto-0.1.0/docs/multi-agent.md +185 -0
- coreouto-0.1.0/docs/philosophy.md +99 -0
- coreouto-0.1.0/docs/presets.md +147 -0
- coreouto-0.1.0/docs/providers.md +366 -0
- coreouto-0.1.0/docs/quickstart.md +165 -0
- coreouto-0.1.0/docs/tools.md +190 -0
- coreouto-0.1.0/examples/01_simple.py +43 -0
- coreouto-0.1.0/examples/02_tools.py +61 -0
- coreouto-0.1.0/examples/03_presets.py +76 -0
- coreouto-0.1.0/examples/04_hooks.py +60 -0
- coreouto-0.1.0/examples/05_multi_agent.py +89 -0
- coreouto-0.1.0/examples/06_custom_provider.py +81 -0
- coreouto-0.1.0/examples/07_provider_settings.py +58 -0
- coreouto-0.1.0/examples/08_force_finish.py +77 -0
- coreouto-0.1.0/examples/09_agent_delegation.py +79 -0
- coreouto-0.1.0/examples/10_custom_endpoints.py +89 -0
- coreouto-0.1.0/examples/12_history.py +88 -0
- coreouto-0.1.0/examples/13_inject.py +94 -0
- coreouto-0.1.0/pyproject.toml +92 -0
- coreouto-0.1.0/src/coreouto/__init__.py +95 -0
- coreouto-0.1.0/src/coreouto/_types.py +82 -0
- coreouto-0.1.0/src/coreouto/_version.py +3 -0
- coreouto-0.1.0/src/coreouto/agent.py +213 -0
- coreouto-0.1.0/src/coreouto/contrib/__init__.py +0 -0
- coreouto-0.1.0/src/coreouto/contrib/hooks.py +97 -0
- coreouto-0.1.0/src/coreouto/hooks.py +40 -0
- coreouto-0.1.0/src/coreouto/multi_agent.py +108 -0
- coreouto-0.1.0/src/coreouto/presets.py +85 -0
- coreouto-0.1.0/src/coreouto/providers/__init__.py +42 -0
- coreouto-0.1.0/src/coreouto/providers/anthropic.py +161 -0
- coreouto-0.1.0/src/coreouto/providers/base.py +23 -0
- coreouto-0.1.0/src/coreouto/providers/google.py +183 -0
- coreouto-0.1.0/src/coreouto/providers/openai.py +157 -0
- coreouto-0.1.0/src/coreouto/providers/openai_response.py +188 -0
- coreouto-0.1.0/src/coreouto/settings.py +119 -0
- coreouto-0.1.0/src/coreouto/sync.py +27 -0
- coreouto-0.1.0/src/coreouto/tools.py +157 -0
- coreouto-0.1.0/tests/__init__.py +0 -0
- coreouto-0.1.0/tests/conftest.py +126 -0
- coreouto-0.1.0/tests/test_agent.py +738 -0
- coreouto-0.1.0/tests/test_contrib_hooks.py +332 -0
- coreouto-0.1.0/tests/test_hooks.py +201 -0
- coreouto-0.1.0/tests/test_multi_agent.py +377 -0
- coreouto-0.1.0/tests/test_presets.py +175 -0
- coreouto-0.1.0/tests/test_providers_anthropic.py +402 -0
- coreouto-0.1.0/tests/test_providers_base.py +94 -0
- coreouto-0.1.0/tests/test_providers_google.py +662 -0
- coreouto-0.1.0/tests/test_providers_openai.py +242 -0
- coreouto-0.1.0/tests/test_providers_openai_response.py +452 -0
- coreouto-0.1.0/tests/test_providers_registry.py +85 -0
- coreouto-0.1.0/tests/test_public_api.py +94 -0
- coreouto-0.1.0/tests/test_settings.py +128 -0
- coreouto-0.1.0/tests/test_sync.py +111 -0
- coreouto-0.1.0/tests/test_tools.py +252 -0
- 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
|
coreouto-0.1.0/AGENTS.md
ADDED
|
@@ -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.
|
coreouto-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
coreouto-0.1.0/README.md
ADDED
|
@@ -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
|
+
```
|