copass-core-agents 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.
- copass_core_agents-0.1.0/.gitignore +38 -0
- copass_core_agents-0.1.0/PKG-INFO +60 -0
- copass_core_agents-0.1.0/README.md +38 -0
- copass_core_agents-0.1.0/pyproject.toml +55 -0
- copass_core_agents-0.1.0/src/copass_core_agents/__init__.py +99 -0
- copass_core_agents-0.1.0/src/copass_core_agents/backends/__init__.py +10 -0
- copass_core_agents-0.1.0/src/copass_core_agents/backends/base_backend.py +87 -0
- copass_core_agents-0.1.0/src/copass_core_agents/base_agent.py +167 -0
- copass_core_agents-0.1.0/src/copass_core_agents/base_tool.py +104 -0
- copass_core_agents-0.1.0/src/copass_core_agents/events.py +102 -0
- copass_core_agents-0.1.0/src/copass_core_agents/invocation_context.py +58 -0
- copass_core_agents-0.1.0/src/copass_core_agents/registry.py +79 -0
- copass_core_agents-0.1.0/src/copass_core_agents/scope.py +47 -0
- copass_core_agents-0.1.0/src/copass_core_agents/tool_registry.py +67 -0
- copass_core_agents-0.1.0/src/copass_core_agents/tool_resolver.py +50 -0
- copass_core_agents-0.1.0/tests/test_base_agent.py +173 -0
- copass_core_agents-0.1.0/tests/test_invocation_context.py +35 -0
- copass_core_agents-0.1.0/tests/test_scope.py +36 -0
- copass_core_agents-0.1.0/tests/test_tool_registry.py +71 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build output
|
|
5
|
+
dist/
|
|
6
|
+
*.tsbuildinfo
|
|
7
|
+
|
|
8
|
+
# Environment
|
|
9
|
+
.env
|
|
10
|
+
.env.*
|
|
11
|
+
|
|
12
|
+
# IDE
|
|
13
|
+
.vscode/
|
|
14
|
+
.idea/
|
|
15
|
+
*.swp
|
|
16
|
+
*.swo
|
|
17
|
+
*~
|
|
18
|
+
|
|
19
|
+
# OS
|
|
20
|
+
.DS_Store
|
|
21
|
+
Thumbs.db
|
|
22
|
+
|
|
23
|
+
# Test
|
|
24
|
+
coverage/
|
|
25
|
+
|
|
26
|
+
# Lerna
|
|
27
|
+
lerna-debug.log
|
|
28
|
+
.nx/cache
|
|
29
|
+
.nx/workspace-data
|
|
30
|
+
|
|
31
|
+
# Python
|
|
32
|
+
__pycache__/
|
|
33
|
+
*.pyc
|
|
34
|
+
*.pyo
|
|
35
|
+
*.egg-info/
|
|
36
|
+
.venv/
|
|
37
|
+
venv/
|
|
38
|
+
.olane
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: copass-core-agents
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Provider-neutral agent primitives shared by every Copass agent SDK
|
|
5
|
+
Project-URL: Homepage, https://github.com/olane-labs/copass-harness
|
|
6
|
+
Project-URL: Repository, https://github.com/olane-labs/copass-harness.git
|
|
7
|
+
Author: Olane Inc.
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: abc,agents,copass,knowledge-graph,primitives
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# copass-core-agents
|
|
24
|
+
|
|
25
|
+
Provider-neutral agent primitives shared by every Copass agent SDK.
|
|
26
|
+
|
|
27
|
+
This package owns the ABCs, value types, and registries that each
|
|
28
|
+
provider-specific SDK (`copass-anthropic-agents`, future
|
|
29
|
+
`copass-openai-agents`, `copass-google-agents`, etc.) implements
|
|
30
|
+
against. It carries zero vendor dependencies.
|
|
31
|
+
|
|
32
|
+
## What's in here
|
|
33
|
+
|
|
34
|
+
- `BaseAgent` — identity + prompt + tool surface + backend
|
|
35
|
+
- `AgentScope`, `AgentInvocationContext` — tenancy + per-call context
|
|
36
|
+
- `AgentTool`, `AgentToolRegistry`, `AgentToolResolver`, `ToolSpec`,
|
|
37
|
+
`ToolCall` — tool abstractions
|
|
38
|
+
- `AgentEvent` (plus `AgentTextDelta`, `AgentToolCall`,
|
|
39
|
+
`AgentToolResult`, `AgentFinish`) — streaming event union
|
|
40
|
+
- `AgentBackend` ABC + `AgentRunResult`
|
|
41
|
+
- `register_agent` / `register_agent_tool` registries
|
|
42
|
+
|
|
43
|
+
## Which package should I install?
|
|
44
|
+
|
|
45
|
+
| I want to… | Install |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Run a Claude Managed Agent (Anthropic) | `copass-anthropic-agents` (pulls this in transitively) |
|
|
48
|
+
| Build my own backend / extend the ABCs | `copass-core-agents` directly |
|
|
49
|
+
|
|
50
|
+
If you're end-user code invoking an agent, you almost never install
|
|
51
|
+
this package directly — you install a provider SDK and it re-exports
|
|
52
|
+
what you need.
|
|
53
|
+
|
|
54
|
+
## Dependencies
|
|
55
|
+
|
|
56
|
+
Zero runtime dependencies. Python ≥ 3.10.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# copass-core-agents
|
|
2
|
+
|
|
3
|
+
Provider-neutral agent primitives shared by every Copass agent SDK.
|
|
4
|
+
|
|
5
|
+
This package owns the ABCs, value types, and registries that each
|
|
6
|
+
provider-specific SDK (`copass-anthropic-agents`, future
|
|
7
|
+
`copass-openai-agents`, `copass-google-agents`, etc.) implements
|
|
8
|
+
against. It carries zero vendor dependencies.
|
|
9
|
+
|
|
10
|
+
## What's in here
|
|
11
|
+
|
|
12
|
+
- `BaseAgent` — identity + prompt + tool surface + backend
|
|
13
|
+
- `AgentScope`, `AgentInvocationContext` — tenancy + per-call context
|
|
14
|
+
- `AgentTool`, `AgentToolRegistry`, `AgentToolResolver`, `ToolSpec`,
|
|
15
|
+
`ToolCall` — tool abstractions
|
|
16
|
+
- `AgentEvent` (plus `AgentTextDelta`, `AgentToolCall`,
|
|
17
|
+
`AgentToolResult`, `AgentFinish`) — streaming event union
|
|
18
|
+
- `AgentBackend` ABC + `AgentRunResult`
|
|
19
|
+
- `register_agent` / `register_agent_tool` registries
|
|
20
|
+
|
|
21
|
+
## Which package should I install?
|
|
22
|
+
|
|
23
|
+
| I want to… | Install |
|
|
24
|
+
|---|---|
|
|
25
|
+
| Run a Claude Managed Agent (Anthropic) | `copass-anthropic-agents` (pulls this in transitively) |
|
|
26
|
+
| Build my own backend / extend the ABCs | `copass-core-agents` directly |
|
|
27
|
+
|
|
28
|
+
If you're end-user code invoking an agent, you almost never install
|
|
29
|
+
this package directly — you install a provider SDK and it re-exports
|
|
30
|
+
what you need.
|
|
31
|
+
|
|
32
|
+
## Dependencies
|
|
33
|
+
|
|
34
|
+
Zero runtime dependencies. Python ≥ 3.10.
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "copass-core-agents"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Provider-neutral agent primitives shared by every Copass agent SDK"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Olane Inc." }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = [
|
|
14
|
+
"copass",
|
|
15
|
+
"knowledge-graph",
|
|
16
|
+
"agents",
|
|
17
|
+
"abc",
|
|
18
|
+
"primitives",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8.0",
|
|
32
|
+
"pytest-asyncio>=0.23",
|
|
33
|
+
"mypy>=1.10",
|
|
34
|
+
"ruff>=0.5",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/olane-labs/copass-harness"
|
|
39
|
+
Repository = "https://github.com/olane-labs/copass-harness.git"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/copass_core_agents"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
asyncio_mode = "auto"
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 100
|
|
50
|
+
target-version = "py310"
|
|
51
|
+
|
|
52
|
+
[tool.mypy]
|
|
53
|
+
python_version = "3.10"
|
|
54
|
+
strict = true
|
|
55
|
+
packages = ["copass_core_agents"]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Copass Core Agents — provider-neutral agent primitives.
|
|
2
|
+
|
|
3
|
+
Shared ABCs and value types used by every provider-specific Copass
|
|
4
|
+
agent SDK (``copass-anthropic-agents``, future ``copass-openai-agents``,
|
|
5
|
+
``copass-google-agents``, etc.). Concrete backends, convenience
|
|
6
|
+
subclasses, and vendor-specific wiring live in those per-provider
|
|
7
|
+
packages; this package owns nothing vendor-specific.
|
|
8
|
+
|
|
9
|
+
Public surface:
|
|
10
|
+
|
|
11
|
+
Core
|
|
12
|
+
BaseAgent — identity + prompt + tools + backend
|
|
13
|
+
AgentScope — tenancy payload
|
|
14
|
+
AgentInvocationContext — per-call runtime context
|
|
15
|
+
AgentTool — ABC for a tool an agent can invoke
|
|
16
|
+
AgentToolRegistry — per-agent collection of tools
|
|
17
|
+
AgentToolResolver — scope-aware dynamic tool producer
|
|
18
|
+
ToolSpec, ToolCall — tool catalog shapes
|
|
19
|
+
ToolConflictPolicy — "error" | "dynamic_wins" | "static_wins"
|
|
20
|
+
ToolConflictError
|
|
21
|
+
|
|
22
|
+
Events (emitted by AgentBackend.stream)
|
|
23
|
+
AgentEvent — tagged union
|
|
24
|
+
AgentTextDelta
|
|
25
|
+
AgentToolCall
|
|
26
|
+
AgentToolResult
|
|
27
|
+
AgentFinish
|
|
28
|
+
|
|
29
|
+
Backends
|
|
30
|
+
AgentBackend — ABC (concrete impls live per-provider)
|
|
31
|
+
AgentRunResult — reduced run() output
|
|
32
|
+
|
|
33
|
+
Registries (optional lookup-by-name)
|
|
34
|
+
register_agent, get_agent_class, list_agents
|
|
35
|
+
register_agent_tool, get_agent_tool, try_get_agent_tool,
|
|
36
|
+
list_agent_tools
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from copass_core_agents.backends import AgentBackend, AgentRunResult
|
|
40
|
+
from copass_core_agents.base_agent import BaseAgent
|
|
41
|
+
from copass_core_agents.base_tool import AgentTool, ToolCall, ToolSpec
|
|
42
|
+
from copass_core_agents.events import (
|
|
43
|
+
AgentEvent,
|
|
44
|
+
AgentFinish,
|
|
45
|
+
AgentTextDelta,
|
|
46
|
+
AgentToolCall,
|
|
47
|
+
AgentToolResult,
|
|
48
|
+
)
|
|
49
|
+
from copass_core_agents.invocation_context import AgentInvocationContext
|
|
50
|
+
from copass_core_agents.registry import (
|
|
51
|
+
get_agent_class,
|
|
52
|
+
get_agent_tool,
|
|
53
|
+
list_agent_tools,
|
|
54
|
+
list_agents,
|
|
55
|
+
register_agent,
|
|
56
|
+
register_agent_tool,
|
|
57
|
+
try_get_agent_tool,
|
|
58
|
+
)
|
|
59
|
+
from copass_core_agents.scope import AgentScope
|
|
60
|
+
from copass_core_agents.tool_registry import AgentToolRegistry
|
|
61
|
+
from copass_core_agents.tool_resolver import (
|
|
62
|
+
AgentToolResolver,
|
|
63
|
+
ToolConflictError,
|
|
64
|
+
ToolConflictPolicy,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
__version__ = "0.1.0"
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"__version__",
|
|
71
|
+
# Core
|
|
72
|
+
"BaseAgent",
|
|
73
|
+
"AgentScope",
|
|
74
|
+
"AgentInvocationContext",
|
|
75
|
+
"AgentTool",
|
|
76
|
+
"AgentToolRegistry",
|
|
77
|
+
"AgentToolResolver",
|
|
78
|
+
"ToolSpec",
|
|
79
|
+
"ToolCall",
|
|
80
|
+
"ToolConflictError",
|
|
81
|
+
"ToolConflictPolicy",
|
|
82
|
+
# Events
|
|
83
|
+
"AgentEvent",
|
|
84
|
+
"AgentTextDelta",
|
|
85
|
+
"AgentToolCall",
|
|
86
|
+
"AgentToolResult",
|
|
87
|
+
"AgentFinish",
|
|
88
|
+
# Backends
|
|
89
|
+
"AgentBackend",
|
|
90
|
+
"AgentRunResult",
|
|
91
|
+
# Registries
|
|
92
|
+
"register_agent",
|
|
93
|
+
"get_agent_class",
|
|
94
|
+
"list_agents",
|
|
95
|
+
"register_agent_tool",
|
|
96
|
+
"get_agent_tool",
|
|
97
|
+
"try_get_agent_tool",
|
|
98
|
+
"list_agent_tools",
|
|
99
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Agent backend ABCs — concrete backends live in per-provider
|
|
2
|
+
packages (``copass-anthropic-agents``, future ``copass-openai-agents``,
|
|
3
|
+
``copass-google-agents``)."""
|
|
4
|
+
|
|
5
|
+
from copass_core_agents.backends.base_backend import (
|
|
6
|
+
AgentBackend,
|
|
7
|
+
AgentRunResult,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = ["AgentBackend", "AgentRunResult"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""AgentBackend — runtime ABC for executing an agent turn.
|
|
2
|
+
|
|
3
|
+
A backend is the seam between the provider-neutral agent surface
|
|
4
|
+
(``BaseAgent``, ``AgentTool``, ``AgentEvent``) and a concrete
|
|
5
|
+
SDK/provider.
|
|
6
|
+
|
|
7
|
+
Design rules:
|
|
8
|
+
|
|
9
|
+
- Base classes must NOT import any vendor SDK. This file only depends
|
|
10
|
+
on plain data types and the SDK's own ABCs.
|
|
11
|
+
- Backends translate between the provider's tool-use format and the
|
|
12
|
+
agent's ``AgentToolRegistry`` + ``AgentEvent`` stream.
|
|
13
|
+
- A single backend instance may be shared across agents and requests.
|
|
14
|
+
Per-request state lives on ``AgentInvocationContext``, not on the
|
|
15
|
+
backend.
|
|
16
|
+
|
|
17
|
+
Concrete backends live in per-provider packages
|
|
18
|
+
(``copass-anthropic-agents``, future ``copass-openai-agents``,
|
|
19
|
+
``copass-google-agents``). This file and ``AgentRunResult`` are the
|
|
20
|
+
only ABC surface they implement against.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from abc import ABC, abstractmethod
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import TYPE_CHECKING, AsyncIterator, List, Optional
|
|
28
|
+
|
|
29
|
+
from copass_core_agents.events import AgentEvent
|
|
30
|
+
from copass_core_agents.invocation_context import AgentInvocationContext
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from copass_core_agents.base_agent import BaseAgent
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class AgentRunResult:
|
|
38
|
+
"""Reduced output of a non-streaming ``AgentBackend.run``."""
|
|
39
|
+
|
|
40
|
+
final_text: str
|
|
41
|
+
tool_calls: List[dict] = field(default_factory=list)
|
|
42
|
+
stop_reason: str = "end_turn"
|
|
43
|
+
usage: dict = field(default_factory=dict)
|
|
44
|
+
session_id: Optional[str] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AgentBackend(ABC):
|
|
48
|
+
"""Runtime adapter between the agent surface and a provider SDK.
|
|
49
|
+
|
|
50
|
+
Construction takes an optional ``config`` dict. Backend-specific
|
|
51
|
+
knobs (API keys, model allow-lists, timeout overrides) live there
|
|
52
|
+
rather than as strong fields so the ABC signature stays stable as
|
|
53
|
+
providers evolve.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, *, config: Optional[dict] = None) -> None:
|
|
57
|
+
self._config = dict(config or {})
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def config(self) -> dict:
|
|
61
|
+
"""Backend-specific configuration. Read-only view."""
|
|
62
|
+
return dict(self._config)
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
async def run(
|
|
66
|
+
self,
|
|
67
|
+
agent: "BaseAgent",
|
|
68
|
+
messages: List[dict],
|
|
69
|
+
context: AgentInvocationContext,
|
|
70
|
+
) -> AgentRunResult:
|
|
71
|
+
"""Drive the conversation to a stop condition, return a
|
|
72
|
+
reduced result."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def stream(
|
|
77
|
+
self,
|
|
78
|
+
agent: "BaseAgent",
|
|
79
|
+
messages: List[dict],
|
|
80
|
+
context: AgentInvocationContext,
|
|
81
|
+
) -> AsyncIterator[AgentEvent]:
|
|
82
|
+
"""Drive the conversation and yield ``AgentEvent`` as they
|
|
83
|
+
occur."""
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = ["AgentBackend", "AgentRunResult"]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""BaseAgent — ABC for an identity-bound, tool-equipped agent.
|
|
2
|
+
|
|
3
|
+
An agent is the composition of:
|
|
4
|
+
|
|
5
|
+
- an **identity** (stable string used in logs, prompts, and registry
|
|
6
|
+
lookups)
|
|
7
|
+
- a **model** (provider/SDK-specific model name — the backend
|
|
8
|
+
validates it)
|
|
9
|
+
- a **system prompt** (role + instructions fed on every turn)
|
|
10
|
+
- an **AgentToolRegistry** holding the *static* tools
|
|
11
|
+
- an optional **AgentToolResolver** producing *dynamic* tools per
|
|
12
|
+
invocation
|
|
13
|
+
- an **AgentBackend** (the runtime that actually drives turns)
|
|
14
|
+
|
|
15
|
+
``BaseAgent`` itself has no turn-execution logic — both ``run`` and
|
|
16
|
+
``stream`` compute the effective tool registry for the invocation
|
|
17
|
+
(via :meth:`build_tools`) and delegate to the backend.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from abc import ABC
|
|
24
|
+
from typing import AsyncIterator, List, Optional
|
|
25
|
+
|
|
26
|
+
from copass_core_agents.backends.base_backend import (
|
|
27
|
+
AgentBackend,
|
|
28
|
+
AgentRunResult,
|
|
29
|
+
)
|
|
30
|
+
from copass_core_agents.base_tool import AgentTool
|
|
31
|
+
from copass_core_agents.events import AgentEvent
|
|
32
|
+
from copass_core_agents.invocation_context import AgentInvocationContext
|
|
33
|
+
from copass_core_agents.tool_registry import AgentToolRegistry
|
|
34
|
+
from copass_core_agents.tool_resolver import (
|
|
35
|
+
AgentToolResolver,
|
|
36
|
+
ToolConflictError,
|
|
37
|
+
ToolConflictPolicy,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BaseAgent(ABC):
|
|
44
|
+
"""Identity + prompt + tools + resolver + backend bundle.
|
|
45
|
+
|
|
46
|
+
Not abstract in the Python sense (no ``@abstractmethod``) — the
|
|
47
|
+
``ABC`` inheritance is a signal to subclassers that this is a
|
|
48
|
+
base type to extend, not to instantiate directly. Instantiating
|
|
49
|
+
``BaseAgent`` is valid for quick scripts/tests but production
|
|
50
|
+
callers should subclass with concrete identity and prompt.
|
|
51
|
+
|
|
52
|
+
Either ``tools`` or ``tool_resolver`` (or both) must be provided.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
*,
|
|
58
|
+
identity: str,
|
|
59
|
+
model: str,
|
|
60
|
+
system_prompt: str,
|
|
61
|
+
backend: AgentBackend,
|
|
62
|
+
tools: Optional[AgentToolRegistry] = None,
|
|
63
|
+
tool_resolver: Optional[AgentToolResolver] = None,
|
|
64
|
+
on_conflict: ToolConflictPolicy = "dynamic_wins",
|
|
65
|
+
) -> None:
|
|
66
|
+
if tools is None and tool_resolver is None:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"BaseAgent requires at least one of `tools` or "
|
|
69
|
+
"`tool_resolver` — an agent with no capabilities has "
|
|
70
|
+
"no reason to exist."
|
|
71
|
+
)
|
|
72
|
+
if on_conflict not in ("error", "dynamic_wins", "static_wins"):
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"BaseAgent: invalid on_conflict policy {on_conflict!r}. "
|
|
75
|
+
f"Expected 'error', 'dynamic_wins', or 'static_wins'."
|
|
76
|
+
)
|
|
77
|
+
self.identity = identity
|
|
78
|
+
self.model = model
|
|
79
|
+
self.system_prompt = system_prompt
|
|
80
|
+
self.backend = backend
|
|
81
|
+
self.tools = tools if tools is not None else AgentToolRegistry()
|
|
82
|
+
self.tool_resolver = tool_resolver
|
|
83
|
+
self.on_conflict: ToolConflictPolicy = on_conflict
|
|
84
|
+
|
|
85
|
+
async def run(
|
|
86
|
+
self,
|
|
87
|
+
messages: List[dict],
|
|
88
|
+
*,
|
|
89
|
+
context: AgentInvocationContext,
|
|
90
|
+
) -> AgentRunResult:
|
|
91
|
+
"""Drive a turn to completion and return the reduced result."""
|
|
92
|
+
return await self.backend.run(self, messages, context)
|
|
93
|
+
|
|
94
|
+
async def stream(
|
|
95
|
+
self,
|
|
96
|
+
messages: List[dict],
|
|
97
|
+
*,
|
|
98
|
+
context: AgentInvocationContext,
|
|
99
|
+
) -> AsyncIterator[AgentEvent]:
|
|
100
|
+
"""Drive a turn and yield ``AgentEvent`` as they occur."""
|
|
101
|
+
async for evt in self.backend.stream(self, messages, context):
|
|
102
|
+
yield evt
|
|
103
|
+
|
|
104
|
+
async def build_tools(
|
|
105
|
+
self, context: AgentInvocationContext
|
|
106
|
+
) -> AgentToolRegistry:
|
|
107
|
+
"""Compute the effective tool registry for this invocation."""
|
|
108
|
+
if self.tool_resolver is None:
|
|
109
|
+
return self.tools
|
|
110
|
+
|
|
111
|
+
dynamic_tools: List[AgentTool] = await self.tool_resolver.resolve(context)
|
|
112
|
+
|
|
113
|
+
merged = AgentToolRegistry()
|
|
114
|
+
static_names = {tool.spec.name for tool in self.tools}
|
|
115
|
+
dynamic_names = {tool.spec.name for tool in dynamic_tools}
|
|
116
|
+
collisions = static_names & dynamic_names
|
|
117
|
+
|
|
118
|
+
if collisions and self.on_conflict == "error":
|
|
119
|
+
raise ToolConflictError(
|
|
120
|
+
f"BaseAgent {self.identity!r}: static and dynamic tools "
|
|
121
|
+
f"collide on names: {sorted(collisions)}. Set "
|
|
122
|
+
f"on_conflict='dynamic_wins' or 'static_wins' to pick "
|
|
123
|
+
f"a resolution policy, or fix the duplicated name."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if self.on_conflict == "static_wins":
|
|
127
|
+
for tool in dynamic_tools:
|
|
128
|
+
if tool.spec.name not in static_names:
|
|
129
|
+
merged.add(tool)
|
|
130
|
+
for tool in self.tools:
|
|
131
|
+
merged.add(tool)
|
|
132
|
+
else:
|
|
133
|
+
for tool in self.tools:
|
|
134
|
+
if tool.spec.name not in dynamic_names:
|
|
135
|
+
merged.add(tool)
|
|
136
|
+
for tool in dynamic_tools:
|
|
137
|
+
merged.add(tool)
|
|
138
|
+
|
|
139
|
+
if collisions:
|
|
140
|
+
logger.info(
|
|
141
|
+
"BaseAgent.build_tools: resolved tool name collisions",
|
|
142
|
+
extra={
|
|
143
|
+
"identity": self.identity,
|
|
144
|
+
"policy": self.on_conflict,
|
|
145
|
+
"collisions": sorted(collisions),
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
return merged
|
|
149
|
+
|
|
150
|
+
def __repr__(self) -> str:
|
|
151
|
+
resolver = (
|
|
152
|
+
self.tool_resolver.__class__.__name__
|
|
153
|
+
if self.tool_resolver is not None
|
|
154
|
+
else "None"
|
|
155
|
+
)
|
|
156
|
+
return (
|
|
157
|
+
f"{self.__class__.__name__}("
|
|
158
|
+
f"identity={self.identity!r}, "
|
|
159
|
+
f"model={self.model!r}, "
|
|
160
|
+
f"static_tools={len(self.tools)}, "
|
|
161
|
+
f"resolver={resolver}, "
|
|
162
|
+
f"backend={self.backend.__class__.__name__}"
|
|
163
|
+
f")"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ["BaseAgent"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""AgentTool — tool ABC callable by an agent during a turn.
|
|
2
|
+
|
|
3
|
+
``ToolSpec`` / ``ToolCall`` are the provider-neutral catalog shapes.
|
|
4
|
+
Defined here in the core package; provider adapters (Anthropic,
|
|
5
|
+
OpenAI, Google) translate them into provider-specific tool-use
|
|
6
|
+
catalog payloads.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from copass_core_agents.invocation_context import AgentInvocationContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ToolSpec:
|
|
20
|
+
"""Catalog entry for one tool — what the model sees.
|
|
21
|
+
|
|
22
|
+
Shape matches the JSON-schema-based tool-use conventions across
|
|
23
|
+
providers so a single ``ToolSpec`` can be passed to any backend's
|
|
24
|
+
``tools=[]`` parameter after trivial adaptation.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
name: str
|
|
28
|
+
description: str
|
|
29
|
+
input_schema: dict
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ToolCall:
|
|
34
|
+
"""Audit record of one tool invocation within a turn.
|
|
35
|
+
|
|
36
|
+
Not used by the agent runtime's hot path (events carry the
|
|
37
|
+
canonical structure); this type is the shape backends / consumers
|
|
38
|
+
serialize when they want to persist a turn's tool history.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
arguments: dict
|
|
43
|
+
result: dict
|
|
44
|
+
error: Optional[str] = None
|
|
45
|
+
metadata: dict = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AgentTool(ABC):
|
|
49
|
+
"""One capability an agent can invoke during a turn.
|
|
50
|
+
|
|
51
|
+
Implementations must be:
|
|
52
|
+
|
|
53
|
+
- Stateless across calls (no per-invocation mutable state on the
|
|
54
|
+
instance). Concurrency is driven by the backend and multiple
|
|
55
|
+
``invoke`` calls may run in parallel.
|
|
56
|
+
- JSON-result-returning. The returned dict is fed back to the
|
|
57
|
+
model verbatim; it must be serializable and should stay small
|
|
58
|
+
enough to fit in the model's context window.
|
|
59
|
+
- Schema-honoring. ``arguments`` always matches
|
|
60
|
+
``spec.input_schema`` — the backend is responsible for
|
|
61
|
+
validating before calling ``invoke``.
|
|
62
|
+
|
|
63
|
+
Implementations must NOT:
|
|
64
|
+
|
|
65
|
+
- Raise arbitrary exceptions on recoverable errors. Convert
|
|
66
|
+
tool-internal failures into a dict result with an ``error``
|
|
67
|
+
field the model can read and retry against. Reserve raising
|
|
68
|
+
for programmer errors (schema drift, unreachable branches).
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def spec(self) -> ToolSpec:
|
|
74
|
+
"""Return the catalog entry. Must be stable across calls."""
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
async def invoke(
|
|
79
|
+
self,
|
|
80
|
+
arguments: dict,
|
|
81
|
+
*,
|
|
82
|
+
context: Optional[AgentInvocationContext] = None,
|
|
83
|
+
) -> dict:
|
|
84
|
+
"""Execute the tool.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
arguments: JSON-decoded tool arguments. Matches
|
|
88
|
+
``spec.input_schema``.
|
|
89
|
+
context: Per-invocation context (scope, dek, handles).
|
|
90
|
+
Optional — tools that don't need runtime handles
|
|
91
|
+
should tolerate ``None``.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
JSON-serializable dict. Goes straight back to the model.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
Exception: Only for programmer errors / unrecoverable
|
|
98
|
+
failures. Recoverable issues belong in the return
|
|
99
|
+
dict.
|
|
100
|
+
"""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
__all__ = ["AgentTool", "ToolCall", "ToolSpec"]
|