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
13 changes: 12 additions & 1 deletion src/arkruntime/resources/agents/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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

Expand Down Expand Up @@ -42,6 +43,14 @@ def _list_query(
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 @@ -61,6 +70,7 @@ def create(
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 Down Expand Up @@ -151,7 +161,7 @@ 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,
extra_headers=None,
extra_query=None,
Expand Down Expand Up @@ -241,6 +251,7 @@ async def create(
extra_body=None,
timeout=None,
) -> Agent:
multiagent = _validate_create_multiagent(multiagent)
return await self._post(
_PREFIX,
body=dump_body(
Expand Down
10 changes: 8 additions & 2 deletions src/arkruntime/resources/sessions/session_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,10 @@ def stream(
client = self._client._client # httpx.Client
base = str(self._client._base_url).rstrip("/")
url = f"{base}/sessions/{session_id}/events/stream"
headers = {"Accept": "text/event-stream", **(self._client.auth_headers or {})}
headers = {
**self._client.default_headers(),
"Accept": "text/event-stream",
}
with client.stream("GET", url, headers=headers, timeout=timeout) as resp:
_raise_for_stream_status(self, resp)
event_name = ""
Expand Down Expand Up @@ -312,7 +315,10 @@ async def stream(
client = self._client._client # httpx.AsyncClient
base = str(self._client._base_url).rstrip("/")
url = f"{base}/sessions/{session_id}/events/stream"
headers = {"Accept": "text/event-stream", **(self._client.auth_headers or {})}
headers = {
**self._client.default_headers(),
"Accept": "text/event-stream",
}
async with client.stream("GET", url, headers=headers, timeout=timeout) as resp:
await _araise_for_stream_status(self, resp)
event_name = ""
Expand Down
10 changes: 8 additions & 2 deletions src/arkruntime/resources/sessions/session_threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ def stream(
client = self._client._client
base = str(self._client._base_url).rstrip("/")
url = f"{base}/sessions/{session_id}/threads/{thread_id}/stream"
headers = {"Accept": "text/event-stream", **(self._client.auth_headers or {})}
headers = {
**self._client.default_headers(),
"Accept": "text/event-stream",
}
with client.stream("GET", url, headers=headers, timeout=timeout) as resp:
_raise_for_stream_status(self, resp)
event_name = ""
Expand Down Expand Up @@ -251,7 +254,10 @@ async def stream(
client = self._client._client
base = str(self._client._base_url).rstrip("/")
url = f"{base}/sessions/{session_id}/threads/{thread_id}/stream"
headers = {"Accept": "text/event-stream", **(self._client.auth_headers or {})}
headers = {
**self._client.default_headers(),
"Accept": "text/event-stream",
}
async with client.stream("GET", url, headers=headers, timeout=timeout) as resp:
await _araise_for_stream_status(self, resp)
event_name = ""
Expand Down
Loading
Loading