Skip to content
Closed
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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,21 @@ See the [examples/](./examples) directory for runnable scripts:

MCP examples are provided for both clouds and explicitly send `ark-beta-mcp: true`. Other built-in-tool examples are CN-only and show their required beta headers.

## Managed Agents multiagent updates

`MultiagentType` is a string enum. Its wire value remains `coordinator` or
`multiagent_20261001`, while `str(config.type)` includes the enum class name.
Use `.value` when an application needs the wire string.

`MultiagentConfig` represents Create and Response values, while
`MultiagentPatch` represents Update fields: `coordinator` uses `agents` and
`multiagent_20261001` uses `subagents`. The patch model separately represents
omitted and explicit-null values.

When updating an Agent, omit `multiagent` to leave the topology unchanged, pass
`multiagent=None` to remove it, or pass `agents=[]` to clear a coordinator's
agent roster.

## Self-hosted MCP tools

The self-hosted worker core exposes a protocol-independent `MCPClient`
Expand All @@ -336,6 +351,15 @@ declaration and `mcp_tools()` for the worker registry. Keep both alive for the
entire worker lifetime. See the runnable
[`self_hosted_mcp_worker`](./examples/self_hosted_mcp_worker) example.

`EnvironmentWorkerOptions` exposes `prefer_stream` and
`stream_timeout_seconds`; the defaults remain stream mode. The timeout is applied
to httpx connect/read phases, so a stream that receives no bytes for 30 seconds
reconnects rather than expiring after 30 seconds in total. Worker logs record
stream connection attempts, the first business event, session status
transitions, reconnects, and a warning when a running session has made no
business progress for 60 seconds. Periodic `span.*` events do not count as
business progress. These logs do not add an automatic progress timeout.

The adapter wraps an already connected MCP `ClientSession`, so applications may
use stdio or another transport supported by their MCP client. One client
session is reused across all Managed Agents Sessions handled by the worker;
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.17.0
0.18.0
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "arkruntime"
version = "0.17.0"
version = "0.18.0"
description = "Ark runtime SDK for Python"
readme = "README.md"
requires-python = ">=3.8"
Expand Down
74 changes: 73 additions & 1 deletion src/arkruntime/resources/agents/agents.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
from __future__ import annotations

from typing import List, Optional
from urllib.parse import quote

import httpx

from ..._base_client import make_request_options
from ..._managed_agents_serialize import dump_body
from ..._resource import AsyncAPIResource, SyncAPIResource
from ..._types import NOT_GIVEN, Body, Headers, NotGiven, Query

# AUTOGEN-START create-imports
from ...types.agent.agent import Agent
from ...types.agent.agent_skill_ref import AgentSkillRef
from ...types.agent.context_management_policies_param import ContextManagementPoliciesParam
from ...types.agent.delete_agent_response import DeleteAgentResponse
from ...types.agent.list_agents_response import ListAgentsResponse
from ...types.agent.mcp_server import MCPServer
from ...types.agent.model_config import ModelConfig
from ...types.agent.multiagent_config import MultiagentConfig
from ...types.agent.multiagent_patch import MultiagentPatch
from ...types.agent.tag import Tag
from ...types.agent.tool_item import ToolItem

# AUTOGEN-END create-imports

__all__ = ["Agents", "AsyncAgents"]

_PREFIX = "/agents"


def _list_query(
*,
include_archived: bool | NotGiven = NOT_GIVEN,
limit=NOT_GIVEN,
page=NOT_GIVEN,
created_at_gte=NOT_GIVEN,
Expand All @@ -39,9 +47,19 @@ def _list_query(
}.items():
if v is not NOT_GIVEN and v is not None:
q[k] = v
if include_archived is not NOT_GIVEN and include_archived is not None:
q["include_archived"] = include_archived
return q


def _validate_create_multiagent(
multiagent: MultiagentConfig | NotGiven | None,
) -> MultiagentConfig | NotGiven | None:
if multiagent is NOT_GIVEN or multiagent is None:
return multiagent
return MultiagentConfig.model_validate(multiagent)


class Agents(SyncAPIResource):
def create(
self,
Expand All @@ -56,11 +74,15 @@ def create(
multiagent: Optional[MultiagentConfig] | NotGiven = NOT_GIVEN,
metadata: Optional[dict] | NotGiven = NOT_GIVEN,
tags: Optional[List[Tag]] | NotGiven = NOT_GIVEN,
# AUTOGEN-START create-kwargs
context_management: Optional[ContextManagementPoliciesParam] | NotGiven = NOT_GIVEN,
# AUTOGEN-END create-kwargs
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None = None,
) -> Agent:
multiagent = _validate_create_multiagent(multiagent)
return self._post(
_PREFIX,
body=dump_body(
Expand All @@ -75,6 +97,9 @@ def create(
"multiagent": multiagent,
"metadata": metadata,
"tags": tags,
# AUTOGEN-START create-body
"context_management": context_management,
# AUTOGEN-END create-body
}
),
options=make_request_options(
Expand Down Expand Up @@ -113,6 +138,7 @@ def retrieve(
def list(
self,
*,
include_archived: bool | NotGiven = NOT_GIVEN,
limit=NOT_GIVEN,
page=NOT_GIVEN,
created_at_gte=NOT_GIVEN,
Expand All @@ -126,6 +152,7 @@ def list(
_PREFIX,
options=make_request_options(
query=_list_query(
include_archived=include_archived,
limit=limit,
page=page,
created_at_gte=created_at_gte,
Expand All @@ -151,8 +178,11 @@ def update(
mcp_servers: Optional[List[MCPServer]] | NotGiven = NOT_GIVEN,
tools: Optional[List[ToolItem]] | NotGiven = NOT_GIVEN,
skills: Optional[List[AgentSkillRef]] | NotGiven = NOT_GIVEN,
multiagent: Optional[MultiagentConfig] | NotGiven = NOT_GIVEN,
multiagent: Optional[MultiagentPatch] | NotGiven = NOT_GIVEN,
metadata: Optional[dict] | NotGiven = NOT_GIVEN,
# AUTOGEN-START update-kwargs
context_management: Optional[ContextManagementPoliciesParam] | NotGiven = NOT_GIVEN,
# AUTOGEN-END update-kwargs
extra_headers=None,
extra_query=None,
extra_body=None,
Expand All @@ -174,6 +204,9 @@ def update(
"skills": skills,
"multiagent": multiagent,
"metadata": metadata,
# AUTOGEN-START update-body
"context_management": context_management,
# AUTOGEN-END update-body
}
),
options=make_request_options(
Expand Down Expand Up @@ -221,6 +254,18 @@ def list_versions(
cast_to=ListAgentsResponse,
)

def archive(self, agent_id: str, *, extra_headers=None, extra_query=None, timeout=None) -> Agent:
"""Archive this agent; the POST has no request body."""
if not agent_id:
raise ValueError("agent_id is required")
return self._post(
f"{_PREFIX}/{quote(agent_id, safe='')}/archive",
# Raw empty bytes bypass the client's default empty JSON object.
body=b"", # type: ignore[arg-type]
options=make_request_options(extra_headers=extra_headers, extra_query=extra_query, timeout=timeout),
cast_to=Agent,
)


class AsyncAgents(AsyncAPIResource):
async def create(
Expand All @@ -236,11 +281,15 @@ async def create(
multiagent=NOT_GIVEN,
metadata=NOT_GIVEN,
tags=NOT_GIVEN,
# AUTOGEN-START create-kwargs
context_management: Optional[ContextManagementPoliciesParam] | NotGiven = NOT_GIVEN,
# AUTOGEN-END create-kwargs
extra_headers=None,
extra_query=None,
extra_body=None,
timeout=None,
) -> Agent:
multiagent = _validate_create_multiagent(multiagent)
return await self._post(
_PREFIX,
body=dump_body(
Expand All @@ -255,6 +304,9 @@ async def create(
"multiagent": multiagent,
"metadata": metadata,
"tags": tags,
# AUTOGEN-START create-body
"context_management": context_management,
# AUTOGEN-END create-body
}
),
options=make_request_options(
Expand Down Expand Up @@ -293,6 +345,7 @@ async def retrieve(
async def list(
self,
*,
include_archived: bool | NotGiven = NOT_GIVEN,
limit=NOT_GIVEN,
page=NOT_GIVEN,
created_at_gte=NOT_GIVEN,
Expand All @@ -306,6 +359,7 @@ async def list(
_PREFIX,
options=make_request_options(
query=_list_query(
include_archived=include_archived,
limit=limit,
page=page,
created_at_gte=created_at_gte,
Expand Down Expand Up @@ -333,6 +387,9 @@ async def update(
skills=NOT_GIVEN,
multiagent=NOT_GIVEN,
metadata=NOT_GIVEN,
# AUTOGEN-START update-kwargs
context_management: Optional[ContextManagementPoliciesParam] | NotGiven = NOT_GIVEN,
# AUTOGEN-END update-kwargs
extra_headers=None,
extra_query=None,
extra_body=None,
Expand All @@ -354,6 +411,9 @@ async def update(
"skills": skills,
"multiagent": multiagent,
"metadata": metadata,
# AUTOGEN-START update-body
"context_management": context_management,
# AUTOGEN-END update-body
}
),
options=make_request_options(
Expand Down Expand Up @@ -400,3 +460,15 @@ async def list_versions(
),
cast_to=ListAgentsResponse,
)

async def archive(self, agent_id: str, *, extra_headers=None, extra_query=None, timeout=None) -> Agent:
"""Archive this agent; the POST has no request body."""
if not agent_id:
raise ValueError("agent_id is required")
return await self._post(
f"{_PREFIX}/{quote(agent_id, safe='')}/archive",
# Raw empty bytes bypass the client's default empty JSON object.
body=b"", # type: ignore[arg-type]
options=make_request_options(extra_headers=extra_headers, extra_query=extra_query, timeout=timeout),
cast_to=Agent,
)
53 changes: 48 additions & 5 deletions src/arkruntime/resources/environments/environments.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from typing import Optional
from urllib.parse import quote

import httpx

Expand All @@ -21,12 +22,14 @@
_ENV_PREFIX = "/environments"


def _list_query(*, limit=NOT_GIVEN, page=NOT_GIVEN) -> dict:
def _list_query(*, include_archived: bool | NotGiven = NOT_GIVEN, limit=NOT_GIVEN, page=NOT_GIVEN) -> dict:
q: dict = {}
if limit is not NOT_GIVEN and limit is not None:
q["limit"] = limit
if page is not NOT_GIVEN and page is not None:
q["page"] = page
if include_archived is not NOT_GIVEN and include_archived is not None:
q["include_archived"] = include_archived
return q


Expand Down Expand Up @@ -73,12 +76,20 @@ def retrieve(
)

def list(
self, *, limit=NOT_GIVEN, page=NOT_GIVEN, extra_headers=None, extra_query=None, extra_body=None, timeout=None
self,
*,
include_archived: bool | NotGiven = NOT_GIVEN,
limit=NOT_GIVEN,
page=NOT_GIVEN,
extra_headers=None,
extra_query=None,
extra_body=None,
timeout=None,
) -> ListEnvironmentsResponse:
return self._get(
_ENV_PREFIX,
options=make_request_options(
query=_list_query(limit=limit, page=page),
query=_list_query(include_archived=include_archived, limit=limit, page=page),
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
Expand Down Expand Up @@ -127,6 +138,18 @@ def delete(
cast_to=DeleteEnvironmentResponse,
)

def archive(self, environment_id: str, *, extra_headers=None, extra_query=None, timeout=None) -> Environment:
"""Archive this environment; the POST has no request body."""
if not environment_id:
raise ValueError("environment_id is required")
return self._post(
f"{_ENV_PREFIX}/{quote(environment_id, safe='')}/archive",
# Raw empty bytes bypass the client's default empty JSON object.
body=b"", # type: ignore[arg-type]
options=make_request_options(extra_headers=extra_headers, extra_query=extra_query, timeout=timeout),
cast_to=Environment,
)


class AsyncEnvironments(AsyncAPIResource):
@cached_property
Expand Down Expand Up @@ -171,12 +194,20 @@ async def retrieve(
)

async def list(
self, *, limit=NOT_GIVEN, page=NOT_GIVEN, extra_headers=None, extra_query=None, extra_body=None, timeout=None
self,
*,
include_archived: bool | NotGiven = NOT_GIVEN,
limit=NOT_GIVEN,
page=NOT_GIVEN,
extra_headers=None,
extra_query=None,
extra_body=None,
timeout=None,
) -> ListEnvironmentsResponse:
return await self._get(
_ENV_PREFIX,
options=make_request_options(
query=_list_query(limit=limit, page=page),
query=_list_query(include_archived=include_archived, limit=limit, page=page),
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
Expand Down Expand Up @@ -224,3 +255,15 @@ async def delete(
),
cast_to=DeleteEnvironmentResponse,
)

async def archive(self, environment_id: str, *, extra_headers=None, extra_query=None, timeout=None) -> Environment:
"""Archive this environment; the POST has no request body."""
if not environment_id:
raise ValueError("environment_id is required")
return await self._post(
f"{_ENV_PREFIX}/{quote(environment_id, safe='')}/archive",
# Raw empty bytes bypass the client's default empty JSON object.
body=b"", # type: ignore[arg-type]
options=make_request_options(extra_headers=extra_headers, extra_query=extra_query, timeout=timeout),
cast_to=Environment,
)
Loading
Loading