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
4 changes: 2 additions & 2 deletions integrations/dify-plugin/provider/agentcube.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@


class AgentcubeCodeInterpreterProvider(ToolProvider):

def _validate_credentials(self, credentials: dict[str, Any]) -> None:
try:
"""
Expand All @@ -43,7 +43,7 @@ def _validate_credentials(self, credentials: dict[str, Any]) -> None:
# except Exception as e:
# raise ToolProviderOAuthError(str(e))
# return ""

# def _oauth_get_credentials(
# self, redirect_uri: str, system_credentials: Mapping[str, Any], request: Request
# ) -> Mapping[str, Any]:
Expand Down
18 changes: 10 additions & 8 deletions integrations/dify-plugin/tools/agentcube-code-interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ class AgentcubeCodeInterpreterTool(Tool):
def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]:
result = self.execute(**tool_parameters)
yield self.create_json_message(result)


def execute(self, router_url=None, workload_manager_url=None, language="python", code_interpreter_id=None, session_id=None, code=None, command=None, session_reuse=False, **kwargs):

def execute(self, router_url=None, workload_manager_url=None, language="python",
code_interpreter_id=None, session_id=None, code=None,
command=None, session_reuse=False, **kwargs):
# Validate required URLs
if not router_url or not workload_manager_url:
return {"status": "error", "reason": "router_url and workload_manager_url are required"}
Expand All @@ -49,11 +51,11 @@ def execute(self, router_url=None, workload_manager_url=None, language="python",
if command:
command_result = ci_client.execute_command(command)
results.append({"type": "command", "result": command_result})

if language and code:
code_result = ci_client.run_code(language, code)
results.append({"type": "code", "result": code_result})

if not command and not code:
raise ValueError("Either command or code must be provided")
except Exception as e:
Expand All @@ -71,11 +73,11 @@ def execute(self, router_url=None, workload_manager_url=None, language="python",
result["session_id"] = ci_client.session_id
else:
result = {
"status": "success",
"session_id": ci_client.session_id,
"status": "success",
"session_id": ci_client.session_id,
"code_interpreter_id": ci_client.name,
"results": results
}

return result

3 changes: 2 additions & 1 deletion sdk-python/agentcube/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
# limitations under the License.

from .code_interpreter import CodeInterpreterClient
from .agent_runtime import AgentRuntimeClient

__all__ = ["CodeInterpreterClient"]
__all__ = ["CodeInterpreterClient", "AgentRuntimeClient"]
93 changes: 93 additions & 0 deletions sdk-python/agentcube/agent_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Copyright The Volcano Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import os
from typing import Any, Dict, Optional

from requests.exceptions import JSONDecodeError
from agentcube.clients.agent_runtime_data_plane import AgentRuntimeDataPlaneClient
from agentcube.utils.log import get_logger
Comment thread
warjiang marked this conversation as resolved.


class AgentRuntimeClient:

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AgentRuntimeClient class is missing a docstring. Following the pattern from CodeInterpreterClient (sdk-python/agentcube/code_interpreter.py:24-49), please add a comprehensive docstring that explains:

  1. What the class does (manages agent runtime sessions)
  2. How sessions are created/reused
  3. Usage examples with context manager and session reuse
    This is important for API documentation and developer understanding.
Suggested change
class AgentRuntimeClient:
class AgentRuntimeClient:
"""
Client for managing Agent Runtime sessions via the AgentCube Router.
This class provides a high-level interface for invoking long-lived agent
runtimes exposed through the Router's data plane API. It is responsible for:
* Bootstrapping a new agent runtime session when no ``session_id`` is provided.
* Reusing an existing session when a ``session_id`` is supplied.
* Sending invocation payloads to the agent runtime and returning the response.
* Managing the underlying HTTP client lifecycle, including optional context
manager support.
Session management
------------------
When instantiated without a ``session_id``, the client will automatically
bootstrap a new Agent Runtime session by calling
:meth:`AgentRuntimeDataPlaneClient.bootstrap_session_id`. The newly created
``session_id`` is stored on ``self.session_id`` and logged for reference:
* If ``session_id`` is ``None``:
- A new session is created against the configured Router / namespace / agent.
- ``self.session_id`` is set to the newly allocated ID.
- Subsequent :meth:`invoke` calls use this session.
* If ``session_id`` is provided:
- The client will reuse the existing session identified by that ID.
- No new session is created; this is useful for resuming work or sharing
sessions across processes.
Usage
-----
The client can be used directly, or as a context manager to ensure the
underlying HTTP resources are cleaned up automatically:
.. code-block:: python
from agentcube.agent_runtime import AgentRuntimeClient
# Create a new Agent Runtime session and invoke the agent
with AgentRuntimeClient(
agent_name="my-agent",
namespace="default",
router_url="https://router.example.com",
verbose=True,
) as client:
result = client.invoke({"input": "hello agent"})
print(result)
You can also reuse an existing session by passing a known ``session_id``:
.. code-block:: python
# Assume you have a previously created session_id
existing_session_id = "session-1234"
client = AgentRuntimeClient(
agent_name="my-agent",
namespace="default",
router_url="https://router.example.com",
session_id=existing_session_id,
)
try:
result = client.invoke({"input": "continue conversation"})
print(result)
finally:
client.close()
Parameters
----------
agent_name:
Name of the Agent Runtime to invoke, as configured in the Router.
namespace:
Kubernetes namespace (or logical namespace) where the agent is deployed.
Defaults to ``"default"``.
router_url:
Base URL of the AgentCube Router. If not provided, the client will look
up the ``ROUTER_URL`` environment variable. One of these must be set.
verbose:
If ``True``, enables debug-level logging for both this client and the
underlying data plane client.
session_id:
Optional existing session identifier. If provided, the client will reuse
this session instead of creating a new one.
timeout:
Request timeout (in seconds) applied to invocations sent to the Router.
connect_timeout:
Connection timeout (in seconds) for establishing HTTP connections to
the Router.
"""

Copilot uses AI. Check for mistakes.
def __init__(
self,
agent_name: str,
namespace: str = "default",
router_url: Optional[str] = None,
verbose: bool = False,
session_id: Optional[str] = None,
timeout: int = 120,
connect_timeout: float = 5.0,
):
Comment thread
warjiang marked this conversation as resolved.
self.agent_name = agent_name
self.namespace = namespace
self.timeout = timeout
self.connect_timeout = connect_timeout

level = logging.DEBUG if verbose else logging.INFO
self.logger = get_logger(__name__, level=level)

router_url = router_url or os.getenv("ROUTER_URL")
if not router_url:
raise ValueError(
"Router URL for Data Plane communication must be provided via "
"'router_url' argument or 'ROUTER_URL' environment variable."
)
self.router_url = router_url

self.session_id: Optional[str] = session_id
self.dp_client = AgentRuntimeDataPlaneClient(
router_url=self.router_url,
namespace=self.namespace,
agent_name=self.agent_name,
timeout=self.timeout,
connect_timeout=self.connect_timeout,
)
if verbose:
self.dp_client.logger.setLevel(logging.DEBUG)

if not self.session_id:
self.logger.info("Bootstrapping AgentRuntime session...")
self.session_id = self.dp_client.bootstrap_session_id()
Comment thread
warjiang marked this conversation as resolved.
self.logger.info(f"AgentRuntime session created: {self.session_id}")
else:
self.logger.info(f"Reusing AgentRuntime session: {self.session_id}")
Comment thread
warjiang marked this conversation as resolved.

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.close()

def invoke(self, payload: Dict[str, Any], timeout: Optional[float] = None) -> Any:

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The invoke method is missing a docstring that describes its parameters and return value. Following the pattern from CodeInterpreterClient methods (e.g., sdk-python/agentcube/code_interpreter.py:166-177), add a docstring that explains what the method does, the payload parameter, the optional timeout parameter, and what it returns.

Suggested change
def invoke(self, payload: Dict[str, Any], timeout: Optional[float] = None) -> Any:
def invoke(self, payload: Dict[str, Any], timeout: Optional[float] = None) -> Any:
"""Invoke the agent runtime for this session with the given payload.
Parameters
----------
payload : Dict[str, Any]
JSON-serializable request body to send to the agent runtime.
timeout : Optional[float], optional
Per-request timeout in seconds. If provided, this overrides the
default timeout configured on the client.
Returns
-------
Any
The decoded JSON response body if the response contains valid
JSON; otherwise, the raw response text.
"""

Copilot uses AI. Check for mistakes.
if not self.session_id:
raise ValueError("AgentRuntime session_id is not initialized")

resp = self.dp_client.invoke(
session_id=self.session_id,
payload=payload,
timeout=timeout,
)
resp.raise_for_status()

try:
return resp.json()
except JSONDecodeError:
Comment on lines +86 to +88

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The except clause catches JSONDecodeError, but the test at sdk-python/tests/test_agent_runtime.py:81 mocks the response to raise ValueError. Since requests.exceptions.JSONDecodeError is a subclass of ValueError, the test should either raise JSONDecodeError instead, or this code should catch ValueError (which is more general and would catch both). Consider catching ValueError for broader compatibility, or update the test to raise JSONDecodeError for consistency.

Copilot uses AI. Check for mistakes.
return resp.text

def close(self) -> None:
Comment thread
warjiang marked this conversation as resolved.
if self.dp_client:
self.dp_client.close()
7 changes: 5 additions & 2 deletions sdk-python/agentcube/clients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
# limitations under the License.

from .control_plane import ControlPlaneClient
from .data_plane import DataPlaneClient
from .code_interpreter_data_plane import CodeInterpreterDataPlaneClient
from .agent_runtime_data_plane import AgentRuntimeDataPlaneClient

__all__ = [
"ControlPlaneClient",
"DataPlaneClient"
"CodeInterpreterDataPlaneClient",
"AgentRuntimeDataPlaneClient",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now with the new class, the previous names are a little general

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, so I renamed DataPlaneClient -> CodeInterpreterDataPlaneClient

]

Comment thread
warjiang marked this conversation as resolved.
89 changes: 89 additions & 0 deletions sdk-python/agentcube/clients/agent_runtime_data_plane.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright The Volcano Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Any, Dict, Optional
from urllib.parse import urljoin

import requests

from agentcube.utils.http import create_session
from agentcube.utils.log import get_logger


class AgentRuntimeDataPlaneClient:

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AgentRuntimeDataPlaneClient class is missing a docstring. Following the pattern from DataPlaneClient (sdk-python/agentcube/clients/data_plane.py:30-36), add a docstring that explains this client handles communication with the Router for AgentRuntime invocations, including session bootstrapping via GET requests.

Suggested change
class AgentRuntimeDataPlaneClient:
class AgentRuntimeDataPlaneClient:
"""Client for communicating with the Router for AgentRuntime invocations.
This client is responsible for interacting with the Router's
`/v1/namespaces/{namespace}/agent-runtimes/{name}/invocations/` endpoint
for a specific AgentRuntime. It bootstraps a new session by issuing a
GET request to obtain a session ID from the Router, and then uses that
session ID in subsequent POST requests to send invocation payloads.
"""

Copilot uses AI. Check for mistakes.
SESSION_HEADER = "x-agentcube-session-id"

def __init__(
self,
router_url: str,
namespace: str,
agent_name: str,
timeout: int = 120,
connect_timeout: float = 5.0,
pool_connections: int = 10,
pool_maxsize: int = 10,
):
Comment thread
warjiang marked this conversation as resolved.
self.router_url = router_url
self.namespace = namespace
self.agent_name = agent_name
self.timeout = timeout
self.connect_timeout = connect_timeout
self.logger = get_logger(f"{__name__}.AgentRuntimeDataPlaneClient")

base_path = (
f"/v1/namespaces/{namespace}/agent-runtimes/{agent_name}/invocations/"
)
self.base_url = urljoin(router_url, base_path)
Comment thread
warjiang marked this conversation as resolved.

self.session = create_session(
pool_connections=pool_connections,
pool_maxsize=pool_maxsize,
)

def bootstrap_session_id(self) -> str:
Comment thread
warjiang marked this conversation as resolved.
resp = self.session.get(
self.base_url,
timeout=(self.connect_timeout, self.timeout),
)
resp.raise_for_status()

session_id = resp.headers.get(self.SESSION_HEADER)
if not session_id:
raise ValueError(
f"Missing required response header: {self.SESSION_HEADER}"
)
return session_id

def invoke(
self,
session_id: str,
payload: Dict[str, Any],
timeout: Optional[float] = None,
) -> requests.Response:

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The invoke method is missing a docstring. Following the pattern from DataPlaneClient.execute_command (sdk-python/agentcube/clients/data_plane.py:126-132), add a docstring that explains the parameters (session_id, payload, timeout) and return value (requests.Response object).

Suggested change
) -> requests.Response:
) -> requests.Response:
"""
Invoke the agent runtime using an existing session.
Args:
session_id: The AgentCube session identifier used to route the
invocation to the correct agent runtime instance.
payload: The JSON-serializable request body to send to the agent
runtime.
timeout: Optional per-request read timeout in seconds. If not
provided, the client's default timeout is used.
Returns:
requests.Response: The HTTP response returned by the router.
"""

Copilot uses AI. Check for mistakes.
headers = {
self.SESSION_HEADER: session_id,
"Content-Type": "application/json",
}
read_timeout = timeout if timeout is not None else self.timeout

self.logger.debug(f"POST {self.base_url}")
return self.session.post(
self.base_url,
json=payload,
headers=headers,
timeout=(self.connect_timeout, read_timeout),
)

def close(self) -> None:

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close method is missing a docstring. Following the pattern from DataPlaneClient and ControlPlaneClient, add a docstring that explains this method closes the underlying HTTP session and releases connection pool resources.

Suggested change
def close(self) -> None:
def close(self) -> None:
"""Close the underlying HTTP session and release connection pool resources."""

Copilot uses AI. Check for mistakes.
self.session.close()
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from agentcube.utils.http import create_session
from agentcube.exceptions import CommandExecutionError

class DataPlaneClient:
class CodeInterpreterDataPlaneClient:
"""Client for AgentCube Data Plane (Router -> PicoD).
Handles command execution and file operations via the Router.

Expand Down Expand Up @@ -65,7 +65,7 @@ def __init__(
self.connect_timeout = connect_timeout
self.pool_connections = pool_connections
self.pool_maxsize = pool_maxsize
self.logger = get_logger(f"{__name__}.DataPlaneClient")
self.logger = get_logger(f"{__name__}.CodeInterpreterDataPlaneClient")

if base_url:
self.base_url = base_url
Expand Down
8 changes: 4 additions & 4 deletions sdk-python/agentcube/code_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from typing import Optional

from agentcube.clients.control_plane import ControlPlaneClient
from agentcube.clients.data_plane import DataPlaneClient
from agentcube.clients.code_interpreter_data_plane import CodeInterpreterDataPlaneClient
from agentcube.utils.log import get_logger


Expand Down Expand Up @@ -100,7 +100,7 @@ def __init__(

# Session state
self.session_id: Optional[str] = session_id
self.dp_client: Optional[DataPlaneClient] = None
self.dp_client: Optional[CodeInterpreterDataPlaneClient] = None

# Initialize session
if session_id:
Expand All @@ -117,7 +117,7 @@ def __init__(
try:
self._init_data_plane()
except Exception:
# Cleanup session if DataPlaneClient initialization fails
# Cleanup session if CodeInterpreterDataPlaneClient initialization fails
self.logger.warning(
f"Failed to initialize data plane client, "
f"deleting session {self.session_id} to prevent resource leak"
Expand All @@ -128,7 +128,7 @@ def __init__(

def _init_data_plane(self):
"""Initialize the Data Plane client."""
self.dp_client = DataPlaneClient(
self.dp_client = CodeInterpreterDataPlaneClient(
cr_name=self.name,
router_url=self.router_url,
namespace=self.namespace,
Expand Down
47 changes: 47 additions & 0 deletions sdk-python/examples/agent_runtime_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright The Volcano Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from agentcube import AgentRuntimeClient

# first time: it will create a new pod
agent_client_v1 = AgentRuntimeClient(
agent_name="my-agent",
router_url="http://localhost:18081",
namespace="default",
verbose=True,
)
print(agent_client_v1.session_id)

result_v1 = agent_client_v1.invoke(
payload={"prompt": "Hello World!"},
)
print(result_v1)

# second time: it will try to reuse the pod created before
agent_client_v2 = AgentRuntimeClient(
agent_name="my-agent",
router_url="http://localhost:18081",
namespace="default",
session_id=agent_client_v1.session_id,
verbose=True,
)
# same with the first time
print(agent_client_v2.session_id)

result_v2 = agent_client_v2.invoke(
payload={"prompt": "Hello World!"},
)
print(result_v2)


Comment thread
warjiang marked this conversation as resolved.
Comment thread
warjiang marked this conversation as resolved.

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example should demonstrate proper resource cleanup by either using the context manager pattern or explicitly calling close(). Following the pattern from sdk-python/examples/basic_usage.py, consider adding a context manager example or explicitly calling agent_client_v2.close() at the end to release HTTP connection pool resources.

Suggested change
# close clients to release HTTP connection pool resources
agent_client_v2.close()
agent_client_v1.close()

Copilot uses AI. Check for mistakes.
Loading
Loading