dreadnode 2.0.30__py3-none-any.whl → 2.0.32__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.
- dreadnode/agents/agent.py +48 -0
- dreadnode/agents/engines/AGENTS.md +27 -0
- dreadnode/agents/engines/__init__.py +89 -0
- dreadnode/agents/engines/base.py +166 -0
- dreadnode/agents/engines/claude_code.py +528 -0
- dreadnode/agents/engines/native.py +26 -0
- dreadnode/agents/events.py +6 -0
- dreadnode/agents/mcp/client.py +76 -2
- dreadnode/agents/subagent.py +50 -10
- dreadnode/agents/tools.py +4 -0
- dreadnode/airt/assessment.py +11 -2
- dreadnode/airt/message_parts.py +263 -0
- dreadnode/airt/multimodal.py +191 -31
- dreadnode/app/api/client.py +509 -0
- dreadnode/app/api/models.py +62 -0
- dreadnode/app/cli/airt.py +20 -5
- dreadnode/app/cli/args.py +1 -0
- dreadnode/app/cli/capability.py +20 -0
- dreadnode/app/cli/environment.py +20 -0
- dreadnode/app/cli/evaluation.py +72 -0
- dreadnode/app/cli/main.py +41 -10
- dreadnode/app/cli/shared.py +57 -0
- dreadnode/app/cli/task.py +21 -0
- dreadnode/app/cli/task_set.py +791 -0
- dreadnode/app/client/managed_client.py +6 -0
- dreadnode/app/client/models.py +2 -0
- dreadnode/app/client/runtime_client.py +106 -2
- dreadnode/app/print_mode.py +96 -1
- dreadnode/app/server/app.py +413 -19
- dreadnode/app/server/prompt.py +57 -0
- dreadnode/app/server/session_persistence.py +29 -1
- dreadnode/app/server/utils.py +34 -0
- dreadnode/app/server/websocket.py +14 -3
- dreadnode/app/server/worker_manager.py +16 -2
- dreadnode/app/server/ws_auth.py +76 -0
- dreadnode/app/tui/app.py +43 -9
- dreadnode/app/tui/command_dispatcher.py +4 -1
- dreadnode/app/tui/dreadnode.tcss +11 -0
- dreadnode/app/tui/model_manager.py +84 -5
- dreadnode/app/tui/screens/capabilities.py +43 -0
- dreadnode/app/tui/screens/environments.py +31 -1
- dreadnode/app/tui/screens/models.py +5 -21
- dreadnode/app/tui/screens/readme.py +101 -0
- dreadnode/app/tui/sessions_manager.py +16 -14
- dreadnode/app/tui/turn_reducer.py +8 -0
- dreadnode/app/tui/update_check.py +54 -0
- dreadnode/app/tui/widgets/context_bar.py +9 -1
- dreadnode/app/tui/widgets/conversation.py +80 -30
- dreadnode/app/tui/widgets/tool.py +68 -6
- dreadnode/app/tui/wire_events.py +4 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/INDEX.md +2 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/capabilities/agents.md +10 -9
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/capabilities/custom-engines.md +130 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/capabilities/engines.md +109 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/capabilities/manifest.md +2 -2
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/sdk/agents.md +23 -6
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/sdk/capabilities.md +43 -0
- dreadnode/builtin_capabilities/dreadnode/skills/creating-capabilities/references/tui/default-tools.md +24 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/INDEX.md +1 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/capability.md +1 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/env.md +1 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/evaluation.md +3 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/task-set.md +223 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/cli/task.md +2 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-cli/references/getting-started/authentication.md +10 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-concepts/references/getting-started/authentication.md +10 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-concepts/references/platform/settings.md +1 -1
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-concepts/references/platform/users.md +3 -0
- dreadnode/builtin_capabilities/dreadnode/skills/dreadnode-concepts/references/runtimes/configuration.md +1 -1
- dreadnode/capabilities/loader.py +6 -0
- dreadnode/capabilities/types.py +4 -0
- dreadnode/capabilities/worker_runner.py +32 -4
- dreadnode/core/types/video.py +29 -5
- dreadnode/generators/generator/base.py +12 -5
- dreadnode/generators/generator/litellm_.py +158 -0
- dreadnode/generators/proxy.py +22 -0
- dreadnode/items/__init__.py +15 -0
- dreadnode/items/config.py +150 -0
- dreadnode/items/models.py +80 -0
- dreadnode/judges/trajectory.py +3 -39
- dreadnode/optimization/jobs.py +4 -27
- dreadnode/optimization/study.py +38 -3
- dreadnode/packaging/manifest.py +7 -0
- dreadnode/packaging/oci.py +57 -0
- dreadnode/packaging/task_validation.py +83 -17
- dreadnode/policies/__init__.py +23 -0
- dreadnode/policies/guard.py +13 -0
- dreadnode/policies/reconciliation.py +138 -0
- dreadnode/tools/__init__.py +21 -1
- dreadnode/tools/interaction.py +54 -0
- dreadnode/tools/project_memory.py +318 -0
- dreadnode/tools/read.py +12 -2
- dreadnode/tools/report_items.py +662 -0
- dreadnode/tracing/constants.py +6 -0
- dreadnode/tracing/span.py +22 -0
- {dreadnode-2.0.30.dist-info → dreadnode-2.0.32.dist-info}/METADATA +10 -5
- {dreadnode-2.0.30.dist-info → dreadnode-2.0.32.dist-info}/RECORD +100 -82
- {dreadnode-2.0.30.dist-info → dreadnode-2.0.32.dist-info}/WHEEL +1 -1
- {dreadnode-2.0.30.dist-info → dreadnode-2.0.32.dist-info}/entry_points.txt +0 -0
- {dreadnode-2.0.30.dist-info → dreadnode-2.0.32.dist-info}/licenses/LICENSE +0 -0
dreadnode/agents/agent.py
CHANGED
|
@@ -20,6 +20,7 @@ from pydantic import (
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
import dreadnode
|
|
23
|
+
from dreadnode.agents.engines import AgentEngine, EngineContext, PermissionBridge, resolve_engine
|
|
23
24
|
from dreadnode.agents.events import (
|
|
24
25
|
AgentEnd,
|
|
25
26
|
AgentError,
|
|
@@ -185,10 +186,22 @@ class Agent(Executor[AgentEvent, Trajectory]):
|
|
|
185
186
|
"""Base factor for exponential backoff: wait = base_factor * 2 ** (attempt - 1)."""
|
|
186
187
|
backoff_jitter: bool = Config(default=True)
|
|
187
188
|
"""Whether to add up to ``backoff_base_factor`` seconds of random jitter to each wait."""
|
|
189
|
+
engine: str | AgentEngine | None = Config(default=None, expose_as=str | None)
|
|
190
|
+
"""Owner of the agent loop. ``None``/``"native"`` runs the in-process loop; a
|
|
191
|
+
built-in name (e.g. ``"claude-code"``) or a ``mod:attr`` reference delegates the
|
|
192
|
+
loop to a foreign harness while sessions/eval/optimization/policy keep working."""
|
|
188
193
|
|
|
189
194
|
# Private state
|
|
190
195
|
_generator: Generator | None = PrivateAttr(None, init=False)
|
|
191
196
|
_current_input: str = PrivateAttr("", init=False)
|
|
197
|
+
_capability: tuple[str, str] | None = PrivateAttr(None, init=False)
|
|
198
|
+
"""The (name, version) capability this agent runs under, if any. Bound to
|
|
199
|
+
``current_capability`` for the duration of a run (and reset afterward) so
|
|
200
|
+
runtime tools attribute emitted items to the right producer without leaking
|
|
201
|
+
attribution into later agents that share the async context."""
|
|
202
|
+
_permission_bridge: PermissionBridge | None = PrivateAttr(None, init=False)
|
|
203
|
+
"""Tool-approval bridge injected by the runtime so foreign engines wire their
|
|
204
|
+
permission callback into the native HITL path. ``None`` for bare-SDK use."""
|
|
192
205
|
|
|
193
206
|
# Discoverability namespace for the AgentJudge specialized construction.
|
|
194
207
|
# Assigned at module-load time by ``dreadnode.agents.__init__._lazy_init``
|
|
@@ -660,6 +673,7 @@ class Agent(Executor[AgentEvent, Trajectory]):
|
|
|
660
673
|
# out of the result body.
|
|
661
674
|
tool_error = message.metadata.get("error")
|
|
662
675
|
tool_error_type = message.metadata.get("error_type")
|
|
676
|
+
tool_cost_usd = message.metadata.get("subagent_cost_usd")
|
|
663
677
|
|
|
664
678
|
end_event = ToolEnd(
|
|
665
679
|
agent_id=self.agent_id,
|
|
@@ -670,6 +684,7 @@ class Agent(Executor[AgentEvent, Trajectory]):
|
|
|
670
684
|
stop=stop,
|
|
671
685
|
error=tool_error,
|
|
672
686
|
error_type=tool_error_type,
|
|
687
|
+
cost_usd=tool_cost_usd,
|
|
673
688
|
)
|
|
674
689
|
async for event in self._dispatch(end_event):
|
|
675
690
|
yield event
|
|
@@ -715,6 +730,32 @@ class Agent(Executor[AgentEvent, Trajectory]):
|
|
|
715
730
|
|
|
716
731
|
async def _stream(
|
|
717
732
|
self, trajectory: Trajectory | None = None
|
|
733
|
+
) -> t.AsyncGenerator[AgentEvent, None]:
|
|
734
|
+
"""Drive the loop through the resolved :class:`AgentEngine`.
|
|
735
|
+
|
|
736
|
+
For the native engine this delegates straight to ``_native_run_loop``
|
|
737
|
+
(which dispatches its own hooks inline). Foreign engines yield translated
|
|
738
|
+
native events and call ``ctx.dispatch`` themselves for observational hooks.
|
|
739
|
+
This shim is the ``Executor._stream`` implementation; the surrounding span,
|
|
740
|
+
trajectory accumulation, and tool-context management stay in ``stream``.
|
|
741
|
+
"""
|
|
742
|
+
engine = self._resolve_engine()
|
|
743
|
+
ctx = EngineContext(
|
|
744
|
+
agent=self,
|
|
745
|
+
trajectory=trajectory if trajectory is not None else self.trajectory,
|
|
746
|
+
goal=self._current_input,
|
|
747
|
+
dispatch=self._dispatch,
|
|
748
|
+
permission=self._permission_bridge,
|
|
749
|
+
)
|
|
750
|
+
async for event in engine.run_loop(ctx):
|
|
751
|
+
yield event
|
|
752
|
+
|
|
753
|
+
def _resolve_engine(self) -> AgentEngine:
|
|
754
|
+
"""Resolve this agent's ``engine`` selector to a concrete engine instance."""
|
|
755
|
+
return resolve_engine(self.engine)
|
|
756
|
+
|
|
757
|
+
async def _native_run_loop(
|
|
758
|
+
self, trajectory: Trajectory | None = None
|
|
718
759
|
) -> t.AsyncGenerator[AgentEvent, None]:
|
|
719
760
|
"""
|
|
720
761
|
Core agent execution loop with inline tracing.
|
|
@@ -1168,6 +1209,13 @@ class Agent(Executor[AgentEvent, Trajectory]):
|
|
|
1168
1209
|
active_trajectory = self.trajectory
|
|
1169
1210
|
|
|
1170
1211
|
async with AsyncExitStack() as stack:
|
|
1212
|
+
# Scope capability attribution to THIS run (reset on exit) so it never
|
|
1213
|
+
# leaks into a later agent sharing the async context.
|
|
1214
|
+
if self._capability is not None:
|
|
1215
|
+
from dreadnode.tracing.span import bind_capability
|
|
1216
|
+
|
|
1217
|
+
stack.enter_context(bind_capability(self._capability))
|
|
1218
|
+
|
|
1171
1219
|
# Enter tool contexts
|
|
1172
1220
|
for tool_container in self.tools:
|
|
1173
1221
|
if hasattr(tool_container, "__aenter__") and hasattr(tool_container, "__aexit__"):
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# AGENTS.md — agent engines
|
|
2
|
+
|
|
3
|
+
An **engine** owns the agent loop. The native engine is the default; foreign
|
|
4
|
+
engines (e.g. `claude-code`) delegate the loop to an external harness while the
|
|
5
|
+
agent stays a first-class `Agent`. Canonical contract: `specs/capabilities/engines.md`
|
|
6
|
+
(`CAP-ENG-*`, `CAP-ERES-*`, `CAP-EGOV-*`, `CAP-EBIND-*`).
|
|
7
|
+
|
|
8
|
+
## Files
|
|
9
|
+
|
|
10
|
+
- `base.py` — `AgentEngine` ABC, `EngineContext`, `EnforcementSurface`, `PolicyFacet`, `PermissionBridge`.
|
|
11
|
+
- `native.py` — `NativeEngine`, a thin delegate to `Agent._native_run_loop` (the historical loop body). Keep native byte-for-byte: the loop logic stays on `Agent`.
|
|
12
|
+
- `__init__.py` — registry (`register_engine`) + `resolve_engine` (built-in name / class / instance / `module.path:ClassName`).
|
|
13
|
+
|
|
14
|
+
## Authoring a built-in engine
|
|
15
|
+
|
|
16
|
+
1. Subclass `AgentEngine`, set a stable `name`, implement `async def run_loop(self, ctx)` yielding native `AgentEvent`s and exactly one terminal `AgentEnd`.
|
|
17
|
+
2. Call `register_engine(YourEngine)` in `__init__.py`.
|
|
18
|
+
3. Translate the harness's activity into native events; do **not** invent a new event schema.
|
|
19
|
+
4. If you do not dispatch inline, call `ctx.dispatch(event)` on every translated event so observational hooks run (leave `dispatches_internally = False`).
|
|
20
|
+
5. Override `describe_enforcement(policy)` with an **honest** `EnforcementSurface` — do not claim `enforces` for a facet you only observe. Override `honored_config()` with the explicit set of `Agent` fields you respect, and `consumed_components()` with the capability component kinds you actually use (Decision A: a harness with its own tools consumes none of ours — don't claim `python_tools` you don't inject; see `CAP-ENG-020..025`).
|
|
21
|
+
|
|
22
|
+
## MUST / NEVER
|
|
23
|
+
|
|
24
|
+
- **MUST** keep the `native` path unchanged — the existing agent-loop test suite is the regression gate.
|
|
25
|
+
- **MUST** spawn third-party / subprocess engines inside the runtime sandbox, never in-process in the runtime (`CAP-EBIND-002`).
|
|
26
|
+
- **NEVER** let an engine silently downgrade governance: an un-enforceable required policy facet must surface as a refusal/warning via reconciliation, not be ignored.
|
|
27
|
+
- **NEVER** import `Agent` at module load (cycle): use `if TYPE_CHECKING` and reach the agent through `ctx.agent`.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Agent engines — pluggable, overridable owners of the agent loop.
|
|
2
|
+
|
|
3
|
+
Built-ins are resolved by name (``native``, ``claude-code``); customer engines
|
|
4
|
+
are resolved from a ``mod:attr`` reference (mirroring ``WorkerDef`` import refs).
|
|
5
|
+
Resolution precedence for the ``engine`` selector itself lives in
|
|
6
|
+
``create_agent`` and mirrors how ``model`` resolves.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
|
|
11
|
+
from dreadnode.agents.engines.base import (
|
|
12
|
+
AgentEngine,
|
|
13
|
+
CapabilityComponent,
|
|
14
|
+
EnforcementSurface,
|
|
15
|
+
EngineContext,
|
|
16
|
+
PermissionBridge,
|
|
17
|
+
PolicyFacet,
|
|
18
|
+
)
|
|
19
|
+
from dreadnode.agents.engines.claude_code import ClaudeCodeEngine, ClaudeCodeTranslationState
|
|
20
|
+
from dreadnode.agents.engines.native import NativeEngine
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AgentEngine",
|
|
24
|
+
"CapabilityComponent",
|
|
25
|
+
"ClaudeCodeEngine",
|
|
26
|
+
"ClaudeCodeTranslationState",
|
|
27
|
+
"EnforcementSurface",
|
|
28
|
+
"EngineContext",
|
|
29
|
+
"NativeEngine",
|
|
30
|
+
"PermissionBridge",
|
|
31
|
+
"PolicyFacet",
|
|
32
|
+
"register_engine",
|
|
33
|
+
"resolve_engine",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
_REGISTRY: dict[str, type[AgentEngine]] = {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def register_engine(cls: type[AgentEngine]) -> type[AgentEngine]:
|
|
40
|
+
"""Register a built-in engine class under its ``name`` (usable as a decorator)."""
|
|
41
|
+
name = getattr(cls, "name", None)
|
|
42
|
+
if not name:
|
|
43
|
+
raise ValueError(f"Engine {cls!r} must define a non-empty 'name'.")
|
|
44
|
+
_REGISTRY[name] = cls
|
|
45
|
+
return cls
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
register_engine(NativeEngine)
|
|
49
|
+
register_engine(ClaudeCodeEngine)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _load_engine_from_ref(ref: str) -> type[AgentEngine]:
|
|
53
|
+
"""Import an engine class from a ``module.path:Attr`` or ``module.path.Attr`` ref."""
|
|
54
|
+
module_path, _, attr = ref.partition(":")
|
|
55
|
+
if not attr:
|
|
56
|
+
module_path, _, attr = ref.rpartition(".")
|
|
57
|
+
if not module_path or not attr:
|
|
58
|
+
raise ValueError(f"Invalid engine reference {ref!r}; expected 'module.path:ClassName'.")
|
|
59
|
+
try:
|
|
60
|
+
module = importlib.import_module(module_path)
|
|
61
|
+
except ImportError as exc:
|
|
62
|
+
raise ValueError(f"Could not import engine module '{module_path}' from {ref!r}.") from exc
|
|
63
|
+
obj = getattr(module, attr, None)
|
|
64
|
+
if obj is None or not (isinstance(obj, type) and issubclass(obj, AgentEngine)):
|
|
65
|
+
raise ValueError(f"Engine reference {ref!r} did not resolve to an AgentEngine subclass.")
|
|
66
|
+
return obj
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def resolve_engine(spec: "str | AgentEngine | type[AgentEngine] | None") -> AgentEngine:
|
|
70
|
+
"""Resolve an ``engine`` selector to a concrete :class:`AgentEngine` instance.
|
|
71
|
+
|
|
72
|
+
Accepts ``None`` (→ native), an instance, a class, a built-in name, or a
|
|
73
|
+
``mod:attr`` reference. A fresh instance is returned for class/name/ref specs
|
|
74
|
+
so stateful engines (subprocess-backed) don't leak state across turns.
|
|
75
|
+
"""
|
|
76
|
+
if spec is None:
|
|
77
|
+
return NativeEngine()
|
|
78
|
+
if isinstance(spec, AgentEngine):
|
|
79
|
+
return spec
|
|
80
|
+
if isinstance(spec, type) and issubclass(spec, AgentEngine):
|
|
81
|
+
return spec()
|
|
82
|
+
if isinstance(spec, str):
|
|
83
|
+
if spec in _REGISTRY:
|
|
84
|
+
return _REGISTRY[spec]()
|
|
85
|
+
if ":" in spec or "." in spec:
|
|
86
|
+
return _load_engine_from_ref(spec)()
|
|
87
|
+
known = ", ".join(sorted(_REGISTRY)) or "(none)"
|
|
88
|
+
raise ValueError(f"Unknown engine '{spec}'. Known built-ins: {known}.")
|
|
89
|
+
raise TypeError(f"Unsupported engine selector: {spec!r}")
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Engine contract for the agent loop.
|
|
2
|
+
|
|
3
|
+
An :class:`AgentEngine` owns the *interior* of an agent — the loop that turns a
|
|
4
|
+
goal into generation/tool events. The native engine is the default; foreign
|
|
5
|
+
engines (e.g. ``claude-code``) delegate the loop to an external harness while the
|
|
6
|
+
rest of the stack (sessions, eval, optimization, policy, telemetry, TUI) keeps
|
|
7
|
+
working because the foreign agent is still an :class:`~dreadnode.agents.agent.Agent`.
|
|
8
|
+
|
|
9
|
+
See ``specs/capabilities/engines.md`` (ENG-*) for the canonical contract.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import typing as t
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import StrEnum
|
|
16
|
+
|
|
17
|
+
if t.TYPE_CHECKING:
|
|
18
|
+
from dreadnode.agents.agent import Agent
|
|
19
|
+
from dreadnode.agents.events import AgentEvent
|
|
20
|
+
from dreadnode.agents.trajectory import Trajectory
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PolicyFacet(StrEnum):
|
|
24
|
+
"""A single governable dimension of a session policy.
|
|
25
|
+
|
|
26
|
+
An engine declares, per facet, whether it *enforces* it, *bridges* it via a
|
|
27
|
+
harness callback, can only *observe* it, or has *no equivalent* mechanism.
|
|
28
|
+
The runtime reconciles the session policy against that declaration (ENG-REC-*).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
AUTONOMY = "autonomy"
|
|
32
|
+
"""Block-vs-auto-allow (native ``is_autonomous`` / harness ``permission_mode``)."""
|
|
33
|
+
TOOL_APPROVAL = "tool_approval"
|
|
34
|
+
"""Human-in-the-loop tool approval (``ask_user`` → ``prompt.required``)."""
|
|
35
|
+
STEP_BUDGET = "step_budget"
|
|
36
|
+
"""Step/turn ceiling (``max_steps`` / harness ``max_turns``)."""
|
|
37
|
+
TOKEN_BUDGET = "token_budget" # noqa: S105 (facet name, not a secret)
|
|
38
|
+
"""Token ceiling (no harness equivalent — enforced by killing the run)."""
|
|
39
|
+
COST_BUDGET = "cost_budget"
|
|
40
|
+
"""Cost ceiling (no harness equivalent — enforced by killing the run)."""
|
|
41
|
+
TIME_BUDGET = "time_budget"
|
|
42
|
+
"""Wall-clock ceiling (no harness equivalent — enforced by killing the run)."""
|
|
43
|
+
GUARD_STEERING = "guard_steering"
|
|
44
|
+
"""Mid-loop prevention/steering via hook ``Reaction`` (in-loop only)."""
|
|
45
|
+
SCORERS = "scorers"
|
|
46
|
+
"""Observational scorers/detectors/metrics (attach to events after the fact)."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CapabilityComponent(StrEnum):
|
|
50
|
+
"""A kind of capability-declared resource an engine may or may not consume.
|
|
51
|
+
|
|
52
|
+
The native engine consumes all kinds (it executes our tools in-loop and
|
|
53
|
+
surfaces our skills). A foreign engine that brings its own tools declares a
|
|
54
|
+
subset; the runtime informs the author which declared components are inert
|
|
55
|
+
under that engine (CAP-ENG-022). See ``specs/capabilities/engines.md``.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
PYTHON_TOOLS = "python_tools"
|
|
59
|
+
"""Capability ``@tool`` / MCP tools from our registry, executed in-loop."""
|
|
60
|
+
MCP_SERVERS = "mcp_servers"
|
|
61
|
+
"""MCP servers the capability declares (folded with python_tools today)."""
|
|
62
|
+
SKILLS = "skills"
|
|
63
|
+
"""``SKILL.md`` directories (the shared Agent Skills standard)."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class EnforcementSurface:
|
|
68
|
+
"""What an engine can actually do about each policy facet.
|
|
69
|
+
|
|
70
|
+
A facet absent from all four sets is treated as ``observes_only`` by the
|
|
71
|
+
reconciler (the conservative default).
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
enforces: frozenset[PolicyFacet] = field(default_factory=frozenset)
|
|
75
|
+
"""Enforced natively or by translatable config (e.g. ``permission_mode``)."""
|
|
76
|
+
bridges: frozenset[PolicyFacet] = field(default_factory=frozenset)
|
|
77
|
+
"""Enforced by wiring a harness callback back into our control plane."""
|
|
78
|
+
observes_only: frozenset[PolicyFacet] = field(default_factory=frozenset)
|
|
79
|
+
"""Visible after the harness acted — can record, cannot prevent."""
|
|
80
|
+
no_equivalent: frozenset[PolicyFacet] = field(default_factory=frozenset)
|
|
81
|
+
"""No mechanism in the harness — enforceable only by killing the run."""
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def all_enforced(cls) -> "EnforcementSurface":
|
|
85
|
+
"""The native engine's surface: it owns the loop, so it enforces everything."""
|
|
86
|
+
return cls(enforces=frozenset(PolicyFacet))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@t.runtime_checkable
|
|
90
|
+
class PermissionBridge(t.Protocol):
|
|
91
|
+
"""Bridges a harness tool-approval callback into the native HITL path.
|
|
92
|
+
|
|
93
|
+
Implemented by the runtime over ``_human_prompt_handler`` so a foreign
|
|
94
|
+
engine's ``can_use_tool`` reuses the existing ``prompt.required`` /
|
|
95
|
+
``prompt.respond`` protocol (TUI widget, eval-worker auto-deny). Wired in M1.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
async def request_tool_approval(
|
|
99
|
+
self, *, tool_name: str, tool_input: dict[str, t.Any]
|
|
100
|
+
) -> bool: ...
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class EngineContext:
|
|
105
|
+
"""Everything an engine needs to run one turn.
|
|
106
|
+
|
|
107
|
+
The native engine uses only ``agent`` + ``trajectory`` (it reaches the rest
|
|
108
|
+
through ``agent``). Foreign engines additionally use ``goal``, ``dispatch``
|
|
109
|
+
(to run observational hooks on translated events), and ``permission``.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
agent: "Agent"
|
|
113
|
+
trajectory: "Trajectory"
|
|
114
|
+
goal: str
|
|
115
|
+
dispatch: "t.Callable[[AgentEvent], t.AsyncIterator[AgentEvent]]"
|
|
116
|
+
"""Run an event through the agent's hooks (metrics + reactions). Foreign
|
|
117
|
+
engines call this per translated event; the native engine dispatches inline."""
|
|
118
|
+
permission: "PermissionBridge | None" = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class AgentEngine(ABC):
|
|
122
|
+
"""Pluggable owner of the agent loop.
|
|
123
|
+
|
|
124
|
+
Implementations live in this package (built-ins) or are supplied by a
|
|
125
|
+
customer via a ``mod:attr`` reference. Built-ins register via
|
|
126
|
+
:func:`~dreadnode.agents.engines.register_engine`.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
name: t.ClassVar[str]
|
|
130
|
+
"""Stable identifier used in ``engine:`` declarations and the registry."""
|
|
131
|
+
|
|
132
|
+
dispatches_internally: t.ClassVar[bool] = False
|
|
133
|
+
"""True when ``run_loop`` already routes its events through ``ctx.dispatch``
|
|
134
|
+
(the native engine does, inline). Foreign engines leave this False and call
|
|
135
|
+
``ctx.dispatch`` themselves on translated events."""
|
|
136
|
+
|
|
137
|
+
@abstractmethod
|
|
138
|
+
def run_loop(self, ctx: EngineContext) -> "t.AsyncIterator[AgentEvent]":
|
|
139
|
+
"""Drive one turn, yielding native ``AgentEvent``s as it progresses."""
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
def describe_enforcement(self, policy: t.Any) -> EnforcementSurface: # noqa: ARG002
|
|
143
|
+
"""Declare which policy facets this engine can enforce for ``policy``.
|
|
144
|
+
|
|
145
|
+
Defaults to fully-enforced (correct for any engine that owns a native
|
|
146
|
+
loop). Foreign engines override with an honest, partial surface.
|
|
147
|
+
"""
|
|
148
|
+
return EnforcementSurface.all_enforced()
|
|
149
|
+
|
|
150
|
+
def honored_config(self) -> set[str] | None:
|
|
151
|
+
"""Which ``Agent`` config fields this engine respects.
|
|
152
|
+
|
|
153
|
+
``None`` means "all" (the native engine). A foreign engine returns the
|
|
154
|
+
explicit set it honors so the runtime can warn about ignored config.
|
|
155
|
+
"""
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def consumed_components(self) -> set[CapabilityComponent]:
|
|
159
|
+
"""Which capability component kinds this engine consumes.
|
|
160
|
+
|
|
161
|
+
Defaults to all (the native engine executes our tools in-loop and
|
|
162
|
+
surfaces our skills). A foreign engine that uses its own tools returns a
|
|
163
|
+
subset; the runtime informs the author about the unconsumed components
|
|
164
|
+
a capability declares (CAP-ENG-022).
|
|
165
|
+
"""
|
|
166
|
+
return set(CapabilityComponent)
|