crowe-agent-core 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.
- crowe_agent_core-0.1.0/LICENSE +115 -0
- crowe_agent_core-0.1.0/PKG-INFO +72 -0
- crowe_agent_core-0.1.0/README.md +44 -0
- crowe_agent_core-0.1.0/crowe_agent_core/__init__.py +52 -0
- crowe_agent_core-0.1.0/crowe_agent_core/adapters/__init__.py +14 -0
- crowe_agent_core-0.1.0/crowe_agent_core/adapters/anthropic.py +210 -0
- crowe_agent_core-0.1.0/crowe_agent_core/adapters/base.py +58 -0
- crowe_agent_core-0.1.0/crowe_agent_core/adapters/openai_compat.py +251 -0
- crowe_agent_core-0.1.0/crowe_agent_core/adapters/scripted.py +38 -0
- crowe_agent_core-0.1.0/crowe_agent_core/adapters/single_turn.py +61 -0
- crowe_agent_core-0.1.0/crowe_agent_core/capability.py +150 -0
- crowe_agent_core-0.1.0/crowe_agent_core/hooks.py +83 -0
- crowe_agent_core-0.1.0/crowe_agent_core/kernel.py +139 -0
- crowe_agent_core-0.1.0/crowe_agent_core/protocol.py +75 -0
- crowe_agent_core-0.1.0/crowe_agent_core/protocol_cmp.py +94 -0
- crowe_agent_core-0.1.0/crowe_agent_core/roles.py +40 -0
- crowe_agent_core-0.1.0/crowe_agent_core/tool_args.py +132 -0
- crowe_agent_core-0.1.0/crowe_agent_core/tools/__init__.py +3 -0
- crowe_agent_core-0.1.0/crowe_agent_core/tools/registry.py +84 -0
- crowe_agent_core-0.1.0/crowe_agent_core.egg-info/PKG-INFO +72 -0
- crowe_agent_core-0.1.0/crowe_agent_core.egg-info/SOURCES.txt +31 -0
- crowe_agent_core-0.1.0/crowe_agent_core.egg-info/dependency_links.txt +1 -0
- crowe_agent_core-0.1.0/crowe_agent_core.egg-info/requires.txt +7 -0
- crowe_agent_core-0.1.0/crowe_agent_core.egg-info/top_level.txt +1 -0
- crowe_agent_core-0.1.0/pyproject.toml +42 -0
- crowe_agent_core-0.1.0/setup.cfg +4 -0
- crowe_agent_core-0.1.0/tests/test_anthropic_adapter.py +179 -0
- crowe_agent_core-0.1.0/tests/test_capability.py +134 -0
- crowe_agent_core-0.1.0/tests/test_kernel.py +138 -0
- crowe_agent_core-0.1.0/tests/test_openai_compat_adapter.py +192 -0
- crowe_agent_core-0.1.0/tests/test_protocol_cmp.py +72 -0
- crowe_agent_core-0.1.0/tests/test_single_turn_adapter.py +42 -0
- crowe_agent_core-0.1.0/tests/test_tool_args.py +107 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Crowe Logic Proprietary Software License
|
|
2
|
+
Copyright (c) 2026 Crowe Logic, Inc. All rights reserved.
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: LicenseRef-Proprietary
|
|
5
|
+
|
|
6
|
+
This software is proprietary and confidential. It is licensed, not sold. Your
|
|
7
|
+
right to use it comes only from this license.
|
|
8
|
+
|
|
9
|
+
1. DEFINITIONS
|
|
10
|
+
|
|
11
|
+
"Software" means the crowe-logic package, its source code, configuration,
|
|
12
|
+
system prompts, model definitions, documentation, and any updates provided
|
|
13
|
+
to you.
|
|
14
|
+
|
|
15
|
+
"Crowe Logic" means Crowe Logic, Inc.
|
|
16
|
+
|
|
17
|
+
"Plan" means a current, paid subscription to a Crowe Logic offering, or the
|
|
18
|
+
no-cost tier Crowe Logic makes available, each subject to its own posted
|
|
19
|
+
terms and usage limits.
|
|
20
|
+
|
|
21
|
+
2. GRANT
|
|
22
|
+
|
|
23
|
+
Subject to your compliance with this license, Crowe Logic grants you a
|
|
24
|
+
limited, non-exclusive, non-transferable, revocable right to download,
|
|
25
|
+
install, and run the Software on machines you control, for your own internal
|
|
26
|
+
or business purposes, for as long as you hold an applicable Plan.
|
|
27
|
+
|
|
28
|
+
3. RESTRICTIONS
|
|
29
|
+
|
|
30
|
+
You may not:
|
|
31
|
+
|
|
32
|
+
(a) redistribute, publish, sublicense, sell, rent, lease, or lend the
|
|
33
|
+
Software or any part of it, including its system prompts and model
|
|
34
|
+
configuration, whether modified or not;
|
|
35
|
+
|
|
36
|
+
(b) create or distribute derivative works of the Software;
|
|
37
|
+
|
|
38
|
+
(c) reverse engineer, decompile, or disassemble the Software except to the
|
|
39
|
+
extent applicable law expressly permits despite this restriction;
|
|
40
|
+
|
|
41
|
+
(d) remove, obscure, or alter any copyright, license, or attribution notice;
|
|
42
|
+
|
|
43
|
+
(e) use the Software to build or operate a product or service that competes
|
|
44
|
+
with Crowe Logic offerings;
|
|
45
|
+
|
|
46
|
+
(f) circumvent, disable, or interfere with any licensing, entitlement,
|
|
47
|
+
metering, or usage-limit mechanism, or access paid capabilities without
|
|
48
|
+
an applicable Plan.
|
|
49
|
+
|
|
50
|
+
4. OWNERSHIP
|
|
51
|
+
|
|
52
|
+
Crowe Logic retains all right, title, and interest in the Software,
|
|
53
|
+
including all intellectual property rights. No rights are granted except
|
|
54
|
+
those expressly stated here.
|
|
55
|
+
|
|
56
|
+
5. THIRD-PARTY COMPONENTS
|
|
57
|
+
|
|
58
|
+
The Software depends on third-party packages that are not covered by this
|
|
59
|
+
license and remain governed by their own license terms. This license applies
|
|
60
|
+
only to material authored by Crowe Logic. Nothing here restricts your rights
|
|
61
|
+
in those third-party components.
|
|
62
|
+
|
|
63
|
+
6. FEEDBACK
|
|
64
|
+
|
|
65
|
+
If you send Crowe Logic suggestions or feedback about the Software, Crowe
|
|
66
|
+
Logic may use it without obligation or compensation to you. You retain
|
|
67
|
+
ownership of anything you author using the Software.
|
|
68
|
+
|
|
69
|
+
7. TERM AND TERMINATION
|
|
70
|
+
|
|
71
|
+
This license takes effect when you first obtain the Software and continues
|
|
72
|
+
while you hold an applicable Plan. It terminates automatically if you breach
|
|
73
|
+
any term, or when your Plan lapses. On termination you must stop using the
|
|
74
|
+
Software and delete all copies. Sections 3 through 10 survive termination.
|
|
75
|
+
|
|
76
|
+
8. NO WARRANTY
|
|
77
|
+
|
|
78
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
79
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
80
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. The Software
|
|
81
|
+
orchestrates third-party language models whose outputs may be inaccurate. You are
|
|
82
|
+
responsible for reviewing and verifying all outputs before relying on them.
|
|
83
|
+
|
|
84
|
+
9. LIMITATION OF LIABILITY
|
|
85
|
+
|
|
86
|
+
TO THE MAXIMUM EXTENT PERMITTED BY LAW, CROWE LOGIC SHALL NOT BE LIABLE FOR
|
|
87
|
+
ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES, OR
|
|
88
|
+
FOR ANY LOST PROFITS, REVENUE, DATA, OR GOODWILL, ARISING OUT OF OR RELATED
|
|
89
|
+
TO THE SOFTWARE. CROWE LOGIC'S TOTAL AGGREGATE LIABILITY SHALL NOT EXCEED
|
|
90
|
+
THE AMOUNTS YOU PAID CROWE LOGIC FOR THE SOFTWARE IN THE TWELVE MONTHS
|
|
91
|
+
PRECEDING THE CLAIM.
|
|
92
|
+
|
|
93
|
+
10. GOVERNING LAW
|
|
94
|
+
|
|
95
|
+
This license is governed by the laws of the State of Arizona, United States,
|
|
96
|
+
excluding its conflict-of-laws rules. The state and federal courts located
|
|
97
|
+
in Maricopa County, Arizona shall have exclusive jurisdiction over any
|
|
98
|
+
dispute arising out of this license.
|
|
99
|
+
|
|
100
|
+
11. ENTIRE AGREEMENT
|
|
101
|
+
|
|
102
|
+
This license is the entire agreement between you and Crowe Logic regarding
|
|
103
|
+
the Software and supersedes any prior understanding. If any provision is
|
|
104
|
+
found unenforceable, the remainder stays in effect.
|
|
105
|
+
|
|
106
|
+
--------------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
NOTE ON EARLIER RELEASES
|
|
109
|
+
|
|
110
|
+
Versions of this package published before 0.7.1 shipped an Apache License 2.0
|
|
111
|
+
text. Those releases were distributed under that license, and this license does
|
|
112
|
+
not, and cannot, retroactively change the terms under which they were
|
|
113
|
+
obtained. This license governs version 0.7.1 and later.
|
|
114
|
+
|
|
115
|
+
Licensing questions: michael@crowelogic.com
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crowe-agent-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Crowe Agent Kernel — the one provider-agnostic agent loop the Crowe ecosystem mounts.
|
|
5
|
+
Author-email: Michael Crowe <michael@crowelogic.com>
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://crowelogic.com/foundry/
|
|
8
|
+
Project-URL: Release notes, https://pypi.org/project/crowe-agent-core/#history
|
|
9
|
+
Keywords: agent,kernel,llm,tool-calling,crowe-logic
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
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 :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: adapters
|
|
23
|
+
Requires-Dist: httpx>=0.28.0; extra == "adapters"
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# crowe-agent-core
|
|
30
|
+
|
|
31
|
+
The **Crowe Agent Kernel** — one provider-agnostic, capability-gated agent loop
|
|
32
|
+
that every Crowe surface mounts (foundry CLI, `crowelm-dp`, Cortex, `cla`, the
|
|
33
|
+
Terminal). It replaces the sprawl of divergent per-provider loops with a single
|
|
34
|
+
kernel + thin transport adapters.
|
|
35
|
+
|
|
36
|
+
Canonical architecture doc:
|
|
37
|
+
`crowe-agent-team/docs/superpowers/specs/2026-07-02-crowe-agent-kernel-architecture.md`
|
|
38
|
+
|
|
39
|
+
## The shape
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Kernel.run(role, messages) -> async stream of normalized Events
|
|
43
|
+
│
|
|
44
|
+
├─ TransportAdapter (per-provider; the ONLY thing that varies)
|
|
45
|
+
├─ ToolRegistry (federates Python/Go/MCP tools)
|
|
46
|
+
├─ HookChain (pre_turn / pre_tool_use / post_tool_use / stop)
|
|
47
|
+
│ └─ CapabilityGate (CCCP guardedCall(); human/agent actor split)
|
|
48
|
+
└─ Role (roles/<name>.role.json — a clone is data)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **One loop, many transports.** Providers implement `TransportAdapter.stream()`
|
|
52
|
+
and nothing else. DeepParallel is an adapter (one turn = one cluster synthesis),
|
|
53
|
+
not its own loop.
|
|
54
|
+
- **Every dispatch is gated.** Tool calls pass the capability choke point before
|
|
55
|
+
running. The gate is a hook; add cost caps / human-confirm as more hooks.
|
|
56
|
+
- **Agents are data.** A role file selects prompt, backend, tool allowlist, and
|
|
57
|
+
gate policy. A subagent is a kernel instance with a different role.
|
|
58
|
+
|
|
59
|
+
## Status
|
|
60
|
+
|
|
61
|
+
Scaffold. The kernel loop, adapter contract, tool registry, hook/capability
|
|
62
|
+
seam, and role loader are implemented and tested with a deterministic
|
|
63
|
+
`ScriptedAdapter`. Real adapters (openai_compat, anthropic, deepparallel) and the
|
|
64
|
+
CMP event mapping are the next phase — see the build sequence in the arch doc.
|
|
65
|
+
|
|
66
|
+
## Develop
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv venv --python 3.11 .venv
|
|
70
|
+
uv pip install --python .venv/bin/python -e ".[test]"
|
|
71
|
+
.venv/bin/python -m pytest -q
|
|
72
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# crowe-agent-core
|
|
2
|
+
|
|
3
|
+
The **Crowe Agent Kernel** — one provider-agnostic, capability-gated agent loop
|
|
4
|
+
that every Crowe surface mounts (foundry CLI, `crowelm-dp`, Cortex, `cla`, the
|
|
5
|
+
Terminal). It replaces the sprawl of divergent per-provider loops with a single
|
|
6
|
+
kernel + thin transport adapters.
|
|
7
|
+
|
|
8
|
+
Canonical architecture doc:
|
|
9
|
+
`crowe-agent-team/docs/superpowers/specs/2026-07-02-crowe-agent-kernel-architecture.md`
|
|
10
|
+
|
|
11
|
+
## The shape
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Kernel.run(role, messages) -> async stream of normalized Events
|
|
15
|
+
│
|
|
16
|
+
├─ TransportAdapter (per-provider; the ONLY thing that varies)
|
|
17
|
+
├─ ToolRegistry (federates Python/Go/MCP tools)
|
|
18
|
+
├─ HookChain (pre_turn / pre_tool_use / post_tool_use / stop)
|
|
19
|
+
│ └─ CapabilityGate (CCCP guardedCall(); human/agent actor split)
|
|
20
|
+
└─ Role (roles/<name>.role.json — a clone is data)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **One loop, many transports.** Providers implement `TransportAdapter.stream()`
|
|
24
|
+
and nothing else. DeepParallel is an adapter (one turn = one cluster synthesis),
|
|
25
|
+
not its own loop.
|
|
26
|
+
- **Every dispatch is gated.** Tool calls pass the capability choke point before
|
|
27
|
+
running. The gate is a hook; add cost caps / human-confirm as more hooks.
|
|
28
|
+
- **Agents are data.** A role file selects prompt, backend, tool allowlist, and
|
|
29
|
+
gate policy. A subagent is a kernel instance with a different role.
|
|
30
|
+
|
|
31
|
+
## Status
|
|
32
|
+
|
|
33
|
+
Scaffold. The kernel loop, adapter contract, tool registry, hook/capability
|
|
34
|
+
seam, and role loader are implemented and tested with a deterministic
|
|
35
|
+
`ScriptedAdapter`. Real adapters (openai_compat, anthropic, deepparallel) and the
|
|
36
|
+
CMP event mapping are the next phase — see the build sequence in the arch doc.
|
|
37
|
+
|
|
38
|
+
## Develop
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv venv --python 3.11 .venv
|
|
42
|
+
uv pip install --python .venv/bin/python -e ".[test]"
|
|
43
|
+
.venv/bin/python -m pytest -q
|
|
44
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""crowe-agent-core — the one agent kernel the Crowe ecosystem mounts.
|
|
2
|
+
|
|
3
|
+
See docs: crowe-agent-team/docs/superpowers/specs/2026-07-02-crowe-agent-kernel-architecture.md
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from crowe_agent_core.capability import (
|
|
9
|
+
AllowAllPolicy,
|
|
10
|
+
AllowlistPolicy,
|
|
11
|
+
CapabilityGate,
|
|
12
|
+
CapabilityPolicy,
|
|
13
|
+
SensitivePathPolicy,
|
|
14
|
+
)
|
|
15
|
+
from crowe_agent_core.hooks import Hook, HookChain, HookContext, HookDecision
|
|
16
|
+
from crowe_agent_core.kernel import Kernel
|
|
17
|
+
from crowe_agent_core.protocol import (
|
|
18
|
+
Event,
|
|
19
|
+
EventType,
|
|
20
|
+
Message,
|
|
21
|
+
ToolCall,
|
|
22
|
+
ToolResult,
|
|
23
|
+
)
|
|
24
|
+
from crowe_agent_core.protocol_cmp import cmp_stream, to_cmp_event
|
|
25
|
+
from crowe_agent_core.roles import Role, load_role
|
|
26
|
+
from crowe_agent_core.tools.registry import ToolRegistry, ToolSpec
|
|
27
|
+
|
|
28
|
+
__version__ = "0.0.1"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Kernel",
|
|
32
|
+
"Message",
|
|
33
|
+
"Event",
|
|
34
|
+
"EventType",
|
|
35
|
+
"ToolCall",
|
|
36
|
+
"ToolResult",
|
|
37
|
+
"ToolRegistry",
|
|
38
|
+
"ToolSpec",
|
|
39
|
+
"Role",
|
|
40
|
+
"load_role",
|
|
41
|
+
"Hook",
|
|
42
|
+
"HookChain",
|
|
43
|
+
"HookContext",
|
|
44
|
+
"HookDecision",
|
|
45
|
+
"CapabilityGate",
|
|
46
|
+
"CapabilityPolicy",
|
|
47
|
+
"AllowAllPolicy",
|
|
48
|
+
"AllowlistPolicy",
|
|
49
|
+
"SensitivePathPolicy",
|
|
50
|
+
"cmp_stream",
|
|
51
|
+
"to_cmp_event",
|
|
52
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from crowe_agent_core.adapters.anthropic import AnthropicAdapter
|
|
2
|
+
from crowe_agent_core.adapters.base import AdapterChunk, TransportAdapter
|
|
3
|
+
from crowe_agent_core.adapters.openai_compat import OpenAICompatAdapter
|
|
4
|
+
from crowe_agent_core.adapters.scripted import ScriptedAdapter
|
|
5
|
+
from crowe_agent_core.adapters.single_turn import SingleTurnAdapter
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"TransportAdapter",
|
|
9
|
+
"AdapterChunk",
|
|
10
|
+
"ScriptedAdapter",
|
|
11
|
+
"OpenAICompatAdapter",
|
|
12
|
+
"AnthropicAdapter",
|
|
13
|
+
"SingleTurnAdapter",
|
|
14
|
+
]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Anthropic Messages API transport adapter.
|
|
2
|
+
|
|
3
|
+
The native Anthropic wire shape differs from OpenAI: the system prompt is a
|
|
4
|
+
top-level param (not a message), assistant tool requests are ``tool_use``
|
|
5
|
+
content blocks, and tool results go back as ``tool_result`` blocks inside a
|
|
6
|
+
*user* message. Streaming arrives as typed events (``content_block_start`` /
|
|
7
|
+
``content_block_delta`` with ``text_delta`` / ``thinking_delta`` /
|
|
8
|
+
``input_json_delta`` / ``message_delta``). This adapter normalizes all of that
|
|
9
|
+
into the same :class:`AdapterChunk` stream the kernel consumes, so the one loop
|
|
10
|
+
serves Claude too.
|
|
11
|
+
|
|
12
|
+
Client-agnostic: accepts any object exposing ``.messages.create(stream=True)``
|
|
13
|
+
(the ``anthropic`` SDK client or a test double).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import AsyncIterator
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from crowe_agent_core.adapters.base import AdapterChunk, TransportAdapter
|
|
22
|
+
from crowe_agent_core.adapters.openai_compat import _parse_args
|
|
23
|
+
from crowe_agent_core.protocol import Message, ToolCall
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _to_anthropic(messages: list[Message]) -> tuple[str, list[dict[str, Any]]]:
|
|
27
|
+
"""Return (system_prompt, anthropic_messages). Groups consecutive tool
|
|
28
|
+
results into a single following user message, as the API requires."""
|
|
29
|
+
system = ""
|
|
30
|
+
out: list[dict[str, Any]] = []
|
|
31
|
+
pending_results: list[dict[str, Any]] = []
|
|
32
|
+
|
|
33
|
+
def flush() -> None:
|
|
34
|
+
nonlocal pending_results
|
|
35
|
+
if pending_results:
|
|
36
|
+
out.append({"role": "user", "content": pending_results})
|
|
37
|
+
pending_results = []
|
|
38
|
+
|
|
39
|
+
for m in messages:
|
|
40
|
+
if m.role == "system":
|
|
41
|
+
system = m.content
|
|
42
|
+
continue
|
|
43
|
+
if m.role == "tool":
|
|
44
|
+
pending_results.append(
|
|
45
|
+
{
|
|
46
|
+
"type": "tool_result",
|
|
47
|
+
"tool_use_id": m.tool_call_id,
|
|
48
|
+
"content": m.content,
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
continue
|
|
52
|
+
flush()
|
|
53
|
+
if m.role == "assistant":
|
|
54
|
+
blocks: list[dict[str, Any]] = []
|
|
55
|
+
if m.content:
|
|
56
|
+
blocks.append({"type": "text", "text": m.content})
|
|
57
|
+
for tc in m.tool_calls:
|
|
58
|
+
blocks.append(
|
|
59
|
+
{"type": "tool_use", "id": tc.id, "name": tc.name, "input": tc.arguments}
|
|
60
|
+
)
|
|
61
|
+
out.append({"role": "assistant", "content": blocks or (m.content or " ")})
|
|
62
|
+
else: # user
|
|
63
|
+
out.append({"role": "user", "content": m.content})
|
|
64
|
+
flush()
|
|
65
|
+
return system, out
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _to_anthropic_tools(
|
|
69
|
+
tools: list[dict[str, Any]], *, cache_last: bool = False
|
|
70
|
+
) -> list[dict[str, Any]]:
|
|
71
|
+
"""Convert OpenAI-style tool schemas to Anthropic ``input_schema`` shape.
|
|
72
|
+
|
|
73
|
+
``cache_last`` marks the final tool with ``cache_control: ephemeral``,
|
|
74
|
+
which tells Anthropic to cache the whole (stable, reused-every-turn)
|
|
75
|
+
tools prefix — cached input is ~10x cheaper than fresh input.
|
|
76
|
+
"""
|
|
77
|
+
out: list[dict[str, Any]] = []
|
|
78
|
+
for t in tools:
|
|
79
|
+
fn = t.get("function", t)
|
|
80
|
+
out.append(
|
|
81
|
+
{
|
|
82
|
+
"name": fn.get("name"),
|
|
83
|
+
"description": fn.get("description", ""),
|
|
84
|
+
"input_schema": fn.get("parameters", {"type": "object", "properties": {}}),
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
if cache_last and out:
|
|
88
|
+
out[-1] = {**out[-1], "cache_control": {"type": "ephemeral"}}
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _mark_conversation_cached(amsgs: list[dict[str, Any]]) -> None:
|
|
93
|
+
"""Add a cache breakpoint at the end of the conversation (in place).
|
|
94
|
+
|
|
95
|
+
Marks the last content block of the last message with
|
|
96
|
+
``cache_control: ephemeral`` so Anthropic caches the entire history
|
|
97
|
+
prefix. In a multi-turn tool loop each turn reuses the prior turn's
|
|
98
|
+
cached prefix (5-min TTL), so only the newest tokens pay full price —
|
|
99
|
+
where "input dominates agentic volume". A string content is promoted to
|
|
100
|
+
a one-block list so the marker has somewhere to live. No-op on empty.
|
|
101
|
+
"""
|
|
102
|
+
if not amsgs:
|
|
103
|
+
return
|
|
104
|
+
last = amsgs[-1]
|
|
105
|
+
content = last.get("content")
|
|
106
|
+
if isinstance(content, str):
|
|
107
|
+
last["content"] = [
|
|
108
|
+
{"type": "text", "text": content, "cache_control": {"type": "ephemeral"}}
|
|
109
|
+
]
|
|
110
|
+
elif isinstance(content, list) and content:
|
|
111
|
+
content[-1] = {**content[-1], "cache_control": {"type": "ephemeral"}}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AnthropicAdapter(TransportAdapter):
|
|
115
|
+
backend_id = "anthropic"
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
client: Any,
|
|
120
|
+
model: str,
|
|
121
|
+
*,
|
|
122
|
+
max_tokens: int = 4096,
|
|
123
|
+
cache_system: bool = True,
|
|
124
|
+
cache_tools: bool = True,
|
|
125
|
+
cache_conversation: bool = True,
|
|
126
|
+
extra_params: dict[str, Any] | None = None,
|
|
127
|
+
) -> None:
|
|
128
|
+
self._client = client
|
|
129
|
+
self._model = model
|
|
130
|
+
self._max_tokens = max_tokens
|
|
131
|
+
self._cache_system = cache_system
|
|
132
|
+
self._cache_tools = cache_tools
|
|
133
|
+
self._cache_conversation = cache_conversation
|
|
134
|
+
self._extra = dict(extra_params or {})
|
|
135
|
+
|
|
136
|
+
async def stream(
|
|
137
|
+
self,
|
|
138
|
+
messages: list[Message],
|
|
139
|
+
tools: list[dict[str, Any]],
|
|
140
|
+
) -> AsyncIterator[AdapterChunk]:
|
|
141
|
+
system, amsgs = _to_anthropic(messages)
|
|
142
|
+
system_block: Any = system
|
|
143
|
+
if self._cache_system and system:
|
|
144
|
+
system_block = [
|
|
145
|
+
{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
if self._cache_conversation:
|
|
149
|
+
_mark_conversation_cached(amsgs)
|
|
150
|
+
|
|
151
|
+
create_kwargs: dict[str, Any] = {
|
|
152
|
+
"model": self._model,
|
|
153
|
+
"max_tokens": self._max_tokens,
|
|
154
|
+
"messages": amsgs,
|
|
155
|
+
"stream": True,
|
|
156
|
+
}
|
|
157
|
+
if system:
|
|
158
|
+
create_kwargs["system"] = system_block
|
|
159
|
+
create_kwargs.update(self._extra)
|
|
160
|
+
if tools:
|
|
161
|
+
create_kwargs["tools"] = _to_anthropic_tools(
|
|
162
|
+
tools, cache_last=self._cache_tools
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
stream = self._client.messages.create(**create_kwargs)
|
|
166
|
+
|
|
167
|
+
blocks: dict[int, dict[str, str]] = {}
|
|
168
|
+
stop_reason = "end_turn"
|
|
169
|
+
|
|
170
|
+
for event in stream:
|
|
171
|
+
etype = getattr(event, "type", None)
|
|
172
|
+
if etype == "content_block_start":
|
|
173
|
+
cb = getattr(event, "content_block", None)
|
|
174
|
+
if cb is not None and getattr(cb, "type", None) == "tool_use":
|
|
175
|
+
blocks[event.index] = {
|
|
176
|
+
"id": cb.id,
|
|
177
|
+
"name": cb.name,
|
|
178
|
+
"input": "",
|
|
179
|
+
}
|
|
180
|
+
elif etype == "content_block_delta":
|
|
181
|
+
d = getattr(event, "delta", None)
|
|
182
|
+
dtype = getattr(d, "type", None) if d else None
|
|
183
|
+
if dtype == "thinking_delta":
|
|
184
|
+
yield AdapterChunk(kind="reasoning", text=getattr(d, "thinking", "") or "")
|
|
185
|
+
elif dtype == "text_delta":
|
|
186
|
+
yield AdapterChunk(kind="text", text=getattr(d, "text", "") or "")
|
|
187
|
+
elif dtype == "input_json_delta":
|
|
188
|
+
blk = blocks.get(getattr(event, "index", -1))
|
|
189
|
+
if blk is not None:
|
|
190
|
+
blk["input"] += getattr(d, "partial_json", "") or ""
|
|
191
|
+
elif etype == "message_delta":
|
|
192
|
+
d = getattr(event, "delta", None)
|
|
193
|
+
sr = getattr(d, "stop_reason", None) if d else None
|
|
194
|
+
if sr:
|
|
195
|
+
stop_reason = sr
|
|
196
|
+
|
|
197
|
+
for idx in sorted(blocks):
|
|
198
|
+
blk = blocks[idx]
|
|
199
|
+
yield AdapterChunk(
|
|
200
|
+
kind="tool_call",
|
|
201
|
+
tool_call=ToolCall(
|
|
202
|
+
id=blk["id"],
|
|
203
|
+
name=(blk["name"] or "").strip() or "invalid_tool_call",
|
|
204
|
+
arguments=_parse_args(blk["input"]),
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if blocks or stop_reason == "tool_use":
|
|
209
|
+
stop_reason = "tool_use"
|
|
210
|
+
yield AdapterChunk(kind="done", stop_reason=stop_reason)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Transport adapter contract — the ONLY thing that varies per provider.
|
|
2
|
+
|
|
3
|
+
An adapter does exactly one job: given the conversation and the available tool
|
|
4
|
+
schemas, stream back normalized chunks (text/reasoning deltas and structured
|
|
5
|
+
tool-call requests) and a terminal ``done`` chunk carrying the stop reason. It
|
|
6
|
+
does NOT run the tool-calling loop — that is the kernel's job. This inversion is
|
|
7
|
+
what lets one loop serve OpenAI-compat, Anthropic-native, NVIDIA, Ollama,
|
|
8
|
+
watsonx, the sovereign Azure gateway, and DeepParallel (where one "turn" is a
|
|
9
|
+
full cluster synthesis) without divergent loops.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import abc
|
|
15
|
+
from collections.abc import AsyncIterator
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Literal
|
|
18
|
+
|
|
19
|
+
from crowe_agent_core.protocol import Message, ToolCall
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class AdapterChunk:
|
|
24
|
+
"""One streamed unit from a provider, normalized.
|
|
25
|
+
|
|
26
|
+
``kind``:
|
|
27
|
+
- "text" -> ``text`` holds an answer delta
|
|
28
|
+
- "reasoning" -> ``text`` holds a hidden-reasoning delta
|
|
29
|
+
- "tool_call" -> ``tool_call`` holds a (possibly partial-then-complete) call
|
|
30
|
+
- "done" -> ``stop_reason`` is "end_turn" | "tool_use" | "length"
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
kind: Literal["text", "reasoning", "tool_call", "done"]
|
|
34
|
+
text: str = ""
|
|
35
|
+
tool_call: ToolCall | None = None
|
|
36
|
+
stop_reason: str | None = None
|
|
37
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TransportAdapter(abc.ABC):
|
|
41
|
+
"""Base class for provider adapters. Subclasses implement :meth:`stream`."""
|
|
42
|
+
|
|
43
|
+
#: Stable id used by role files to select a backend (e.g. "openai_compat").
|
|
44
|
+
backend_id: str = "abstract"
|
|
45
|
+
|
|
46
|
+
@abc.abstractmethod
|
|
47
|
+
def stream(
|
|
48
|
+
self,
|
|
49
|
+
messages: list[Message],
|
|
50
|
+
tools: list[dict[str, Any]],
|
|
51
|
+
) -> AsyncIterator[AdapterChunk]:
|
|
52
|
+
"""Stream normalized chunks for one model turn.
|
|
53
|
+
|
|
54
|
+
Implementations are async generators. They must emit a terminal
|
|
55
|
+
``AdapterChunk(kind="done", stop_reason=...)`` so the kernel knows
|
|
56
|
+
whether to continue (tool_use) or finish (end_turn).
|
|
57
|
+
"""
|
|
58
|
+
raise NotImplementedError
|