Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions sdks/python/agenta/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@
from .sdk.utils.logging import get_module_logger # noqa: F401
from .sdk.utils.preinit import PreInitObject # noqa: F401

# Agent runtime (the agents subsystem). `Message` is intentionally not re-exported here:
# `agenta.Message` already names the prompt message type; import the agents one from
# `agenta.sdk.agents` when needed.
from .sdk.agents import ( # noqa: F401
AgentaHarness,
AgentConfig,
ClaudeHarness,
Environment,
InProcessPiBackend,
LocalBackend,
PiHarness,
SandboxAgentBackend,
RunSelection,
SessionConfig,
make_harness,
)

DEFAULT_AGENTA_SINGLETON_INSTANCE = AgentaSingleton()

types = client_types
Expand Down
188 changes: 188 additions & 0 deletions sdks/python/agenta/sdk/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
"""Agenta agent runtime: run a coding harness (Pi, Claude, ...) as a swappable port.

Layers (Agenta's hexagonal vocabulary):

- ``dtos.py`` — data contracts (``AgentConfig``, ``SessionConfig``, ``Message``, ...).
- ``interfaces.py`` — the ports (ABCs): ``Backend``, ``Environment``, ``Sandbox``,
``Session``, ``Harness``.
- ``adapters/`` — implementations: ``SandboxAgentBackend`` / ``InProcessPiBackend`` / ``LocalBackend``
and ``PiHarness`` / ``ClaudeHarness``.
- ``utils/`` — shared plumbing (the ``/run`` wire and the transports to the TS runner).

Standalone usage::

import agenta as ag
from agenta.sdk.agents import Message

cfg = ag.ConfigManager.get_from_registry(app_slug="my-agent")
agent = ag.AgentConfig.from_params(cfg)
harness = ag.PiHarness(ag.Environment(ag.SandboxAgentBackend()))
result = await harness.prompt(ag.SessionConfig(agent=agent), [Message(role="user", content="hi")])
"""

from .adapters import (
AgentaHarness,
ClaudeHarness,
InProcessPiBackend,
LocalBackend,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
PiHarness,
SandboxAgentBackend,
make_harness,
)
from .dtos import (
AgentaAgentConfig,
AgentConfig,
AgentEvent,
AgentResult,
ClaudeAgentConfig,
ContentBlock,
HarnessAgentConfig,
HarnessCapabilities,
HarnessType,
Message,
PermissionPolicy,
PiAgentConfig,
RunSelection,
SessionConfig,
ToolCallback,
TraceContext,
to_messages,
)
from .errors import (
AgentRunnerConfigurationError,
ToolResolutionError,
UnsupportedHarnessError,
)
from .interfaces import (
Backend,
Environment,
Harness,
NoopSessionStore,
Sandbox,
Session,
SessionStore,
)
from .mcp import (
MCPConfigurationError,
MCPError,
MCPResolver,
MCPServerConfig,
MissingMCPSecretError,
ResolvedMCPServer,
)
from .streaming import AgentRun
from .tools import (
BuiltinToolConfig,
CallbackToolSpec,
ClientToolConfig,
ClientToolSpec,
CodeToolConfig,
CodeToolSpec,
DuplicateToolNameError,
EnvironmentToolSecretProvider,
GatewayToolResolver,
GatewayToolConfig,
GatewayToolResolution,
GatewayToolResolutionError,
MissingSecretPolicy,
MissingToolSecretError,
ResolvedToolSet,
ToolConfig,
ToolConfigError,
ToolConfigurationError,
ToolError,
ToolResolver,
ToolSecretProvider,
ToolSpec,
UnsupportedToolProviderError,
coerce_tool_config,
coerce_tool_configs,
parse_tool_config,
parse_tool_configs,
)
from .adapters.vercel import (
from_ui_messages,
to_ui_message,
ui_message_stream,
)

__all__ = [
# DTOs
"AgentConfig",
"RunSelection",
"SessionConfig",
"HarnessAgentConfig",
"PiAgentConfig",
"ClaudeAgentConfig",
"AgentaAgentConfig",
"HarnessType",
"HarnessCapabilities",
"ContentBlock",
"Message",
"to_messages",
"AgentEvent",
"AgentResult",
"AgentRun",
# Former flat Vercel adapter names (compatibility; new code uses adapters.vercel)
"from_ui_messages",
"to_ui_message",
"ui_message_stream",
"TraceContext",
"ToolCallback",
"PermissionPolicy",
# Canonical tools API
"ToolConfig",
"BuiltinToolConfig",
"GatewayToolConfig",
"CodeToolConfig",
"ClientToolConfig",
"ToolSpec",
"CallbackToolSpec",
"CodeToolSpec",
"ClientToolSpec",
"ResolvedToolSet",
"GatewayToolResolution",
"ToolResolver",
"ToolSecretProvider",
"GatewayToolResolver",
"EnvironmentToolSecretProvider",
"MissingSecretPolicy",
"parse_tool_config",
"parse_tool_configs",
"coerce_tool_config",
"coerce_tool_configs",
"ToolError",
"ToolConfigError",
"ToolConfigurationError",
"GatewayToolResolutionError",
"UnsupportedToolProviderError",
"MissingToolSecretError",
"DuplicateToolNameError",
# MCP is a sibling subsystem
"MCPServerConfig",
"ResolvedMCPServer",
"MCPResolver",
"MCPError",
"MCPConfigurationError",
"MissingMCPSecretError",
# Interfaces (ports)
"Backend",
"Sandbox",
"Session",
"SessionStore",
"NoopSessionStore",
"Environment",
"Harness",
# Errors
"AgentRunnerConfigurationError",
"UnsupportedHarnessError",
"ToolResolutionError",
# Adapters
"SandboxAgentBackend",
"InProcessPiBackend",
"LocalBackend",
"PiHarness",
"ClaudeHarness",
"AgentaHarness",
"make_harness",
]
24 changes: 24 additions & 0 deletions sdks/python/agenta/sdk/agents/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Adapters: concrete implementations of the agent runtime ports.

- Backend adapters: ``SandboxAgentBackend`` (sandbox-agent over ACP), ``InProcessPiBackend`` (in-process Pi,
the reference backend), ``LocalBackend`` (standalone SDK runs; not yet implemented).
- Harness adapters: ``PiHarness``, ``ClaudeHarness``, ``AgentaHarness`` (+ ``make_harness``).
- HTTP/browser protocol adapters live in subpackages, e.g. ``adapters.vercel``.

Shared plumbing for the runner-backed adapters lives in ``agents/utils``.
"""

from .harnesses import AgentaHarness, ClaudeHarness, PiHarness, make_harness
from .in_process import InProcessPiBackend
from .local import LocalBackend
from .sandbox_agent import SandboxAgentBackend

__all__ = [
"SandboxAgentBackend",
"InProcessPiBackend",
"LocalBackend",
"PiHarness",
"ClaudeHarness",
"AgentaHarness",
"make_harness",
]
54 changes: 54 additions & 0 deletions sdks/python/agenta/sdk/agents/adapters/_runner_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Shared constructor validation for runner-backed adapters."""

from __future__ import annotations

from pathlib import Path
from typing import List, Optional, Sequence

from ..errors import AgentRunnerConfigurationError

DEFAULT_RUNNER_COMMAND = ["pnpm", "exec", "tsx", "src/cli.ts"]
RUNNER_CLI_PATH = Path("src") / "cli.ts"


def resolve_runner_command(
*,
backend_name: str,
url: Optional[str],
command: Optional[Sequence[str]],
cwd: Optional[str],
) -> List[str]:
def _validated_command(raw: Sequence[str]) -> List[str]:
cmd = list(raw)
if not cmd:
raise AgentRunnerConfigurationError(
f"{backend_name} received an empty command. Pass a non-empty command, "
"pass url for an HTTP runner, or set cwd to a runner directory containing "
f"{RUNNER_CLI_PATH.as_posix()}."
)
return cmd

if url:
return (
_validated_command(command)
if command is not None
else list(DEFAULT_RUNNER_COMMAND)
)
if command is not None:
return _validated_command(command)
if not cwd:
raise AgentRunnerConfigurationError(
f"{backend_name} requires a runner transport: pass url for an HTTP runner, "
"pass command for a custom subprocess runner, or pass cwd pointing to a "
f"runner directory containing {RUNNER_CLI_PATH.as_posix()}."
)

cli_path = Path(cwd) / RUNNER_CLI_PATH
if not cli_path.is_file():
raise AgentRunnerConfigurationError(
f"{backend_name} could not find runner CLI at {cli_path}. Pass url for an "
"HTTP runner, pass command for a custom subprocess runner, or set cwd to a "
f"runner directory containing {RUNNER_CLI_PATH.as_posix()}."
)

return list(DEFAULT_RUNNER_COMMAND)
90 changes: 90 additions & 0 deletions sdks/python/agenta/sdk/agents/adapters/agenta_builtins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""The Agenta harness's forced defaults: the things ``AgentaHarness`` always applies.

``AgentaHarness`` is Pi with an opinion. It is the same engine as :class:`PiHarness`, but
every run carries a fixed set of Agenta-shipped extras the author cannot turn off:

- a base **persona** appended to Pi's system prompt (``AGENTA_FORCED_APPEND_SYSTEM``),
- a base **AGENTS.md preamble** the author's instructions are appended to (``AGENTA_PREAMBLE``),
- a set of **forced tools** (``AGENTA_FORCED_TOOLS``), and
- a set of **forced skills** (``AGENTA_FORCED_SKILLS``).

The forced *policy* lives here (harness knowledge). The forced skill *files* live with the
runner that runs Pi, under ``services/agent/skills/<name>/``; the contract between the two is
the skill directory **name**, so each entry in ``AGENTA_FORCED_SKILLS`` must match a committed
directory there.

Two layers, kept distinct on purpose (matching Pi's own split, see :class:`PiAgentConfig`):
the *persona* is an ``append_system`` (changes Pi's base prompt), while *project conventions*
belong in ``AGENTS.md``. ``AGENTA_PREAMBLE`` is the AGENTS.md layer; ``AGENTA_FORCED_APPEND_SYSTEM``
is the persona layer.
"""

from __future__ import annotations

from typing import List, Optional

# The base AGENTS.md preamble. The author's own ``instructions`` are appended after this, so
# the final AGENTS.md is ``AGENTA_PREAMBLE`` + the author's project conventions.
#
# TODO(product): replace this placeholder with the real Agenta AGENTS.md preamble.
AGENTA_PREAMBLE = """\
# Agenta agent

You are an agent running on the Agenta platform. The instructions below are Agenta's
baseline; the user's own instructions follow and take precedence where they are more
specific.

- Prefer the tools and skills provided to you over guessing.
- When a skill matches the task, read its SKILL.md fully before acting.
- Keep answers grounded in what the tools and skills actually return."""

# The base persona, always appended to Pi's built-in system prompt (never replaces it). This
# is the "who the agent is" layer, distinct from the AGENTS.md project-context layer above.
#
# TODO(product): replace this placeholder with the real Agenta persona framing.
AGENTA_FORCED_APPEND_SYSTEM = """\
You are an Agenta agent. Be precise, cite what your tools and skills return, and do not
fabricate results."""

# Built-in tools every Agenta run forces on, unioned with the agent's resolved tools.
# ``read`` is mandatory: Pi only renders the skills section into the system prompt when the
# ``read`` tool is available. ``bash`` lets skills run their helper scripts.
AGENTA_FORCED_TOOLS: List[str] = ["read", "bash"]

# Built-in skills every Agenta run forces on. Each name must match a committed directory under
# the runner's ``services/agent/skills/<name>/`` (the runner resolves names to those dirs).
#
# TODO(product): grow this with the real Agenta skill set.
AGENTA_FORCED_SKILLS: List[str] = ["agenta-getting-started"]


def _join(*parts: Optional[str]) -> Optional[str]:
"""Join the non-empty parts with a blank line, or ``None`` when nothing remains."""
kept = [part.strip() for part in parts if part and part.strip()]
if not kept:
return None
return "\n\n".join(kept)


def compose_instructions(user: Optional[str]) -> Optional[str]:
"""The AGENTS.md the harness ships: the base preamble with the author's instructions
appended after it."""
return _join(AGENTA_PREAMBLE, user)


def compose_append_system(user: Optional[str]) -> Optional[str]:
"""The ``append_system`` the harness ships: the forced base persona with the author's own
``append_system`` appended after it."""
return _join(AGENTA_FORCED_APPEND_SYSTEM, user)


def force_tools(builtin_tools: List[str]) -> List[str]:
"""Union the resolved built-in tools with the forced set, order-stable and de-duplicated
(resolved tools first, then any forced tools not already present)."""
seen = set()
out: List[str] = []
for name in list(builtin_tools) + AGENTA_FORCED_TOOLS:
if name and name not in seen:
seen.add(name)
out.append(name)
return out
Loading
Loading