-
Notifications
You must be signed in to change notification settings - Fork 608
feat(sdk): agent runtime behind backend/harness ports #4771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
b9e62f9
741fc73
2a7c129
0beb120
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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: ``RivetBackend`` / ``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.RivetBackend())) | ||
| result = await harness.prompt(ag.SessionConfig(agent=agent), [Message(role="user", content="hi")]) | ||
| """ | ||
|
|
||
| from .adapters import ( | ||
| AgentaHarness, | ||
| ClaudeHarness, | ||
| InProcessPiBackend, | ||
| LocalBackend, | ||
| PiHarness, | ||
| RivetBackend, | ||
| 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 | ||
| "RivetBackend", | ||
| "InProcessPiBackend", | ||
| "LocalBackend", | ||
| "PiHarness", | ||
| "ClaudeHarness", | ||
| "AgentaHarness", | ||
| "make_harness", | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| """Adapters: concrete implementations of the agent runtime ports. | ||
|
|
||
| - Backend adapters: ``RivetBackend`` (rivet 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 .rivet import RivetBackend | ||
|
|
||
| __all__ = [ | ||
| "RivetBackend", | ||
| "InProcessPiBackend", | ||
| "LocalBackend", | ||
| "PiHarness", | ||
| "ClaudeHarness", | ||
| "AgentaHarness", | ||
| "make_harness", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| """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]: | ||
| if url: | ||
| return list(command) if command is not None else list(DEFAULT_RUNNER_COMMAND) | ||
| if command is not None: | ||
| return list(command) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject empty At Line 22 and Line 24, Suggested fix def resolve_runner_command(
@@
) -> 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, "
+ f"or set cwd to a runner wrapper containing {RUNNER_CLI_PATH.as_posix()}."
+ )
+ return cmd
+
if url:
- return list(command) if command is not None else list(DEFAULT_RUNNER_COMMAND)
+ return _validated_command(command) if command is not None else list(DEFAULT_RUNNER_COMMAND)
if command is not None:
- return list(command)
+ 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 wrapper 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 wrapper containing {RUNNER_CLI_PATH.as_posix()}." | ||
| ) | ||
|
|
||
| return list(DEFAULT_RUNNER_COMMAND) | ||
| 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 |
Uh oh!
There was an error while loading. Please reload this page.