runspace-agent 0.1.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.
- runspace_agent/__init__.py +30 -0
- runspace_agent/_docker/Dockerfile +38 -0
- runspace_agent/agents/__init__.py +61 -0
- runspace_agent/agents/base.py +132 -0
- runspace_agent/agents/claude_code/__init__.py +13 -0
- runspace_agent/agents/claude_code/agent.py +171 -0
- runspace_agent/agents/claude_code/defaults.py +38 -0
- runspace_agent/agents/claude_code/env.py +42 -0
- runspace_agent/agents/claude_code/options.py +58 -0
- runspace_agent/agents/claude_code/serializer.py +109 -0
- runspace_agent/cli.py +235 -0
- runspace_agent/container.py +209 -0
- runspace_agent/core.py +187 -0
- runspace_agent/entrypoint.py +86 -0
- runspace_agent/local.py +124 -0
- runspace_agent/prompt.py +121 -0
- runspace_agent/sandbox.py +124 -0
- runspace_agent/server/__init__.py +1 -0
- runspace_agent/server/app.py +612 -0
- runspace_agent/server/models.py +93 -0
- runspace_agent/server/session_manager.py +249 -0
- runspace_agent/server/static/assets/index-BloqQ5R_.js +40 -0
- runspace_agent/server/static/assets/index-C5Az2FQF.css +2 -0
- runspace_agent/server/static/favicon.svg +1 -0
- runspace_agent/server/static/icons.svg +24 -0
- runspace_agent/server/static/index.html +14 -0
- runspace_agent/skills.py +134 -0
- runspace_agent/workspaces.py +80 -0
- runspace_agent-0.1.0.dist-info/METADATA +819 -0
- runspace_agent-0.1.0.dist-info/RECORD +34 -0
- runspace_agent-0.1.0.dist-info/WHEEL +5 -0
- runspace_agent-0.1.0.dist-info/entry_points.txt +2 -0
- runspace_agent-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
- runspace_agent-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""runspace_agent — Sandboxed execution environment for AI agents.
|
|
2
|
+
|
|
3
|
+
Provides a simple interface for running AI agents that operate on
|
|
4
|
+
filesystem directories: an **editable** directory (the agent's output)
|
|
5
|
+
and a **read-only context** directory (traces, domain knowledge, etc.).
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
from runspace_agent import RunspaceSession, run_agent
|
|
10
|
+
|
|
11
|
+
result = await run_agent(RunspaceSession(
|
|
12
|
+
editable_dir=Path("./my_skill"),
|
|
13
|
+
context_dir=Path("./context"),
|
|
14
|
+
prompt="Improve the skill based on the traces.",
|
|
15
|
+
))
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from runspace_agent.agents.base import AgentResult, FilesystemAgent, Workspace
|
|
19
|
+
from runspace_agent.core import RunspaceResult, RunspaceSession, run_agent
|
|
20
|
+
from runspace_agent.skills import Skill
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AgentResult",
|
|
24
|
+
"FilesystemAgent",
|
|
25
|
+
"RunspaceResult",
|
|
26
|
+
"RunspaceSession",
|
|
27
|
+
"Skill",
|
|
28
|
+
"Workspace",
|
|
29
|
+
"run_agent",
|
|
30
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Build recipe for the runspace-agent:latest image.
|
|
2
|
+
#
|
|
3
|
+
# This Dockerfile is shipped inside the wheel (declared as package-data) so it
|
|
4
|
+
# travels with any install — editable, git, or wheel. It is NOT meant to be built
|
|
5
|
+
# from a repo checkout with `docker build .`; the build context is assembled at
|
|
6
|
+
# runtime by runspace_agent.cli (_prepare_build_context), which lays out:
|
|
7
|
+
#
|
|
8
|
+
# ./requirements.txt runtime deps (base + the [claude] extra)
|
|
9
|
+
# ./runspace_agent/ the installed package source tree
|
|
10
|
+
#
|
|
11
|
+
# The image is built automatically by `runspace-srv` (or `--rebuild`).
|
|
12
|
+
FROM python:3.11-slim
|
|
13
|
+
|
|
14
|
+
# System deps + Node.js (required for the Claude Code runtime)
|
|
15
|
+
RUN apt-get update && \
|
|
16
|
+
apt-get install -y --no-install-recommends curl jq && \
|
|
17
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
|
18
|
+
apt-get install -y --no-install-recommends nodejs && \
|
|
19
|
+
apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
20
|
+
|
|
21
|
+
# Install Claude Code CLI globally
|
|
22
|
+
RUN npm install -g @anthropic-ai/claude-code
|
|
23
|
+
|
|
24
|
+
# Install Python deps, then drop in the package source (PYTHONPATH instead of a
|
|
25
|
+
# pip install, so no pyproject.toml is needed in the build context)
|
|
26
|
+
WORKDIR /app
|
|
27
|
+
COPY requirements.txt .
|
|
28
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
29
|
+
COPY runspace_agent/ ./runspace_agent/
|
|
30
|
+
ENV PYTHONPATH=/app
|
|
31
|
+
|
|
32
|
+
# Create non-root user (Claude Code refuses --dangerously-skip-permissions as root)
|
|
33
|
+
RUN useradd -m -s /bin/bash agent && \
|
|
34
|
+
mkdir -p /workspace && chown agent:agent /workspace
|
|
35
|
+
|
|
36
|
+
USER agent
|
|
37
|
+
WORKDIR /workspace
|
|
38
|
+
ENTRYPOINT ["python", "-m", "runspace_agent.entrypoint"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Agent abstractions and implementations for runspace_agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from runspace_agent.agents.base import AgentResult, FilesystemAgent, Workspace
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AgentResult",
|
|
12
|
+
"FilesystemAgent",
|
|
13
|
+
"Workspace",
|
|
14
|
+
"build_agent_options",
|
|
15
|
+
"create_agent",
|
|
16
|
+
"create_default_agent",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
_AGENT_REGISTRY: dict[str, str] = {
|
|
20
|
+
"claude-code": "runspace_agent.agents.claude_code",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_agent_module(agent_type: str) -> Any:
|
|
25
|
+
module_path = _AGENT_REGISTRY.get(agent_type)
|
|
26
|
+
if module_path is None:
|
|
27
|
+
available = ", ".join(sorted(_AGENT_REGISTRY))
|
|
28
|
+
raise ValueError(f"Unknown agent_type: {agent_type!r}. Available: {available}")
|
|
29
|
+
return importlib.import_module(module_path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_agent_options(
|
|
33
|
+
agent_type: str = "claude-code",
|
|
34
|
+
agent_settings: dict[str, Any] | None = None,
|
|
35
|
+
) -> Any:
|
|
36
|
+
"""Build agent-specific options from a freeform settings dict.
|
|
37
|
+
|
|
38
|
+
Each agent's ``build_options`` receives the full dict and reads
|
|
39
|
+
only the keys it understands.
|
|
40
|
+
"""
|
|
41
|
+
mod = _get_agent_module(agent_type)
|
|
42
|
+
return mod.build_options(agent_settings)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_agent(
|
|
46
|
+
agent_type: str = "claude-code",
|
|
47
|
+
options: Any = None,
|
|
48
|
+
) -> FilesystemAgent:
|
|
49
|
+
"""Create an agent instance of the specified type."""
|
|
50
|
+
mod = _get_agent_module(agent_type)
|
|
51
|
+
return mod.create(options)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def create_default_agent(
|
|
55
|
+
options: Any = None,
|
|
56
|
+
) -> FilesystemAgent:
|
|
57
|
+
"""Create the default agent (currently :class:`ClaudeCodeAgent`).
|
|
58
|
+
|
|
59
|
+
Backward-compatible wrapper around :func:`create_agent`.
|
|
60
|
+
"""
|
|
61
|
+
return create_agent(agent_type="claude-code", options=options)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Base types for the FilesystemAgent abstraction.
|
|
2
|
+
|
|
3
|
+
Defines the Protocol that any agent implementation must satisfy,
|
|
4
|
+
plus the Workspace and AgentResult types used across all backends.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Workspace:
|
|
16
|
+
"""The sandboxed view that any FilesystemAgent receives.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
editable_dir: Directory the agent should focus on modifying.
|
|
20
|
+
context_dir: Directory with read-only reference material
|
|
21
|
+
(traces, domain knowledge, performance history, etc.).
|
|
22
|
+
prompt: Fully constructed prompt including directory descriptions.
|
|
23
|
+
skills_dir: Path to the agent-specific skills directory, if loaded.
|
|
24
|
+
cwd: Session root directory (the agent's working directory).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
editable_dir: Path
|
|
28
|
+
context_dir: Path
|
|
29
|
+
prompt: str
|
|
30
|
+
skills_dir: Path | None
|
|
31
|
+
cwd: Path
|
|
32
|
+
hooks: dict[str, list[Any]] | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class AgentResult:
|
|
37
|
+
"""Result returned by a FilesystemAgent after execution.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
success: Whether the agent completed without errors.
|
|
41
|
+
messages: Raw messages from the agent execution (SDK-specific).
|
|
42
|
+
conversation: Serialized conversation as a list of JSON-ready dicts.
|
|
43
|
+
Each agent implementation is responsible for populating this
|
|
44
|
+
from its SDK-specific message types.
|
|
45
|
+
total_tokens: Approximate total tokens consumed.
|
|
46
|
+
duration_ms: Wall-clock execution time in milliseconds.
|
|
47
|
+
error: Error message if the agent failed, None otherwise.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
success: bool
|
|
51
|
+
messages: list[Any] = field(default_factory=list)
|
|
52
|
+
conversation: list[dict[str, Any]] = field(default_factory=list)
|
|
53
|
+
total_tokens: int = 0
|
|
54
|
+
total_cost_usd: float | None = None
|
|
55
|
+
duration_ms: int = 0
|
|
56
|
+
error: str | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@runtime_checkable
|
|
60
|
+
class FilesystemAgent(Protocol):
|
|
61
|
+
"""Protocol for agents that operate on filesystem directories.
|
|
62
|
+
|
|
63
|
+
Any class with a ``skills_folder_name`` attribute and an async ``run``
|
|
64
|
+
method matching this signature satisfies the protocol. This enables
|
|
65
|
+
pluggable agent backends (Claude Code, OpenCode, custom, ...) without
|
|
66
|
+
requiring inheritance.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
skills_folder_name: Relative path from ``cwd`` where this agent
|
|
70
|
+
expects skills to be placed. For example ``".claude/skills"``
|
|
71
|
+
for Claude Code or ``".opencode/skills"`` for OpenCode.
|
|
72
|
+
Used to determine where to copy user-provided skills and default skills.
|
|
73
|
+
default_skills_dir: Absolute path to the directory containing the
|
|
74
|
+
agent's bundled/preinstalled skills on disk. Each subdirectory
|
|
75
|
+
is a separate skill. ``None`` means the agent ships no default
|
|
76
|
+
skills.
|
|
77
|
+
|
|
78
|
+
Adding a new agent type
|
|
79
|
+
-----------------------
|
|
80
|
+
1. Create ``agents/<name>/`` with three files:
|
|
81
|
+
|
|
82
|
+
``agent.py`` — a class satisfying this Protocol::
|
|
83
|
+
|
|
84
|
+
class MyAgent:
|
|
85
|
+
skills_folder_name: str = ".<name>/skills"
|
|
86
|
+
default_skills_dir: Path | None = ...
|
|
87
|
+
|
|
88
|
+
def __init__(self, *, options: MyAgentOptions | None = None): ...
|
|
89
|
+
async def run(self, workspace: Workspace) -> AgentResult: ...
|
|
90
|
+
|
|
91
|
+
``options.py`` — two module-level functions the registry calls::
|
|
92
|
+
|
|
93
|
+
def build_options(
|
|
94
|
+
agent_settings: dict | None = None,
|
|
95
|
+
) -> MyAgentOptions:
|
|
96
|
+
'''Build agent-specific options from the settings dict.
|
|
97
|
+
Read only the keys your agent understands.'''
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
def create(options: Any = None) -> MyAgent:
|
|
101
|
+
'''Instantiate the agent.'''
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
``__init__.py`` — re-export everything::
|
|
105
|
+
|
|
106
|
+
from agents.<name>.agent import MyAgent
|
|
107
|
+
from agents.<name>.options import build_options, create
|
|
108
|
+
|
|
109
|
+
2. Register the agent in ``agents/__init__.py``::
|
|
110
|
+
|
|
111
|
+
_AGENT_REGISTRY: dict[str, str] = {
|
|
112
|
+
"claude-code": "runspace_agent.agents.claude_code",
|
|
113
|
+
"<name>": "runspace_agent.agents.<name>", # add this
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
That's it. The server, container, and entrypoint all resolve the agent
|
|
117
|
+
through the registry, so no changes are needed outside ``agents/``.
|
|
118
|
+
Clients select the agent by passing ``"agent_type": "<name>"`` in the
|
|
119
|
+
``POST /run`` request body (defaults to ``"claude-code"``).
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
skills_folder_name: str
|
|
123
|
+
default_skills_dir: Path | None
|
|
124
|
+
|
|
125
|
+
async def run(self, workspace: Workspace) -> AgentResult:
|
|
126
|
+
"""Execute the agent inside the given workspace.
|
|
127
|
+
|
|
128
|
+
The agent should read from ``workspace.context_dir``, modify files
|
|
129
|
+
in ``workspace.editable_dir``, and follow the instructions in
|
|
130
|
+
``workspace.prompt``.
|
|
131
|
+
"""
|
|
132
|
+
...
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Claude Code agent implementation."""
|
|
2
|
+
|
|
3
|
+
from runspace_agent.agents.claude_code.agent import ClaudeCodeAgent
|
|
4
|
+
from runspace_agent.agents.claude_code.env import ClaudeModel, build_claude_env
|
|
5
|
+
from runspace_agent.agents.claude_code.options import build_options, create
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ClaudeCodeAgent",
|
|
9
|
+
"ClaudeModel",
|
|
10
|
+
"build_claude_env",
|
|
11
|
+
"build_options",
|
|
12
|
+
"create",
|
|
13
|
+
]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""ClaudeCodeAgent -- FilesystemAgent implementation using the Claude Agent SDK.
|
|
2
|
+
|
|
3
|
+
Requires the optional ``claude-code-sdk`` dependency::
|
|
4
|
+
|
|
5
|
+
uv pip install runspace-agent[claude]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from runspace_agent.agents.base import AgentResult, Workspace
|
|
16
|
+
from runspace_agent.agents.claude_code.defaults import (
|
|
17
|
+
DEFAULT_ALLOWED_TOOLS,
|
|
18
|
+
DEFAULT_DISALLOWED_TOOLS,
|
|
19
|
+
DEFAULT_MAX_TURNS,
|
|
20
|
+
DEFAULT_SYSTEM_PROMPT,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from claude_code_sdk import ClaudeCodeOptions
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClaudeCodeAgent:
|
|
28
|
+
"""Run a Claude Code agent inside a :class:`Workspace`.
|
|
29
|
+
|
|
30
|
+
This is the built-in :class:`~runspace_agent.agents.base.FilesystemAgent`
|
|
31
|
+
implementation backed by the Claude Agent SDK.
|
|
32
|
+
|
|
33
|
+
Users pass a :class:`~claude_code_sdk.ClaudeCodeOptions` object to
|
|
34
|
+
configure the agent. The agent **enforces** the following fields
|
|
35
|
+
regardless of what the user sets:
|
|
36
|
+
|
|
37
|
+
* ``permission_mode`` → ``"bypassPermissions"`` (headless container)
|
|
38
|
+
* ``cwd`` → from the workspace (sandbox boundary)
|
|
39
|
+
* ``system_prompt`` → headless system prompt (no interactive prompts)
|
|
40
|
+
* ``disallowed_tools`` → interactive/scheduling tools disabled for headless
|
|
41
|
+
* ``hooks`` → from the workspace (sandbox enforcement)
|
|
42
|
+
|
|
43
|
+
If the user does not set ``allowed_tools`` or ``max_turns``, sensible
|
|
44
|
+
defaults are applied.
|
|
45
|
+
|
|
46
|
+
Parameters:
|
|
47
|
+
options: A :class:`~claude_code_sdk.ClaudeCodeOptions` instance.
|
|
48
|
+
When ``None``, a bare default is created at run time.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
skills_folder_name: str = ".claude/skills"
|
|
52
|
+
|
|
53
|
+
# Repo-root-relative path to bundled skills. Computed once at module load.
|
|
54
|
+
_DEFAULT_SKILLS_DIR: Path = (
|
|
55
|
+
Path(__file__).resolve().parent.parent.parent.parent.parent / ".claude" / "skills"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def __init__(self, options: ClaudeCodeOptions | None = None) -> None:
|
|
59
|
+
self._user_options = options
|
|
60
|
+
self.default_skills_dir: Path | None = self._DEFAULT_SKILLS_DIR
|
|
61
|
+
|
|
62
|
+
async def run(self, workspace: Workspace) -> AgentResult:
|
|
63
|
+
"""Execute the Claude Code agent inside *workspace*."""
|
|
64
|
+
query, ClaudeCodeOptions = self._import_sdk()
|
|
65
|
+
options = self._build_effective_options(ClaudeCodeOptions, workspace)
|
|
66
|
+
return await self._run_agent(query, options, workspace)
|
|
67
|
+
|
|
68
|
+
def _build_effective_options(
|
|
69
|
+
self,
|
|
70
|
+
ClaudeCodeOptions: type,
|
|
71
|
+
workspace: Workspace,
|
|
72
|
+
) -> Any:
|
|
73
|
+
"""Merge user options with enforced sandbox overrides.
|
|
74
|
+
|
|
75
|
+
Priority (highest to lowest):
|
|
76
|
+
1. Enforced fields (always override, non-negotiable)
|
|
77
|
+
2. User-provided fields (from self._user_options)
|
|
78
|
+
3. Sensible defaults (only if user left field at dataclass default)
|
|
79
|
+
"""
|
|
80
|
+
base = self._user_options if self._user_options is not None else ClaudeCodeOptions()
|
|
81
|
+
|
|
82
|
+
overrides: dict[str, Any] = {}
|
|
83
|
+
|
|
84
|
+
# Defaults: fill in only if user did not set them
|
|
85
|
+
if not base.allowed_tools:
|
|
86
|
+
overrides["allowed_tools"] = list(DEFAULT_ALLOWED_TOOLS)
|
|
87
|
+
|
|
88
|
+
if base.max_turns is None:
|
|
89
|
+
overrides["max_turns"] = DEFAULT_MAX_TURNS
|
|
90
|
+
|
|
91
|
+
# Enforced fields: always override regardless of user input
|
|
92
|
+
overrides["permission_mode"] = "bypassPermissions"
|
|
93
|
+
overrides["cwd"] = str(workspace.cwd)
|
|
94
|
+
overrides["system_prompt"] = DEFAULT_SYSTEM_PROMPT
|
|
95
|
+
overrides["disallowed_tools"] = list(DEFAULT_DISALLOWED_TOOLS)
|
|
96
|
+
|
|
97
|
+
if workspace.hooks:
|
|
98
|
+
overrides["hooks"] = self._build_sdk_hooks(workspace.hooks)
|
|
99
|
+
|
|
100
|
+
return dataclasses.replace(base, **overrides)
|
|
101
|
+
|
|
102
|
+
async def _run_agent(
|
|
103
|
+
self,
|
|
104
|
+
query: Any,
|
|
105
|
+
options: Any,
|
|
106
|
+
workspace: Workspace,
|
|
107
|
+
) -> AgentResult:
|
|
108
|
+
"""Iterate the agent loop with pre-built options."""
|
|
109
|
+
messages: list[Any] = []
|
|
110
|
+
total_tokens = 0
|
|
111
|
+
total_cost_usd: float | None = None
|
|
112
|
+
start_ms = int(time.time() * 1000)
|
|
113
|
+
|
|
114
|
+
async for message in query(
|
|
115
|
+
prompt=workspace.prompt,
|
|
116
|
+
options=options,
|
|
117
|
+
):
|
|
118
|
+
messages.append(message)
|
|
119
|
+
|
|
120
|
+
if type(message).__name__ == "ResultMessage":
|
|
121
|
+
usage = getattr(message, "usage", None)
|
|
122
|
+
if isinstance(usage, dict):
|
|
123
|
+
total_tokens += usage.get("input_tokens", 0)
|
|
124
|
+
total_tokens += usage.get("output_tokens", 0)
|
|
125
|
+
cost = getattr(message, "total_cost_usd", None)
|
|
126
|
+
if cost is not None:
|
|
127
|
+
total_cost_usd = cost
|
|
128
|
+
|
|
129
|
+
from runspace_agent.agents.claude_code.serializer import serialize_messages
|
|
130
|
+
|
|
131
|
+
duration_ms = int(time.time() * 1000) - start_ms
|
|
132
|
+
return AgentResult(
|
|
133
|
+
success=True,
|
|
134
|
+
messages=messages,
|
|
135
|
+
conversation=serialize_messages(messages),
|
|
136
|
+
total_tokens=total_tokens,
|
|
137
|
+
total_cost_usd=total_cost_usd,
|
|
138
|
+
duration_ms=duration_ms,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _build_sdk_hooks(hooks: dict[str, list[Any]]) -> dict[str, list[Any]]:
|
|
143
|
+
"""Convert intermediate hooks format to SDK HookMatcher objects."""
|
|
144
|
+
from claude_code_sdk.types import HookMatcher
|
|
145
|
+
|
|
146
|
+
sdk_hooks: dict[str, list[Any]] = {}
|
|
147
|
+
for event_name, matchers in hooks.items():
|
|
148
|
+
sdk_hooks[event_name] = [
|
|
149
|
+
HookMatcher(
|
|
150
|
+
matcher=m["matcher"],
|
|
151
|
+
hooks=[m["hook_fn"]],
|
|
152
|
+
)
|
|
153
|
+
for m in matchers
|
|
154
|
+
]
|
|
155
|
+
return sdk_hooks
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _import_sdk() -> tuple[Any, Any]:
|
|
159
|
+
"""Lazily import the Claude Agent SDK.
|
|
160
|
+
|
|
161
|
+
Raises :class:`ImportError` with a helpful message when the
|
|
162
|
+
optional dependency is missing.
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
from claude_code_sdk import ClaudeCodeOptions, query
|
|
166
|
+
except ImportError:
|
|
167
|
+
raise ImportError(
|
|
168
|
+
"ClaudeCodeAgent requires the 'claude-code-sdk' package. "
|
|
169
|
+
"Install it with: uv pip install claude-code-sdk"
|
|
170
|
+
) from None
|
|
171
|
+
return query, ClaudeCodeOptions
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Default constants for the ClaudeCodeAgent."""
|
|
2
|
+
|
|
3
|
+
DEFAULT_MAX_TURNS: int = 300
|
|
4
|
+
|
|
5
|
+
DEFAULT_ALLOWED_TOOLS: list[str] = [
|
|
6
|
+
"Read",
|
|
7
|
+
"Write",
|
|
8
|
+
"Edit",
|
|
9
|
+
"Bash",
|
|
10
|
+
"Glob",
|
|
11
|
+
"Grep",
|
|
12
|
+
"Skill",
|
|
13
|
+
"WebSearch",
|
|
14
|
+
"WebFetch",
|
|
15
|
+
"Agent",
|
|
16
|
+
"LSP",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
DEFAULT_DISALLOWED_TOOLS: list[str] = [
|
|
20
|
+
"AskUserQuestion",
|
|
21
|
+
"EnterPlanMode",
|
|
22
|
+
"ExitPlanMode",
|
|
23
|
+
"EnterWorktree",
|
|
24
|
+
"ExitWorktree",
|
|
25
|
+
"ScheduleWakeup",
|
|
26
|
+
"CronCreate",
|
|
27
|
+
"CronDelete",
|
|
28
|
+
"CronList",
|
|
29
|
+
"Monitor",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
DEFAULT_SYSTEM_PROMPT: str = (
|
|
33
|
+
"You are running headless in an automated pipeline. "
|
|
34
|
+
"There is no human available to answer questions. "
|
|
35
|
+
"Do NOT use AskHumanQuestion or any interactive prompts. "
|
|
36
|
+
"Complete the task fully and autonomously. "
|
|
37
|
+
"If you encounter ambiguity, make a reasonable decision and proceed."
|
|
38
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Helper for building Claude Code environment variables from the current shell.
|
|
2
|
+
|
|
3
|
+
This is a convenience for the examples and manual tests in this repo — it reads
|
|
4
|
+
your real ``os.environ`` to assemble the env dict passed to Claude Code. Library
|
|
5
|
+
users typically don't need it: they call ``POST /run`` and pass their own
|
|
6
|
+
credentials explicitly via ``agent_settings.env`` (or ``ClaudeCodeOptions.env``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from enum import StrEnum
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ClaudeModel(StrEnum):
|
|
16
|
+
OPUS_4_8 = "claude-opus-4-8"
|
|
17
|
+
SONNET_4_6 = "claude-sonnet-4-6"
|
|
18
|
+
HAIKU_4_5 = "claude-haiku-4-5"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_claude_env(model: ClaudeModel | None = None) -> dict[str, str]:
|
|
22
|
+
"""Build Claude Code env vars from the current environment.
|
|
23
|
+
|
|
24
|
+
Reads credentials/config from ``os.environ`` (with a couple of common
|
|
25
|
+
fallbacks). Callers typically drop empty values before use, e.g.
|
|
26
|
+
``{k: v for k, v in build_claude_env().items() if v}``.
|
|
27
|
+
"""
|
|
28
|
+
model_id = (
|
|
29
|
+
model.value if model else os.environ.get("ANTHROPIC_MODEL", "claude-opus-4-8")
|
|
30
|
+
)
|
|
31
|
+
return {
|
|
32
|
+
"ANTHROPIC_API_KEY": os.environ.get("ANTHROPIC_API_KEY", ""),
|
|
33
|
+
"ANTHROPIC_BASE_URL": os.environ.get("ANTHROPIC_BASE_URL", "")
|
|
34
|
+
or os.environ.get("CLAUDE_CODE_LITELLM_BASE_URL", "")
|
|
35
|
+
or os.environ.get("IBM_THIRD_PARTY_API_BASE", ""),
|
|
36
|
+
"ANTHROPIC_AUTH_TOKEN": os.environ.get("ANTHROPIC_AUTH_TOKEN", "")
|
|
37
|
+
or os.environ.get("IBM_THIRD_PARTY_API_KEY", ""),
|
|
38
|
+
"ANTHROPIC_MODEL": model_id,
|
|
39
|
+
"CLAUDE_CODE_SUBAGENT_MODEL": model_id,
|
|
40
|
+
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1",
|
|
41
|
+
"opusPlanEnabled": "true",
|
|
42
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Claude Code agent options builder and factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from claude_code_sdk import ClaudeCodeOptions
|
|
9
|
+
|
|
10
|
+
from runspace_agent.agents.claude_code.agent import ClaudeCodeAgent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_options(
|
|
14
|
+
agent_settings: dict[str, Any] | None = None,
|
|
15
|
+
) -> ClaudeCodeOptions:
|
|
16
|
+
"""Build a :class:`ClaudeCodeOptions` from a settings dict.
|
|
17
|
+
|
|
18
|
+
The dict may contain any of the following keys (all optional):
|
|
19
|
+
|
|
20
|
+
- ``env``: dict of environment variables
|
|
21
|
+
- ``model``: Claude model name
|
|
22
|
+
- ``permissions.allow`` / ``permissions.disallow``: tool lists
|
|
23
|
+
- ``max_turns``: maximum conversation turns (default 300)
|
|
24
|
+
- ``mcp_servers``: MCP server configuration
|
|
25
|
+
"""
|
|
26
|
+
from claude_code_sdk import ClaudeCodeOptions
|
|
27
|
+
|
|
28
|
+
settings = agent_settings or {}
|
|
29
|
+
kwargs: dict[str, Any] = {}
|
|
30
|
+
|
|
31
|
+
env_vars = {k: str(v) for k, v in settings.get("env", {}).items()}
|
|
32
|
+
if env_vars:
|
|
33
|
+
kwargs["env"] = env_vars
|
|
34
|
+
|
|
35
|
+
model = settings.get("model")
|
|
36
|
+
if model:
|
|
37
|
+
kwargs["model"] = model
|
|
38
|
+
|
|
39
|
+
permissions = settings.get("permissions", {})
|
|
40
|
+
if permissions.get("allow"):
|
|
41
|
+
kwargs["allowed_tools"] = list(permissions["allow"])
|
|
42
|
+
if permissions.get("disallow"):
|
|
43
|
+
kwargs["disallowed_tools"] = list(permissions["disallow"])
|
|
44
|
+
|
|
45
|
+
kwargs["max_turns"] = settings.get("max_turns", 300)
|
|
46
|
+
|
|
47
|
+
mcp_servers = settings.get("mcp_servers")
|
|
48
|
+
if mcp_servers:
|
|
49
|
+
kwargs["mcp_servers"] = mcp_servers
|
|
50
|
+
|
|
51
|
+
return ClaudeCodeOptions(**kwargs)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def create(options: Any = None) -> ClaudeCodeAgent:
|
|
55
|
+
"""Create a :class:`ClaudeCodeAgent` instance."""
|
|
56
|
+
from runspace_agent.agents.claude_code.agent import ClaudeCodeAgent
|
|
57
|
+
|
|
58
|
+
return ClaudeCodeAgent(options=options)
|