anycode-py 0.1.0__py3-none-any.whl
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.
- anycode/__init__.py +121 -0
- anycode/collaboration/__init__.py +6 -0
- anycode/collaboration/kv_store.py +49 -0
- anycode/collaboration/message_bus.py +70 -0
- anycode/collaboration/shared_mem.py +55 -0
- anycode/collaboration/team.py +96 -0
- anycode/core/__init__.py +7 -0
- anycode/core/agent.py +141 -0
- anycode/core/orchestrator.py +267 -0
- anycode/core/pool.py +99 -0
- anycode/core/runner.py +176 -0
- anycode/core/scheduler.py +126 -0
- anycode/helpers/__init__.py +4 -0
- anycode/helpers/concurrency_gate.py +45 -0
- anycode/helpers/usage_tracker.py +14 -0
- anycode/providers/__init__.py +3 -0
- anycode/providers/adapter.py +22 -0
- anycode/providers/anthropic.py +149 -0
- anycode/providers/openai.py +208 -0
- anycode/tasks/__init__.py +4 -0
- anycode/tasks/queue.py +139 -0
- anycode/tasks/task.py +104 -0
- anycode/tools/__init__.py +21 -0
- anycode/tools/bash.py +67 -0
- anycode/tools/built_in.py +18 -0
- anycode/tools/executor.py +51 -0
- anycode/tools/file_edit.py +70 -0
- anycode/tools/file_read.py +52 -0
- anycode/tools/file_write.py +49 -0
- anycode/tools/grep.py +127 -0
- anycode/tools/registry.py +84 -0
- anycode/types.py +313 -0
- anycode_py-0.1.0.dist-info/METADATA +399 -0
- anycode_py-0.1.0.dist-info/RECORD +36 -0
- anycode_py-0.1.0.dist-info/WHEEL +4 -0
- anycode_py-0.1.0.dist-info/licenses/LICENSE +21 -0
anycode/__init__.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""AnyCode — scalable multi-agent AI orchestration framework for Python."""
|
|
2
|
+
|
|
3
|
+
from anycode.collaboration.kv_store import InMemoryStore
|
|
4
|
+
from anycode.collaboration.message_bus import MessageBus
|
|
5
|
+
from anycode.collaboration.shared_mem import SharedMemory
|
|
6
|
+
from anycode.collaboration.team import Team
|
|
7
|
+
from anycode.core.agent import Agent
|
|
8
|
+
from anycode.core.orchestrator import AnyCode, TaskSpec
|
|
9
|
+
from anycode.core.pool import AgentPool
|
|
10
|
+
from anycode.core.runner import AgentRunner
|
|
11
|
+
from anycode.core.scheduler import Scheduler
|
|
12
|
+
from anycode.helpers.concurrency_gate import Semaphore
|
|
13
|
+
from anycode.helpers.usage_tracker import EMPTY_USAGE, merge_usage
|
|
14
|
+
from anycode.providers.adapter import create_adapter
|
|
15
|
+
from anycode.tasks.queue import TaskQueue
|
|
16
|
+
from anycode.tasks.task import create_task, get_task_dependency_order, is_task_ready, validate_task_dependencies
|
|
17
|
+
from anycode.tools.built_in import BUILT_IN_TOOLS, register_built_in_tools
|
|
18
|
+
from anycode.tools.executor import ToolExecutor
|
|
19
|
+
from anycode.tools.registry import ToolRegistry, define_tool
|
|
20
|
+
from anycode.types import (
|
|
21
|
+
AgentConfig,
|
|
22
|
+
AgentInfo,
|
|
23
|
+
AgentRunResult,
|
|
24
|
+
AgentState,
|
|
25
|
+
ContentBlock,
|
|
26
|
+
ImageBlock,
|
|
27
|
+
LLMAdapter,
|
|
28
|
+
LLMChatOptions,
|
|
29
|
+
LLMMessage,
|
|
30
|
+
LLMResponse,
|
|
31
|
+
LLMStreamOptions,
|
|
32
|
+
LLMToolDef,
|
|
33
|
+
MemoryEntry,
|
|
34
|
+
MemoryStore,
|
|
35
|
+
OrchestratorConfig,
|
|
36
|
+
OrchestratorEvent,
|
|
37
|
+
PoolStatus,
|
|
38
|
+
RunnerOptions,
|
|
39
|
+
RunResult,
|
|
40
|
+
SchedulingStrategy,
|
|
41
|
+
StreamEvent,
|
|
42
|
+
Task,
|
|
43
|
+
TaskStatus,
|
|
44
|
+
TeamConfig,
|
|
45
|
+
TeamRunResult,
|
|
46
|
+
TextBlock,
|
|
47
|
+
TokenUsage,
|
|
48
|
+
ToolCallRecord,
|
|
49
|
+
ToolDefinition,
|
|
50
|
+
ToolResult,
|
|
51
|
+
ToolResultBlock,
|
|
52
|
+
ToolUseBlock,
|
|
53
|
+
ToolUseContext,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
# Core
|
|
58
|
+
"AnyCode",
|
|
59
|
+
"Agent",
|
|
60
|
+
"AgentRunner",
|
|
61
|
+
"AgentPool",
|
|
62
|
+
"Scheduler",
|
|
63
|
+
"TaskSpec",
|
|
64
|
+
# Providers
|
|
65
|
+
"create_adapter",
|
|
66
|
+
# Collaboration
|
|
67
|
+
"Team",
|
|
68
|
+
"MessageBus",
|
|
69
|
+
"SharedMemory",
|
|
70
|
+
"InMemoryStore",
|
|
71
|
+
# Tasks
|
|
72
|
+
"TaskQueue",
|
|
73
|
+
"create_task",
|
|
74
|
+
"is_task_ready",
|
|
75
|
+
"get_task_dependency_order",
|
|
76
|
+
"validate_task_dependencies",
|
|
77
|
+
# Tools
|
|
78
|
+
"ToolRegistry",
|
|
79
|
+
"ToolExecutor",
|
|
80
|
+
"define_tool",
|
|
81
|
+
"BUILT_IN_TOOLS",
|
|
82
|
+
"register_built_in_tools",
|
|
83
|
+
# Helpers
|
|
84
|
+
"Semaphore",
|
|
85
|
+
"EMPTY_USAGE",
|
|
86
|
+
"merge_usage",
|
|
87
|
+
# Types
|
|
88
|
+
"ContentBlock",
|
|
89
|
+
"TextBlock",
|
|
90
|
+
"ToolUseBlock",
|
|
91
|
+
"ToolResultBlock",
|
|
92
|
+
"ImageBlock",
|
|
93
|
+
"LLMMessage",
|
|
94
|
+
"TokenUsage",
|
|
95
|
+
"LLMResponse",
|
|
96
|
+
"StreamEvent",
|
|
97
|
+
"LLMToolDef",
|
|
98
|
+
"ToolUseContext",
|
|
99
|
+
"AgentInfo",
|
|
100
|
+
"ToolResult",
|
|
101
|
+
"ToolDefinition",
|
|
102
|
+
"AgentConfig",
|
|
103
|
+
"AgentState",
|
|
104
|
+
"ToolCallRecord",
|
|
105
|
+
"AgentRunResult",
|
|
106
|
+
"TeamConfig",
|
|
107
|
+
"TeamRunResult",
|
|
108
|
+
"TaskStatus",
|
|
109
|
+
"Task",
|
|
110
|
+
"OrchestratorEvent",
|
|
111
|
+
"OrchestratorConfig",
|
|
112
|
+
"MemoryEntry",
|
|
113
|
+
"MemoryStore",
|
|
114
|
+
"LLMChatOptions",
|
|
115
|
+
"LLMStreamOptions",
|
|
116
|
+
"LLMAdapter",
|
|
117
|
+
"RunnerOptions",
|
|
118
|
+
"RunResult",
|
|
119
|
+
"PoolStatus",
|
|
120
|
+
"SchedulingStrategy",
|
|
121
|
+
]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from anycode.collaboration.kv_store import InMemoryStore
|
|
2
|
+
from anycode.collaboration.message_bus import MessageBus
|
|
3
|
+
from anycode.collaboration.shared_mem import SharedMemory
|
|
4
|
+
from anycode.collaboration.team import Team
|
|
5
|
+
|
|
6
|
+
__all__ = ["InMemoryStore", "MessageBus", "SharedMemory", "Team"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Async key-value store backed by a dict."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from anycode.types import MemoryEntry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InMemoryStore:
|
|
12
|
+
"""Async in-memory key-value store implementing the MemoryStore protocol."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._data: dict[str, MemoryEntry] = {}
|
|
16
|
+
|
|
17
|
+
async def get(self, key: str) -> MemoryEntry | None:
|
|
18
|
+
return self._data.get(key)
|
|
19
|
+
|
|
20
|
+
async def set(self, key: str, value: str, metadata: dict[str, Any] | None = None) -> None:
|
|
21
|
+
existing = self._data.get(key)
|
|
22
|
+
self._data[key] = MemoryEntry(
|
|
23
|
+
key=key,
|
|
24
|
+
value=value,
|
|
25
|
+
metadata=dict(metadata) if metadata else None,
|
|
26
|
+
created_at=existing.created_at if existing else datetime.now(UTC),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
async def list(self) -> list[MemoryEntry]:
|
|
30
|
+
return list(self._data.values())
|
|
31
|
+
|
|
32
|
+
async def delete(self, key: str) -> None:
|
|
33
|
+
self._data.pop(key, None)
|
|
34
|
+
|
|
35
|
+
async def clear(self) -> None:
|
|
36
|
+
self._data.clear()
|
|
37
|
+
|
|
38
|
+
async def search(self, query: str) -> list[MemoryEntry]:
|
|
39
|
+
if not query:
|
|
40
|
+
return await self.list()
|
|
41
|
+
lower = query.lower()
|
|
42
|
+
return [e for e in self._data.values() if lower in e.key.lower() or lower in e.value.lower()]
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def size(self) -> int:
|
|
46
|
+
return len(self._data)
|
|
47
|
+
|
|
48
|
+
def has(self, key: str) -> bool:
|
|
49
|
+
return key in self._data
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Agent-to-agent message bus with pub/sub and read-state tracking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from anycode.types import Message
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MessageBus:
|
|
13
|
+
"""Pub/sub messaging between agents."""
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
self._messages: list[Message] = []
|
|
17
|
+
self._read_state: dict[str, set[str]] = {}
|
|
18
|
+
self._subscribers: dict[str, dict[int, Callable[[Message], None]]] = {}
|
|
19
|
+
self._next_id = 0
|
|
20
|
+
|
|
21
|
+
def send(self, from_agent: str, to_agent: str, content: str) -> Message:
|
|
22
|
+
msg = Message(id=str(uuid4()), from_agent=from_agent, to_agent=to_agent, content=content, timestamp=datetime.now(UTC))
|
|
23
|
+
self._dispatch(msg)
|
|
24
|
+
return msg
|
|
25
|
+
|
|
26
|
+
def broadcast(self, from_agent: str, content: str) -> Message:
|
|
27
|
+
return self.send(from_agent, "*", content)
|
|
28
|
+
|
|
29
|
+
def get_unread(self, agent_name: str) -> list[Message]:
|
|
30
|
+
read = self._read_state.get(agent_name, set())
|
|
31
|
+
return [m for m in self._messages if _is_recipient(m, agent_name) and m.id not in read]
|
|
32
|
+
|
|
33
|
+
def get_all(self, agent_name: str) -> list[Message]:
|
|
34
|
+
return [m for m in self._messages if _is_recipient(m, agent_name)]
|
|
35
|
+
|
|
36
|
+
def mark_read(self, agent_name: str, message_ids: list[str]) -> None:
|
|
37
|
+
if not message_ids:
|
|
38
|
+
return
|
|
39
|
+
read = self._read_state.setdefault(agent_name, set())
|
|
40
|
+
read.update(message_ids)
|
|
41
|
+
|
|
42
|
+
def get_conversation(self, agent1: str, agent2: str) -> list[Message]:
|
|
43
|
+
return [
|
|
44
|
+
m for m in self._messages
|
|
45
|
+
if (m.from_agent == agent1 and m.to_agent == agent2) or (m.from_agent == agent2 and m.to_agent == agent1)
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
def subscribe(self, agent_name: str, callback: Callable[[Message], None]) -> Callable[[], None]:
|
|
49
|
+
subs = self._subscribers.setdefault(agent_name, {})
|
|
50
|
+
sub_id = self._next_id
|
|
51
|
+
self._next_id += 1
|
|
52
|
+
subs[sub_id] = callback
|
|
53
|
+
return lambda: subs.pop(sub_id, None)
|
|
54
|
+
|
|
55
|
+
def _dispatch(self, message: Message) -> None:
|
|
56
|
+
self._messages.append(message)
|
|
57
|
+
if message.to_agent == "*":
|
|
58
|
+
for agent_name, subs in self._subscribers.items():
|
|
59
|
+
if agent_name != message.from_agent:
|
|
60
|
+
for cb in subs.values():
|
|
61
|
+
cb(message)
|
|
62
|
+
else:
|
|
63
|
+
for cb in self._subscribers.get(message.to_agent, {}).values():
|
|
64
|
+
cb(message)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _is_recipient(message: Message, agent_name: str) -> bool:
|
|
68
|
+
if message.to_agent == "*":
|
|
69
|
+
return message.from_agent != agent_name
|
|
70
|
+
return message.to_agent == agent_name
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Namespaced shared memory for coordinated agent teams."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from anycode.collaboration.kv_store import InMemoryStore
|
|
8
|
+
from anycode.types import MemoryEntry, MemoryStore
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SharedMemory:
|
|
12
|
+
"""Keys are scoped as <agent_name>/<key> to avoid collisions."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._store = InMemoryStore()
|
|
16
|
+
|
|
17
|
+
async def write(self, agent_name: str, key: str, value: str, metadata: dict[str, Any] | None = None) -> None:
|
|
18
|
+
namespaced = f"{agent_name}/{key}"
|
|
19
|
+
await self._store.set(namespaced, value, {**(metadata or {}), "agent": agent_name})
|
|
20
|
+
|
|
21
|
+
async def read(self, key: str) -> MemoryEntry | None:
|
|
22
|
+
return await self._store.get(key)
|
|
23
|
+
|
|
24
|
+
async def list_all(self) -> list[MemoryEntry]:
|
|
25
|
+
return await self._store.list()
|
|
26
|
+
|
|
27
|
+
async def list_by_agent(self, agent_name: str) -> list[MemoryEntry]:
|
|
28
|
+
prefix = f"{agent_name}/"
|
|
29
|
+
return [e for e in await self._store.list() if e.key.startswith(prefix)]
|
|
30
|
+
|
|
31
|
+
async def get_summary(self) -> str:
|
|
32
|
+
"""Build a markdown summary grouped by agent."""
|
|
33
|
+
all_entries = await self._store.list()
|
|
34
|
+
if not all_entries:
|
|
35
|
+
return ""
|
|
36
|
+
|
|
37
|
+
by_agent: dict[str, list[tuple[str, str]]] = {}
|
|
38
|
+
for entry in all_entries:
|
|
39
|
+
slash_idx = entry.key.find("/")
|
|
40
|
+
agent = entry.key[:slash_idx] if slash_idx != -1 else "_unknown"
|
|
41
|
+
local_key = entry.key[slash_idx + 1:] if slash_idx != -1 else entry.key
|
|
42
|
+
by_agent.setdefault(agent, []).append((local_key, entry.value))
|
|
43
|
+
|
|
44
|
+
lines = ["## Collective Agent Knowledge", ""]
|
|
45
|
+
for agent, entries in by_agent.items():
|
|
46
|
+
lines.append(f"### {agent}")
|
|
47
|
+
for local_key, value in entries:
|
|
48
|
+
display = f"{value[:177]}\u2026" if len(value) > 180 else value
|
|
49
|
+
lines.append(f"- {local_key}: {display}")
|
|
50
|
+
lines.append("")
|
|
51
|
+
|
|
52
|
+
return "\n".join(lines).rstrip()
|
|
53
|
+
|
|
54
|
+
def get_store(self) -> MemoryStore:
|
|
55
|
+
return self._store # type: ignore[return-value]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Manages a named group of agents with messaging, task queue, and shared memory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from anycode.collaboration.message_bus import MessageBus
|
|
9
|
+
from anycode.collaboration.shared_mem import SharedMemory
|
|
10
|
+
from anycode.tasks.queue import TaskQueue
|
|
11
|
+
from anycode.tasks.task import create_task
|
|
12
|
+
from anycode.types import AgentConfig, MemoryStore, Message, OrchestratorEvent, Task, TaskStatus, TeamConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _EventBus:
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
self._listeners: dict[str, dict[int, Callable[[Any], None]]] = {}
|
|
18
|
+
self._next_id = 0
|
|
19
|
+
|
|
20
|
+
def on(self, event: str, handler: Callable[[Any], None]) -> Callable[[], None]:
|
|
21
|
+
subs = self._listeners.setdefault(event, {})
|
|
22
|
+
sub_id = self._next_id
|
|
23
|
+
self._next_id += 1
|
|
24
|
+
subs[sub_id] = handler
|
|
25
|
+
return lambda: subs.pop(sub_id, None)
|
|
26
|
+
|
|
27
|
+
def emit(self, event: str, data: Any) -> None:
|
|
28
|
+
for handler in self._listeners.get(event, {}).values():
|
|
29
|
+
handler(data)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Team:
|
|
33
|
+
"""Agent team with messaging, task queue, and shared memory."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, config: TeamConfig) -> None:
|
|
36
|
+
self.name = config.name
|
|
37
|
+
self.config = config
|
|
38
|
+
self._agent_map: dict[str, AgentConfig] = {a.name: a for a in config.agents}
|
|
39
|
+
self._bus = MessageBus()
|
|
40
|
+
self._queue = TaskQueue()
|
|
41
|
+
self._memory = SharedMemory() if config.shared_memory else None
|
|
42
|
+
self._events = _EventBus()
|
|
43
|
+
|
|
44
|
+
# Relay queue events
|
|
45
|
+
self._queue.on("task:ready", lambda t: self._events.emit("task:ready", OrchestratorEvent(type="task_start", task=t.id, data=t)))
|
|
46
|
+
self._queue.on("task:complete", lambda t: self._events.emit("task:complete", OrchestratorEvent(type="task_complete", task=t.id, data=t)))
|
|
47
|
+
self._queue.on("task:failed", lambda t: self._events.emit("task:failed", OrchestratorEvent(type="error", task=t.id, data=t)))
|
|
48
|
+
self._queue.on("all:complete", lambda: self._events.emit("all:complete", None))
|
|
49
|
+
|
|
50
|
+
def get_agents(self) -> list[AgentConfig]:
|
|
51
|
+
return list(self._agent_map.values())
|
|
52
|
+
|
|
53
|
+
def get_agent(self, name: str) -> AgentConfig | None:
|
|
54
|
+
return self._agent_map.get(name)
|
|
55
|
+
|
|
56
|
+
def send_message(self, from_agent: str, to_agent: str, content: str) -> None:
|
|
57
|
+
msg = self._bus.send(from_agent, to_agent, content)
|
|
58
|
+
self._events.emit("message", OrchestratorEvent(type="message", agent=from_agent, data=msg))
|
|
59
|
+
|
|
60
|
+
def get_messages(self, agent_name: str) -> list[Message]:
|
|
61
|
+
return self._bus.get_all(agent_name)
|
|
62
|
+
|
|
63
|
+
def broadcast(self, from_agent: str, content: str) -> None:
|
|
64
|
+
msg = self._bus.broadcast(from_agent, content)
|
|
65
|
+
self._events.emit("broadcast", OrchestratorEvent(type="message", agent=from_agent, data=msg))
|
|
66
|
+
|
|
67
|
+
def add_task(self, *, title: str, description: str, status: TaskStatus = "pending",
|
|
68
|
+
assignee: str | None = None, depends_on: list[str] | None = None, result: str | None = None) -> Task:
|
|
69
|
+
created = create_task(title=title, description=description, assignee=assignee, depends_on=depends_on)
|
|
70
|
+
final = created.model_copy(update={"status": status, "result": result}) if status != "pending" else created
|
|
71
|
+
self._queue.add(final)
|
|
72
|
+
return final
|
|
73
|
+
|
|
74
|
+
def get_tasks(self) -> list[Task]:
|
|
75
|
+
return self._queue.list()
|
|
76
|
+
|
|
77
|
+
def get_tasks_by_assignee(self, agent_name: str) -> list[Task]:
|
|
78
|
+
return [t for t in self._queue.list() if t.assignee == agent_name]
|
|
79
|
+
|
|
80
|
+
def update_task(self, task_id: str, **kwargs: Any) -> Task:
|
|
81
|
+
return self._queue.update(task_id, **kwargs)
|
|
82
|
+
|
|
83
|
+
def get_next_task(self, agent_name: str) -> Task | None:
|
|
84
|
+
return self._queue.next(agent_name) or self._queue.next_available()
|
|
85
|
+
|
|
86
|
+
def get_shared_memory(self) -> MemoryStore | None:
|
|
87
|
+
return self._memory.get_store() if self._memory else None
|
|
88
|
+
|
|
89
|
+
def get_shared_memory_instance(self) -> SharedMemory | None:
|
|
90
|
+
return self._memory
|
|
91
|
+
|
|
92
|
+
def on(self, event: str, handler: Callable[[Any], None]) -> Callable[[], None]:
|
|
93
|
+
return self._events.on(event, handler)
|
|
94
|
+
|
|
95
|
+
def emit(self, event: str, data: Any) -> None:
|
|
96
|
+
self._events.emit(event, data)
|
anycode/core/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from anycode.core.agent import Agent
|
|
2
|
+
from anycode.core.orchestrator import AnyCode, TaskSpec
|
|
3
|
+
from anycode.core.pool import AgentPool
|
|
4
|
+
from anycode.core.runner import AgentRunner
|
|
5
|
+
from anycode.core.scheduler import Scheduler
|
|
6
|
+
|
|
7
|
+
__all__ = ["Agent", "AgentRunner", "AgentPool", "AnyCode", "TaskSpec", "Scheduler"]
|
anycode/core/agent.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Wraps AgentRunner with persistent conversation history, lifecycle state, and streaming."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
|
|
7
|
+
from anycode.core.runner import AgentRunner
|
|
8
|
+
from anycode.helpers.usage_tracker import EMPTY_USAGE, merge_usage
|
|
9
|
+
from anycode.providers.adapter import create_adapter
|
|
10
|
+
from anycode.tools.executor import ToolExecutor
|
|
11
|
+
from anycode.tools.registry import ToolRegistry
|
|
12
|
+
from anycode.types import (
|
|
13
|
+
AgentConfig,
|
|
14
|
+
AgentInfo,
|
|
15
|
+
AgentRunResult,
|
|
16
|
+
AgentState,
|
|
17
|
+
LLMMessage,
|
|
18
|
+
RunnerOptions,
|
|
19
|
+
RunResult,
|
|
20
|
+
StreamEvent,
|
|
21
|
+
TextBlock,
|
|
22
|
+
ToolDefinition,
|
|
23
|
+
ToolUseContext,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Agent:
|
|
28
|
+
"""High-level agent with state management, conversation history, and streaming support."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, config: AgentConfig | dict[str, object], tool_registry: ToolRegistry, tool_executor: ToolExecutor) -> None:
|
|
31
|
+
typed_config = AgentConfig.model_validate(config) if isinstance(config, dict) else config
|
|
32
|
+
self.name = typed_config.name
|
|
33
|
+
self.config = typed_config
|
|
34
|
+
self._registry = tool_registry
|
|
35
|
+
self._executor = tool_executor
|
|
36
|
+
self._runner: AgentRunner | None = None
|
|
37
|
+
self._state = AgentState()
|
|
38
|
+
self._history: list[LLMMessage] = []
|
|
39
|
+
|
|
40
|
+
async def _get_runner(self) -> AgentRunner:
|
|
41
|
+
if self._runner is not None:
|
|
42
|
+
return self._runner
|
|
43
|
+
|
|
44
|
+
provider = self.config.provider or "anthropic"
|
|
45
|
+
adapter = await create_adapter(provider)
|
|
46
|
+
|
|
47
|
+
opts = RunnerOptions(
|
|
48
|
+
model=self.config.model,
|
|
49
|
+
system_prompt=self.config.system_prompt,
|
|
50
|
+
max_turns=self.config.max_turns,
|
|
51
|
+
max_tokens=self.config.max_tokens,
|
|
52
|
+
temperature=self.config.temperature,
|
|
53
|
+
allowed_tools=self.config.tools,
|
|
54
|
+
agent_name=self.name,
|
|
55
|
+
agent_role=(self.config.system_prompt or "assistant")[:50],
|
|
56
|
+
)
|
|
57
|
+
self._runner = AgentRunner(adapter, self._registry, self._executor, opts)
|
|
58
|
+
return self._runner
|
|
59
|
+
|
|
60
|
+
async def run(self, prompt: str) -> AgentRunResult:
|
|
61
|
+
"""Execute prompt as a standalone conversation (history is ignored)."""
|
|
62
|
+
messages = [LLMMessage(role="user", content=[TextBlock(text=prompt)])]
|
|
63
|
+
return await self._execute_run(messages)
|
|
64
|
+
|
|
65
|
+
async def prompt(self, message: str) -> AgentRunResult:
|
|
66
|
+
"""Continue the ongoing conversation with a new user message."""
|
|
67
|
+
user_msg = LLMMessage(role="user", content=[TextBlock(text=message)])
|
|
68
|
+
self._history.append(user_msg)
|
|
69
|
+
result = await self._execute_run(list(self._history))
|
|
70
|
+
self._history.extend(result.messages)
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
async def stream(self, prompt: str) -> AsyncGenerator[StreamEvent, None]:
|
|
74
|
+
"""Stream a standalone conversation response as incremental events."""
|
|
75
|
+
messages = [LLMMessage(role="user", content=[TextBlock(text=prompt)])]
|
|
76
|
+
async for event in self._execute_stream(messages):
|
|
77
|
+
yield event
|
|
78
|
+
|
|
79
|
+
def get_state(self) -> AgentState:
|
|
80
|
+
return self._state.model_copy(deep=True)
|
|
81
|
+
|
|
82
|
+
def get_history(self) -> list[LLMMessage]:
|
|
83
|
+
return list(self._history)
|
|
84
|
+
|
|
85
|
+
def reset(self) -> None:
|
|
86
|
+
self._history.clear()
|
|
87
|
+
self._state = AgentState()
|
|
88
|
+
|
|
89
|
+
def add_tool(self, tool: ToolDefinition) -> None:
|
|
90
|
+
self._registry.register(tool)
|
|
91
|
+
|
|
92
|
+
def remove_tool(self, name: str) -> None:
|
|
93
|
+
self._registry.deregister(name)
|
|
94
|
+
|
|
95
|
+
def get_tools(self) -> list[str]:
|
|
96
|
+
return [t.name for t in self._registry.list()]
|
|
97
|
+
|
|
98
|
+
async def _execute_run(self, messages: list[LLMMessage]) -> AgentRunResult:
|
|
99
|
+
self._state.status = "running"
|
|
100
|
+
try:
|
|
101
|
+
runner = await self._get_runner()
|
|
102
|
+
result = await runner.run(messages, on_message=lambda msg: self._state.messages.append(msg))
|
|
103
|
+
self._state.token_usage = merge_usage(self._state.token_usage, result.token_usage)
|
|
104
|
+
self._state.status = "completed"
|
|
105
|
+
return AgentRunResult(
|
|
106
|
+
success=True,
|
|
107
|
+
output=result.output,
|
|
108
|
+
messages=result.messages,
|
|
109
|
+
token_usage=result.token_usage,
|
|
110
|
+
tool_calls=result.tool_calls,
|
|
111
|
+
)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
self._state.status = "error"
|
|
114
|
+
self._state.error = str(e)
|
|
115
|
+
return AgentRunResult(success=False, output=str(e), messages=[], token_usage=EMPTY_USAGE, tool_calls=[])
|
|
116
|
+
|
|
117
|
+
async def _execute_stream(self, messages: list[LLMMessage]) -> AsyncGenerator[StreamEvent, None]:
|
|
118
|
+
self._state.status = "running"
|
|
119
|
+
try:
|
|
120
|
+
runner = await self._get_runner()
|
|
121
|
+
async for event in runner.stream(messages):
|
|
122
|
+
if event.type == "done" and isinstance(event.data, RunResult):
|
|
123
|
+
self._state.token_usage = merge_usage(self._state.token_usage, event.data.token_usage)
|
|
124
|
+
self._state.status = "completed"
|
|
125
|
+
elif event.type == "error":
|
|
126
|
+
self._state.status = "error"
|
|
127
|
+
self._state.error = str(event.data)
|
|
128
|
+
yield event
|
|
129
|
+
except Exception as e:
|
|
130
|
+
self._state.status = "error"
|
|
131
|
+
self._state.error = str(e)
|
|
132
|
+
yield StreamEvent(type="error", data=e)
|
|
133
|
+
|
|
134
|
+
def build_tool_context(self) -> ToolUseContext:
|
|
135
|
+
return ToolUseContext(
|
|
136
|
+
agent=AgentInfo(
|
|
137
|
+
name=self.name,
|
|
138
|
+
role=(self.config.system_prompt or "assistant")[:60],
|
|
139
|
+
model=self.config.model,
|
|
140
|
+
)
|
|
141
|
+
)
|