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.
Files changed (33) hide show
  1. crowe_agent_core-0.1.0/LICENSE +115 -0
  2. crowe_agent_core-0.1.0/PKG-INFO +72 -0
  3. crowe_agent_core-0.1.0/README.md +44 -0
  4. crowe_agent_core-0.1.0/crowe_agent_core/__init__.py +52 -0
  5. crowe_agent_core-0.1.0/crowe_agent_core/adapters/__init__.py +14 -0
  6. crowe_agent_core-0.1.0/crowe_agent_core/adapters/anthropic.py +210 -0
  7. crowe_agent_core-0.1.0/crowe_agent_core/adapters/base.py +58 -0
  8. crowe_agent_core-0.1.0/crowe_agent_core/adapters/openai_compat.py +251 -0
  9. crowe_agent_core-0.1.0/crowe_agent_core/adapters/scripted.py +38 -0
  10. crowe_agent_core-0.1.0/crowe_agent_core/adapters/single_turn.py +61 -0
  11. crowe_agent_core-0.1.0/crowe_agent_core/capability.py +150 -0
  12. crowe_agent_core-0.1.0/crowe_agent_core/hooks.py +83 -0
  13. crowe_agent_core-0.1.0/crowe_agent_core/kernel.py +139 -0
  14. crowe_agent_core-0.1.0/crowe_agent_core/protocol.py +75 -0
  15. crowe_agent_core-0.1.0/crowe_agent_core/protocol_cmp.py +94 -0
  16. crowe_agent_core-0.1.0/crowe_agent_core/roles.py +40 -0
  17. crowe_agent_core-0.1.0/crowe_agent_core/tool_args.py +132 -0
  18. crowe_agent_core-0.1.0/crowe_agent_core/tools/__init__.py +3 -0
  19. crowe_agent_core-0.1.0/crowe_agent_core/tools/registry.py +84 -0
  20. crowe_agent_core-0.1.0/crowe_agent_core.egg-info/PKG-INFO +72 -0
  21. crowe_agent_core-0.1.0/crowe_agent_core.egg-info/SOURCES.txt +31 -0
  22. crowe_agent_core-0.1.0/crowe_agent_core.egg-info/dependency_links.txt +1 -0
  23. crowe_agent_core-0.1.0/crowe_agent_core.egg-info/requires.txt +7 -0
  24. crowe_agent_core-0.1.0/crowe_agent_core.egg-info/top_level.txt +1 -0
  25. crowe_agent_core-0.1.0/pyproject.toml +42 -0
  26. crowe_agent_core-0.1.0/setup.cfg +4 -0
  27. crowe_agent_core-0.1.0/tests/test_anthropic_adapter.py +179 -0
  28. crowe_agent_core-0.1.0/tests/test_capability.py +134 -0
  29. crowe_agent_core-0.1.0/tests/test_kernel.py +138 -0
  30. crowe_agent_core-0.1.0/tests/test_openai_compat_adapter.py +192 -0
  31. crowe_agent_core-0.1.0/tests/test_protocol_cmp.py +72 -0
  32. crowe_agent_core-0.1.0/tests/test_single_turn_adapter.py +42 -0
  33. 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