agent86 1.0.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.
- agent86/__init__.py +14 -0
- agent86/__main__.py +6 -0
- agent86/agents/__init__.py +17 -0
- agent86/agents/broker.py +45 -0
- agent86/agents/envelope.py +43 -0
- agent86/agents/orchestrator.py +48 -0
- agent86/agents/subagent.py +169 -0
- agent86/cli.py +993 -0
- agent86/cognitive/__init__.py +17 -0
- agent86/cognitive/anthropic_provider.py +360 -0
- agent86/cognitive/base.py +217 -0
- agent86/cognitive/capabilities.py +212 -0
- agent86/cognitive/catalog.py +118 -0
- agent86/cognitive/http_timeouts.py +66 -0
- agent86/cognitive/llamacpp_provider.py +31 -0
- agent86/cognitive/ollama_provider.py +252 -0
- agent86/cognitive/openai_provider.py +399 -0
- agent86/cognitive/pricing.py +314 -0
- agent86/cognitive/prompt.py +75 -0
- agent86/cognitive/retry.py +207 -0
- agent86/config.py +577 -0
- agent86/config_writer.py +238 -0
- agent86/gateway/__init__.py +11 -0
- agent86/guardrails/__init__.py +15 -0
- agent86/guardrails/egress.py +34 -0
- agent86/guardrails/ingress.py +45 -0
- agent86/guardrails/policy.py +154 -0
- agent86/guardrails/scanners.py +123 -0
- agent86/memory/__init__.py +18 -0
- agent86/memory/embeddings.py +156 -0
- agent86/memory/episodic.py +92 -0
- agent86/memory/semantic.py +25 -0
- agent86/memory/store.py +431 -0
- agent86/memory/system.py +46 -0
- agent86/memory/working.py +257 -0
- agent86/observability/__init__.py +10 -0
- agent86/observability/recorder.py +267 -0
- agent86/observability/redact.py +138 -0
- agent86/observability/tracing.py +217 -0
- agent86/orchestration/__init__.py +13 -0
- agent86/orchestration/circuit.py +71 -0
- agent86/orchestration/loop.py +1248 -0
- agent86/orchestration/router.py +84 -0
- agent86/orchestration/state.py +75 -0
- agent86/secrets.py +194 -0
- agent86/skills/__init__.py +11 -0
- agent86/skills/loader.py +313 -0
- agent86/skills/models.py +49 -0
- agent86/tools/__init__.py +14 -0
- agent86/tools/base.py +168 -0
- agent86/tools/builtin/__init__.py +5 -0
- agent86/tools/builtin/delegate.py +39 -0
- agent86/tools/builtin/files.py +344 -0
- agent86/tools/builtin/memory.py +57 -0
- agent86/tools/builtin/python_exec.py +57 -0
- agent86/tools/builtin/shell.py +58 -0
- agent86/tools/builtin/skills_tool.py +59 -0
- agent86/tools/builtin/web.py +316 -0
- agent86/tools/mcp_client.py +463 -0
- agent86/tools/registry.py +180 -0
- agent86/tools/sandbox/__init__.py +7 -0
- agent86/tools/sandbox/docker_exec.py +137 -0
- agent86/tools/sandbox/executor.py +95 -0
- agent86/tools/sandbox/policy.py +203 -0
- agent86/tools/sandbox/subprocess_exec.py +146 -0
- agent86/tui/__init__.py +1 -0
- agent86/tui/app.py +1477 -0
- agent86/tui/commands.py +577 -0
- agent86/tui/mentions.py +270 -0
- agent86/tui/messages.py +136 -0
- agent86/tui/screens/__init__.py +1 -0
- agent86/tui/screens/_shutdown.py +48 -0
- agent86/tui/screens/approval.py +91 -0
- agent86/tui/screens/connection_test.py +167 -0
- agent86/tui/screens/key_entry.py +75 -0
- agent86/tui/screens/mcp_manager.py +473 -0
- agent86/tui/screens/mcp_test.py +186 -0
- agent86/tui/screens/mode_picker.py +41 -0
- agent86/tui/screens/model_picker.py +127 -0
- agent86/tui/screens/provider_manager.py +206 -0
- agent86/tui/screens/save_diff.py +110 -0
- agent86/tui/screens/session_picker.py +135 -0
- agent86/tui/turn_bridge.py +217 -0
- agent86/tui/widgets/__init__.py +1 -0
- agent86/tui/widgets/prompt_input.py +200 -0
- agent86/tui/widgets/status_footer.py +77 -0
- agent86/tui/widgets/tool_block.py +131 -0
- agent86/tui/widgets/transcript.py +208 -0
- agent86/types.py +296 -0
- agent86/ui/__init__.py +6 -0
- agent86/ui/history.py +208 -0
- agent86/ui/repl.py +464 -0
- agent86/ui/status.py +263 -0
- agent86-1.0.0.dist-info/METADATA +935 -0
- agent86-1.0.0.dist-info/RECORD +98 -0
- agent86-1.0.0.dist-info/WHEEL +4 -0
- agent86-1.0.0.dist-info/entry_points.txt +2 -0
- agent86-1.0.0.dist-info/licenses/LICENSE +21 -0
agent86/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""agent86 — an agentic harness on the command line.
|
|
2
|
+
|
|
3
|
+
A Python CLI that connects to remote or local models and lets them use tools and
|
|
4
|
+
skills, built as a faithful implementation of the five-tier architecture and four
|
|
5
|
+
pillars from *The Agentic Harness* (Tony Fleming, 2026).
|
|
6
|
+
|
|
7
|
+
Core principle — Separation of Concerns: the Cognitive Core (the model) only ever
|
|
8
|
+
*proposes* the next step. The deterministic harness validates, executes, and persists.
|
|
9
|
+
The model never touches the sandbox, the database, or the terminal directly.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "1.0.0"
|
|
13
|
+
|
|
14
|
+
__all__ = ["__version__"]
|
agent86/__main__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Multi-agent (MAS) — collaboration & negotiation.
|
|
2
|
+
|
|
3
|
+
An ``Agent`` is a harness instance bound to a role, system prompt, and toolset.
|
|
4
|
+
Agents communicate via structured message *envelopes* (never raw natural language as a
|
|
5
|
+
wire protocol — the book's "Babel problem") over an in-process async broker. The
|
|
6
|
+
orchestrator spawns sub-agents, wires topologies (supervisor / pipeline / blackboard),
|
|
7
|
+
and applies harness-level conflict resolution rather than trusting pure LLM debate.
|
|
8
|
+
|
|
9
|
+
v0.1 ships single-agent-first with this scaffolding present; supervisor topology is the
|
|
10
|
+
first wired path (Phase 8).
|
|
11
|
+
|
|
12
|
+
Modules:
|
|
13
|
+
agent.py — Agent runtime wrapper
|
|
14
|
+
envelope.py — structured agent message envelope
|
|
15
|
+
broker.py — in-process async message broker
|
|
16
|
+
orchestrator.py — sub-agent spawning, topologies, conflict resolution
|
|
17
|
+
"""
|
agent86/agents/broker.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""In-process message broker (the harness as communication broker).
|
|
2
|
+
|
|
3
|
+
A simple synchronous pub/sub bus: agents post :class:`AgentMessage` envelopes addressed to a
|
|
4
|
+
recipient, and a recipient drains its inbox. This is the local-topology substrate the
|
|
5
|
+
supervisor orchestrator runs on; a networked broker (Redis/NATS) would slot in behind the
|
|
6
|
+
same interface for distributed MAS (a Phase-9+ concern).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections import defaultdict, deque
|
|
12
|
+
|
|
13
|
+
from agent86.agents.envelope import AgentMessage
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MessageBus:
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._inboxes: dict[str, deque[AgentMessage]] = defaultdict(deque)
|
|
19
|
+
self._log: list[AgentMessage] = []
|
|
20
|
+
|
|
21
|
+
def send(self, message: AgentMessage) -> None:
|
|
22
|
+
self._inboxes[message.recipient].append(message)
|
|
23
|
+
self._log.append(message)
|
|
24
|
+
|
|
25
|
+
def receive(self, recipient: str) -> AgentMessage | None:
|
|
26
|
+
inbox = self._inboxes.get(recipient)
|
|
27
|
+
return inbox.popleft() if inbox else None
|
|
28
|
+
|
|
29
|
+
def drain(self, recipient: str) -> list[AgentMessage]:
|
|
30
|
+
inbox = self._inboxes.get(recipient)
|
|
31
|
+
if not inbox:
|
|
32
|
+
return []
|
|
33
|
+
out = list(inbox)
|
|
34
|
+
inbox.clear()
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
def pending(self, recipient: str) -> int:
|
|
38
|
+
return len(self._inboxes.get(recipient, ()))
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def history(self) -> list[AgentMessage]:
|
|
42
|
+
return list(self._log)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = ["MessageBus"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""The agent message envelope.
|
|
2
|
+
|
|
3
|
+
The book's answer to the "Babel problem": agents never exchange raw natural language as a
|
|
4
|
+
wire protocol. Every inter-agent message is a structured envelope with an explicit sender,
|
|
5
|
+
recipient, intent, and correlation id, so the harness can route, match, and audit it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import StrEnum
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Intent(StrEnum):
|
|
16
|
+
REQUEST = "request" # please perform this task
|
|
17
|
+
RESPONSE = "response" # here is the result
|
|
18
|
+
INFORM = "inform" # fyi, no reply expected
|
|
19
|
+
ERROR = "error" # the task failed
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class AgentMessage:
|
|
24
|
+
sender: str
|
|
25
|
+
recipient: str
|
|
26
|
+
intent: Intent
|
|
27
|
+
content: str
|
|
28
|
+
correlation_id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
|
|
29
|
+
metadata: dict = field(default_factory=dict)
|
|
30
|
+
id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
|
|
31
|
+
|
|
32
|
+
def reply(self, content: str, intent: Intent = Intent.RESPONSE) -> AgentMessage:
|
|
33
|
+
"""Build a correlated reply from recipient back to sender."""
|
|
34
|
+
return AgentMessage(
|
|
35
|
+
sender=self.recipient,
|
|
36
|
+
recipient=self.sender,
|
|
37
|
+
intent=intent,
|
|
38
|
+
content=content,
|
|
39
|
+
correlation_id=self.correlation_id,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ["AgentMessage", "Intent"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Supervisor orchestrator (multi-agent topology).
|
|
2
|
+
|
|
3
|
+
The programmatic counterpart to the model-driven ``delegate`` tool: fan a set of role-scoped
|
|
4
|
+
tasks out to sub-agents and collect their results as correlated :class:`AgentMessage`
|
|
5
|
+
envelopes on the bus. Sub-agents run sequentially in-process (the sync harness); the envelope
|
|
6
|
+
protocol keeps the topology explicit and auditable.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from agent86.agents.broker import MessageBus
|
|
14
|
+
from agent86.agents.envelope import AgentMessage, Intent
|
|
15
|
+
from agent86.agents.subagent import SubAgent
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from agent86.orchestration.loop import Harness
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SupervisorOrchestrator:
|
|
22
|
+
def __init__(self, harness: Harness, bus: MessageBus | None = None):
|
|
23
|
+
self.h = harness
|
|
24
|
+
self.bus = bus or MessageBus()
|
|
25
|
+
|
|
26
|
+
def fan_out(
|
|
27
|
+
self, tasks: list[tuple[str, str]], supervisor: str = "supervisor"
|
|
28
|
+
) -> list[AgentMessage]:
|
|
29
|
+
"""Run each (role, task) on a sub-agent; return their reply envelopes in order."""
|
|
30
|
+
replies: list[AgentMessage] = []
|
|
31
|
+
for role, task in tasks:
|
|
32
|
+
request = AgentMessage(
|
|
33
|
+
sender=supervisor, recipient=role, intent=Intent.REQUEST, content=task
|
|
34
|
+
)
|
|
35
|
+
self.bus.send(request)
|
|
36
|
+
try:
|
|
37
|
+
result, usage = SubAgent(self.h, role, depth=1).run(task)
|
|
38
|
+
reply = request.reply(result, intent=Intent.RESPONSE)
|
|
39
|
+
# What the fan-out cost, per branch, is part of the audit trail.
|
|
40
|
+
reply.metadata["usage"] = usage.model_dump()
|
|
41
|
+
except Exception as exc: # a failing sub-agent must not sink the whole fan-out
|
|
42
|
+
reply = request.reply(f"{type(exc).__name__}: {exc}", intent=Intent.ERROR)
|
|
43
|
+
self.bus.send(reply)
|
|
44
|
+
replies.append(reply)
|
|
45
|
+
return replies
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = ["SupervisorOrchestrator"]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Sub-agent runtime (multi-agent systems).
|
|
2
|
+
|
|
3
|
+
A ``SubAgent`` is a role-scoped Reason->Act->Observe loop that *reuses* the parent harness's
|
|
4
|
+
provider, tool registry, sandbox, and approval gate — so a delegated task gets the same
|
|
5
|
+
guardrails and observability as the main agent, without rebuilding memory/MCP/tracing. Its
|
|
6
|
+
depth is tracked so delegation can't recurse without bound.
|
|
7
|
+
|
|
8
|
+
Accountability is the other half of that reuse: a sub-agent runs under the parent's step and
|
|
9
|
+
context budgets, reports every model call to the same recorder tagged with its role and depth,
|
|
10
|
+
and hands its token usage back so the parent turn's cost includes what it spent. A delegated
|
|
11
|
+
task is not a way to escape the harness's limits.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import replace
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
from agent86.orchestration.circuit import CircuitBreaker, CircuitTripped
|
|
20
|
+
from agent86.tools.base import ToolContext
|
|
21
|
+
from agent86.types import (
|
|
22
|
+
CompletionRequest,
|
|
23
|
+
Message,
|
|
24
|
+
Role,
|
|
25
|
+
ToolCall,
|
|
26
|
+
ToolResult,
|
|
27
|
+
Usage,
|
|
28
|
+
invalid_arguments_result,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from agent86.orchestration.loop import Harness
|
|
33
|
+
|
|
34
|
+
#: Used when the config predates ``agents.max_steps``.
|
|
35
|
+
DEFAULT_SUB_MAX_STEPS = 8
|
|
36
|
+
|
|
37
|
+
_ROLE_PREAMBLE = (
|
|
38
|
+
"You are a '{role}' sub-agent operating inside a larger agent system. You were delegated "
|
|
39
|
+
"a focused task by a supervising agent. Use tools as needed, complete the task precisely, "
|
|
40
|
+
"and return ONLY the final result — no preamble."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SubAgent:
|
|
45
|
+
def __init__(self, harness: Harness, role: str, depth: int):
|
|
46
|
+
self.h = harness
|
|
47
|
+
self.role = role or "assistant"
|
|
48
|
+
self.depth = depth
|
|
49
|
+
# A context whose spawn() delegates one level deeper, so nested delegation is depth-aware.
|
|
50
|
+
self.ctx: ToolContext = replace(
|
|
51
|
+
harness.context,
|
|
52
|
+
spawn=lambda r, t: harness.spawn_subagent(r, t, depth + 1),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def max_steps(self) -> int:
|
|
57
|
+
"""Step budget for this sub-agent, from ``agents.max_steps``."""
|
|
58
|
+
return getattr(self.h.config.agents, "max_steps", DEFAULT_SUB_MAX_STEPS)
|
|
59
|
+
|
|
60
|
+
def _system(self) -> Message:
|
|
61
|
+
"""Role preamble + the parent's compiled system prompt.
|
|
62
|
+
|
|
63
|
+
Sub-agents used to get the persona and nothing else: no environment facts and — the
|
|
64
|
+
real problem — no skills list, so ``use_skill`` was advertised to them as a tool with
|
|
65
|
+
no way of knowing what to ask it for. ``build_system_prompt``'s output, which the
|
|
66
|
+
harness compiled once at startup, is reused verbatim beneath the preamble.
|
|
67
|
+
"""
|
|
68
|
+
return Message(
|
|
69
|
+
role=Role.SYSTEM,
|
|
70
|
+
content=f"{_ROLE_PREAMBLE.format(role=self.role)}\n\n{self.h.system_prompt.content}",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def _specs(self):
|
|
74
|
+
specs = self.h.registry.specs()
|
|
75
|
+
# At max depth, hide `delegate` so this sub-agent cannot spawn further.
|
|
76
|
+
if self.depth >= self.h.config.agents.max_depth:
|
|
77
|
+
specs = [s for s in specs if s.name != "delegate"]
|
|
78
|
+
return specs
|
|
79
|
+
|
|
80
|
+
def run(self, task: str) -> tuple[str, Usage]:
|
|
81
|
+
"""Run the delegated task to completion; return its answer and what it cost."""
|
|
82
|
+
provider = self.h.provider
|
|
83
|
+
sid = f"sub:{self.role}"
|
|
84
|
+
system = self._system()
|
|
85
|
+
messages: list[Message] = [Message(role=Role.USER, content=task)]
|
|
86
|
+
breaker = CircuitBreaker(self.h.config.limits, max_steps=self.max_steps)
|
|
87
|
+
spent = Usage()
|
|
88
|
+
|
|
89
|
+
while True:
|
|
90
|
+
try:
|
|
91
|
+
breaker.before_step()
|
|
92
|
+
except CircuitTripped as exc:
|
|
93
|
+
return f"[sub-agent '{self.role}' halted: {exc}]", spent
|
|
94
|
+
|
|
95
|
+
# Same context-window discipline as the main loop: a sub-agent that ran many
|
|
96
|
+
# tool calls could otherwise overflow the window and fail its last model call.
|
|
97
|
+
# The system message is held out of the trim so it can never be the part dropped.
|
|
98
|
+
convo = self.h.working.fit(messages, provider.count_tokens)
|
|
99
|
+
completion = provider.complete(
|
|
100
|
+
CompletionRequest(
|
|
101
|
+
model=provider.model,
|
|
102
|
+
messages=[system, *convo],
|
|
103
|
+
tools=self._specs(),
|
|
104
|
+
temperature=0.0,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
breaker.record_step(completion.usage)
|
|
108
|
+
spent = spent + completion.usage
|
|
109
|
+
self.h.recorder.event(
|
|
110
|
+
sid,
|
|
111
|
+
"model_call",
|
|
112
|
+
agent=self.role,
|
|
113
|
+
depth=self.depth,
|
|
114
|
+
step=breaker.steps,
|
|
115
|
+
model=provider.model,
|
|
116
|
+
input_tokens=completion.usage.input_tokens,
|
|
117
|
+
output_tokens=completion.usage.output_tokens,
|
|
118
|
+
cost_usd=completion.usage.cost_usd,
|
|
119
|
+
stop_reason=completion.stop_reason,
|
|
120
|
+
tool_calls=[tc.name for tc in completion.tool_calls],
|
|
121
|
+
)
|
|
122
|
+
messages.append(
|
|
123
|
+
Message(
|
|
124
|
+
role=Role.ASSISTANT,
|
|
125
|
+
content=completion.text,
|
|
126
|
+
tool_calls=completion.tool_calls,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if not completion.tool_calls:
|
|
131
|
+
self.h.recorder.event(
|
|
132
|
+
sid, "subagent_done", role=self.role, depth=self.depth,
|
|
133
|
+
steps=breaker.steps, cost_usd=spent.cost_usd,
|
|
134
|
+
)
|
|
135
|
+
return completion.text, spent
|
|
136
|
+
|
|
137
|
+
for call in completion.tool_calls:
|
|
138
|
+
result = self._run_tool(call)
|
|
139
|
+
content = self.h._observe(result, call.name, sid)
|
|
140
|
+
messages.append(
|
|
141
|
+
Message(role=Role.TOOL, content=content, tool_call_id=call.id, name=call.name)
|
|
142
|
+
)
|
|
143
|
+
try:
|
|
144
|
+
breaker.record_tool_result(result.ok)
|
|
145
|
+
except CircuitTripped as exc:
|
|
146
|
+
return f"[sub-agent '{self.role}' halted: {exc}]", spent
|
|
147
|
+
|
|
148
|
+
def _run_tool(self, call: ToolCall) -> ToolResult:
|
|
149
|
+
# Same interception as the main loop: unparseable arguments never reach a tool.
|
|
150
|
+
invalid = call.invalid_arguments
|
|
151
|
+
if invalid is not None:
|
|
152
|
+
return invalid_arguments_result(call, invalid)
|
|
153
|
+
tool = self.h.registry.get(call.name)
|
|
154
|
+
if tool is None:
|
|
155
|
+
return self.h.registry.dispatch(call, self.ctx)
|
|
156
|
+
decision = self.h.gate.decide(tool, call)
|
|
157
|
+
if not decision.approved:
|
|
158
|
+
return ToolResult(
|
|
159
|
+
call_id=call.id, name=call.name, ok=False,
|
|
160
|
+
error=f"Not executed: {decision.reason}.",
|
|
161
|
+
)
|
|
162
|
+
result = self.h.registry.dispatch(call, self.ctx)
|
|
163
|
+
self.h.recorder.event(
|
|
164
|
+
f"sub:{self.role}", "tool_call", tool=call.name, ok=result.ok, depth=self.depth
|
|
165
|
+
)
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
__all__ = ["SubAgent", "DEFAULT_SUB_MAX_STEPS"]
|