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.
Files changed (98) hide show
  1. agent86/__init__.py +14 -0
  2. agent86/__main__.py +6 -0
  3. agent86/agents/__init__.py +17 -0
  4. agent86/agents/broker.py +45 -0
  5. agent86/agents/envelope.py +43 -0
  6. agent86/agents/orchestrator.py +48 -0
  7. agent86/agents/subagent.py +169 -0
  8. agent86/cli.py +993 -0
  9. agent86/cognitive/__init__.py +17 -0
  10. agent86/cognitive/anthropic_provider.py +360 -0
  11. agent86/cognitive/base.py +217 -0
  12. agent86/cognitive/capabilities.py +212 -0
  13. agent86/cognitive/catalog.py +118 -0
  14. agent86/cognitive/http_timeouts.py +66 -0
  15. agent86/cognitive/llamacpp_provider.py +31 -0
  16. agent86/cognitive/ollama_provider.py +252 -0
  17. agent86/cognitive/openai_provider.py +399 -0
  18. agent86/cognitive/pricing.py +314 -0
  19. agent86/cognitive/prompt.py +75 -0
  20. agent86/cognitive/retry.py +207 -0
  21. agent86/config.py +577 -0
  22. agent86/config_writer.py +238 -0
  23. agent86/gateway/__init__.py +11 -0
  24. agent86/guardrails/__init__.py +15 -0
  25. agent86/guardrails/egress.py +34 -0
  26. agent86/guardrails/ingress.py +45 -0
  27. agent86/guardrails/policy.py +154 -0
  28. agent86/guardrails/scanners.py +123 -0
  29. agent86/memory/__init__.py +18 -0
  30. agent86/memory/embeddings.py +156 -0
  31. agent86/memory/episodic.py +92 -0
  32. agent86/memory/semantic.py +25 -0
  33. agent86/memory/store.py +431 -0
  34. agent86/memory/system.py +46 -0
  35. agent86/memory/working.py +257 -0
  36. agent86/observability/__init__.py +10 -0
  37. agent86/observability/recorder.py +267 -0
  38. agent86/observability/redact.py +138 -0
  39. agent86/observability/tracing.py +217 -0
  40. agent86/orchestration/__init__.py +13 -0
  41. agent86/orchestration/circuit.py +71 -0
  42. agent86/orchestration/loop.py +1248 -0
  43. agent86/orchestration/router.py +84 -0
  44. agent86/orchestration/state.py +75 -0
  45. agent86/secrets.py +194 -0
  46. agent86/skills/__init__.py +11 -0
  47. agent86/skills/loader.py +313 -0
  48. agent86/skills/models.py +49 -0
  49. agent86/tools/__init__.py +14 -0
  50. agent86/tools/base.py +168 -0
  51. agent86/tools/builtin/__init__.py +5 -0
  52. agent86/tools/builtin/delegate.py +39 -0
  53. agent86/tools/builtin/files.py +344 -0
  54. agent86/tools/builtin/memory.py +57 -0
  55. agent86/tools/builtin/python_exec.py +57 -0
  56. agent86/tools/builtin/shell.py +58 -0
  57. agent86/tools/builtin/skills_tool.py +59 -0
  58. agent86/tools/builtin/web.py +316 -0
  59. agent86/tools/mcp_client.py +463 -0
  60. agent86/tools/registry.py +180 -0
  61. agent86/tools/sandbox/__init__.py +7 -0
  62. agent86/tools/sandbox/docker_exec.py +137 -0
  63. agent86/tools/sandbox/executor.py +95 -0
  64. agent86/tools/sandbox/policy.py +203 -0
  65. agent86/tools/sandbox/subprocess_exec.py +146 -0
  66. agent86/tui/__init__.py +1 -0
  67. agent86/tui/app.py +1477 -0
  68. agent86/tui/commands.py +577 -0
  69. agent86/tui/mentions.py +270 -0
  70. agent86/tui/messages.py +136 -0
  71. agent86/tui/screens/__init__.py +1 -0
  72. agent86/tui/screens/_shutdown.py +48 -0
  73. agent86/tui/screens/approval.py +91 -0
  74. agent86/tui/screens/connection_test.py +167 -0
  75. agent86/tui/screens/key_entry.py +75 -0
  76. agent86/tui/screens/mcp_manager.py +473 -0
  77. agent86/tui/screens/mcp_test.py +186 -0
  78. agent86/tui/screens/mode_picker.py +41 -0
  79. agent86/tui/screens/model_picker.py +127 -0
  80. agent86/tui/screens/provider_manager.py +206 -0
  81. agent86/tui/screens/save_diff.py +110 -0
  82. agent86/tui/screens/session_picker.py +135 -0
  83. agent86/tui/turn_bridge.py +217 -0
  84. agent86/tui/widgets/__init__.py +1 -0
  85. agent86/tui/widgets/prompt_input.py +200 -0
  86. agent86/tui/widgets/status_footer.py +77 -0
  87. agent86/tui/widgets/tool_block.py +131 -0
  88. agent86/tui/widgets/transcript.py +208 -0
  89. agent86/types.py +296 -0
  90. agent86/ui/__init__.py +6 -0
  91. agent86/ui/history.py +208 -0
  92. agent86/ui/repl.py +464 -0
  93. agent86/ui/status.py +263 -0
  94. agent86-1.0.0.dist-info/METADATA +935 -0
  95. agent86-1.0.0.dist-info/RECORD +98 -0
  96. agent86-1.0.0.dist-info/WHEEL +4 -0
  97. agent86-1.0.0.dist-info/entry_points.txt +2 -0
  98. 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,6 @@
1
+ """Enable `python -m agent86`."""
2
+
3
+ from agent86.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -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
+ """
@@ -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"]