diff --git a/README.md b/README.md index 74b1a68..03b0efc 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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; diff --git a/VERSION b/VERSION index c5523bd..6633391 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.17.0 +0.18.0 diff --git a/pyproject.toml b/pyproject.toml index 851d222..b8d6d7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/arkruntime/resources/agents/agents.py b/src/arkruntime/resources/agents/agents.py index 51887e6..b049c9c 100644 --- a/src/arkruntime/resources/agents/agents.py +++ b/src/arkruntime/resources/agents/agents.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import List, Optional +from urllib.parse import quote import httpx @@ -8,16 +9,22 @@ 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" @@ -25,6 +32,7 @@ def _list_query( *, + include_archived: bool | NotGiven = NOT_GIVEN, limit=NOT_GIVEN, page=NOT_GIVEN, created_at_gte=NOT_GIVEN, @@ -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, @@ -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( @@ -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( @@ -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, @@ -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, @@ -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, @@ -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( @@ -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( @@ -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( @@ -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( @@ -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, @@ -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, @@ -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, @@ -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( @@ -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, + ) diff --git a/src/arkruntime/resources/environments/environments.py b/src/arkruntime/resources/environments/environments.py index ad184ca..91d8779 100644 --- a/src/arkruntime/resources/environments/environments.py +++ b/src/arkruntime/resources/environments/environments.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Optional +from urllib.parse import quote import httpx @@ -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 @@ -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, @@ -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 @@ -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, @@ -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, + ) diff --git a/src/arkruntime/resources/images/images.py b/src/arkruntime/resources/images/images.py index 91379cf..1740ef7 100644 --- a/src/arkruntime/resources/images/images.py +++ b/src/arkruntime/resources/images/images.py @@ -17,6 +17,7 @@ from ...types.images.background import Background from ...types.images.image_generation_response import ImageGenerationResponse from ...types.images.image_generation_stream_event import ImageGenerationStreamEvent +from ...types.images.layer_decomposition_options_param import LayerDecompositionOptionsParam from ...types.images.optimize_prompt_options_param import OptimizePromptOptionsParam from ...types.images.output_format import OutputFormat from ...types.images.response_format import ResponseFormat @@ -37,6 +38,7 @@ def generate( *, # AUTOGEN-START create-kwargs model: str, + safety_identifier: Optional[str] = None, prompt: Optional[str] = None, image: Optional[Union[str, List[str]]] = None, stream: Optional[bool] = None, @@ -53,6 +55,7 @@ def generate( output_format: Optional[OutputFormat] = None, background: Optional[Background] = None, layer_decomposition: Optional[bool] = None, + layer_decomposition_options: Optional[LayerDecompositionOptionsParam] = None, # AUTOGEN-END create-kwargs extra_headers: Headers | None = None, extra_query: Query | None = None, @@ -64,6 +67,7 @@ def generate( body={ # AUTOGEN-START create-body "model": model, + "safety_identifier": safety_identifier, "prompt": prompt, "image": image, "stream": stream, @@ -80,6 +84,7 @@ def generate( "output_format": output_format, "background": background, "layer_decomposition": layer_decomposition, + "layer_decomposition_options": layer_decomposition_options, # AUTOGEN-END create-body }, options=make_request_options( @@ -100,6 +105,7 @@ async def generate( *, # AUTOGEN-START create-kwargs model: str, + safety_identifier: Optional[str] = None, prompt: Optional[str] = None, image: Optional[Union[str, List[str]]] = None, stream: Optional[bool] = None, @@ -116,6 +122,7 @@ async def generate( output_format: Optional[OutputFormat] = None, background: Optional[Background] = None, layer_decomposition: Optional[bool] = None, + layer_decomposition_options: Optional[LayerDecompositionOptionsParam] = None, # AUTOGEN-END create-kwargs extra_headers: Headers | None = None, extra_query: Query | None = None, @@ -127,6 +134,7 @@ async def generate( body={ # AUTOGEN-START create-body "model": model, + "safety_identifier": safety_identifier, "prompt": prompt, "image": image, "stream": stream, @@ -143,6 +151,7 @@ async def generate( "output_format": output_format, "background": background, "layer_decomposition": layer_decomposition, + "layer_decomposition_options": layer_decomposition_options, # AUTOGEN-END create-body }, options=make_request_options( diff --git a/src/arkruntime/resources/sessions/session_events.py b/src/arkruntime/resources/sessions/session_events.py index d796a5a..b46aea7 100644 --- a/src/arkruntime/resources/sessions/session_events.py +++ b/src/arkruntime/resources/sessions/session_events.py @@ -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 = "" @@ -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 = "" diff --git a/src/arkruntime/resources/sessions/session_threads.py b/src/arkruntime/resources/sessions/session_threads.py index e37389c..38b0253 100644 --- a/src/arkruntime/resources/sessions/session_threads.py +++ b/src/arkruntime/resources/sessions/session_threads.py @@ -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 = "" @@ -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 = "" diff --git a/src/arkruntime/resources/sessions/sessions.py b/src/arkruntime/resources/sessions/sessions.py index be1e73b..aa45771 100644 --- a/src/arkruntime/resources/sessions/sessions.py +++ b/src/arkruntime/resources/sessions/sessions.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import List, Optional +from urllib.parse import quote from ..._base_client import make_request_options from ..._compat import cached_property @@ -30,6 +31,7 @@ def _list_query( *, + include_archived: bool | NotGiven = NOT_GIVEN, agent_id=NOT_GIVEN, agent_version=NOT_GIVEN, created_at_gt=NOT_GIVEN, @@ -58,6 +60,8 @@ 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 @@ -133,6 +137,7 @@ def retrieve( def list( self, *, + include_archived: bool | NotGiven = NOT_GIVEN, agent_id=NOT_GIVEN, agent_version=NOT_GIVEN, created_at_gt=NOT_GIVEN, @@ -153,6 +158,7 @@ def list( _PREFIX, options=make_request_options( query=_list_query( + include_archived=include_archived, agent_id=agent_id, agent_version=agent_version, created_at_gt=created_at_gt, @@ -250,6 +256,18 @@ def delete( cast_to=DeleteSessionResponse, ) + def archive(self, session_id: str, *, extra_headers=None, extra_query=None, timeout=None) -> Session: + """Archive this session; the POST has no request body.""" + if not session_id: + raise ValueError("session_id is required") + return self._post( + f"{_PREFIX}/{quote(session_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=Session, + ) + class AsyncSessions(AsyncAPIResource): @cached_property @@ -316,6 +334,7 @@ async def retrieve( async def list( self, *, + include_archived: bool | NotGiven = NOT_GIVEN, agent_id=NOT_GIVEN, agent_version=NOT_GIVEN, created_at_gt=NOT_GIVEN, @@ -336,6 +355,7 @@ async def list( _PREFIX, options=make_request_options( query=_list_query( + include_archived=include_archived, agent_id=agent_id, agent_version=agent_version, created_at_gt=created_at_gt, @@ -432,3 +452,15 @@ async def delete( ), cast_to=DeleteSessionResponse, ) + + async def archive(self, session_id: str, *, extra_headers=None, extra_query=None, timeout=None) -> Session: + """Archive this session; the POST has no request body.""" + if not session_id: + raise ValueError("session_id is required") + return await self._post( + f"{_PREFIX}/{quote(session_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=Session, + ) diff --git a/src/arkruntime/selfhosted/session_tool_runner.py b/src/arkruntime/selfhosted/session_tool_runner.py index aa64dc2..0fe9bec 100644 --- a/src/arkruntime/selfhosted/session_tool_runner.py +++ b/src/arkruntime/selfhosted/session_tool_runner.py @@ -52,6 +52,7 @@ STREAM_BACKOFF_START = 0.5 STREAM_BACKOFF_CAP = 10.0 STREAM_HEALTHY_AFTER = 30.0 +STREAM_PROGRESS_WARN_SECONDS = 60.0 SEND_RETRIES = 3 STREAM_QUEUE_SIZE = 256 @@ -98,6 +99,7 @@ class SessionToolRunnerOptions: stop_event: Any = None logger: logging.Logger = logging.getLogger("arkruntime.selfhosted.session_tool_runner") on_tool_error: Any = None + initial_session_status: str = "" @dataclass @@ -149,6 +151,15 @@ def _is_stopped(self) -> bool: return self._stop.is_set() or bool(self.options.stop_event and self.options.stop_event.is_set()) def run(self) -> List[ToolCallResult]: + mode = "stream" if self.options.prefer_stream and hasattr(self.api, "stream_events") else "list" + self.options.logger.info( + "session runner started session_id=%s work_id=%s mode=%s max_idle_seconds=%s stream_timeout_seconds=%.3fs", + self.session_id, + self.options.work_id, + mode, + self.options.max_idle_seconds, + self.options.stream_timeout_seconds, + ) try: if self.options.result_store is not None: pending, processed = self.options.result_store.recover() @@ -161,7 +172,11 @@ def run(self) -> List[ToolCallResult]: self._consume_stream_loop() return self._results except EventStreamUnsupported: - pass + mode = "list" + self.options.logger.info( + "session stream unsupported session_id=%s falling_back=list", + self.session_id, + ) except (IdleTimeout, SessionTerminated): raise except Exception: @@ -172,19 +187,38 @@ def run(self) -> List[ToolCallResult]: return self._results finally: self._state.cancel_active_execution() + self.options.logger.info( + "session runner stopped session_id=%s work_id=%s mode=%s events=%d stream_attempts=%d", + self.session_id, + self.options.work_id, + mode, + self._state.event_count, + self._state.stream_attempts, + ) def _consume_stream_loop(self) -> None: backoff = STREAM_BACKOFF_START while not self._is_stopped(): + self._state.stream_attempts += 1 + attempt = self._state.stream_attempts + self.options.logger.info( + "session stream connecting session_id=%s work_id=%s attempt=%d timeout=%.3fs", + self.session_id, + self.options.work_id, + attempt, + self.options.stream_timeout_seconds, + ) event_queue: "queue.Queue[object]" = queue.Queue(maxsize=STREAM_QUEUE_SIZE) opened_at = time.monotonic() pump = threading.Thread(target=self._pump_stream, args=(event_queue,), daemon=True) pump.start() self._state.reconcile() + reconnect_error: Optional[BaseException] = None while not self._is_stopped() and (pump.is_alive() or not event_queue.empty()): self._state.drain_execution_done() self._state.flush_results() self._raise_if_idle_expired() + self._state.maybe_log_no_progress() try: item = event_queue.get(timeout=self._state.next_wait_seconds(0.5)) except queue.Empty: @@ -196,9 +230,30 @@ def _consume_stream_loop(self) -> None: raise item if is_fatal_4xx(item): raise item + reconnect_error = item break self._state.handle_stream_event(item) # type: ignore[arg-type] - self._sleep_or_idle(_jitter(backoff)) + if self._is_stopped(): + return + reconnect_in = _jitter(backoff) + if reconnect_error is None: + self.options.logger.info( + "session stream ended session_id=%s work_id=%s attempt=%d reconnect_in=%.3fs", + self.session_id, + self.options.work_id, + attempt, + reconnect_in, + ) + else: + self.options.logger.warning( + "session stream disconnected session_id=%s work_id=%s attempt=%d reconnect_in=%.3fs err=%s", + self.session_id, + self.options.work_id, + attempt, + reconnect_in, + reconnect_error, + ) + self._sleep_or_idle(reconnect_in) backoff = min(backoff * 2, STREAM_BACKOFF_CAP) def _pump_stream(self, event_queue: "queue.Queue[object]") -> None: @@ -233,6 +288,7 @@ def _consume_list(self) -> None: self._state.reconcile(reconcile=False) self._state.flush_results() self._raise_if_idle_expired() + self._state.maybe_log_no_progress() self._sleep_or_idle(self.options.event_poll_interval_seconds) def _sleep_or_idle(self, seconds: float) -> None: @@ -240,6 +296,7 @@ def _sleep_or_idle(self, seconds: float) -> None: while not self._is_stopped(): self._state.drain_execution_done() self._raise_if_idle_expired() + self._state.maybe_log_no_progress() remaining = deadline - time.monotonic() if remaining <= 0: return @@ -258,6 +315,13 @@ class _RunnerState: def __init__(self, runner: SessionToolRunner) -> None: self.runner = runner self.page = runner.options.event_page + self.event_count = 0 + self.business_event_count = 0 + self.stream_attempts = 0 + self.session_status = runner.options.initial_session_status.strip().lower() + self.last_event_at = time.monotonic() + self.last_business_event_at = time.monotonic() + self.last_progress_warning_at = 0.0 self.processed: Dict[str, bool] = {} self.seen: Dict[str, bool] = {} self.answered: Dict[str, bool] = {} @@ -266,7 +330,9 @@ def __init__(self, runner: SessionToolRunner) -> None: self.pending_ask: Dict[str, Event] = {} self.confirmations: Dict[str, Event] = {} self.external_tools: Dict[str, Event] = {} + self.unposted_tool_use_results: Set[str] = set() self.tool_use_events: Dict[str, Event] = {} + self.tool_use_call_id_by_event_id: Dict[str, str] = {} self.scheduled: Set[str] = set() self.execution_queue: Deque[_PendingToolEvent] = deque() self.active_execution: Optional[_ActiveToolExecution] = None @@ -274,6 +340,8 @@ def __init__(self, runner: SessionToolRunner) -> None: self.session_tool_uses: Set[str] = set() self.tool_uses_since_status: Set[str] = set() self.blocking_event_ids: Set[str] = set() + self.pending_blocking_event_ids: Set[str] = set() + self.requires_action_fallback: Set[str] = set() self.blocking_events_known = False self.idle_armed_at = 0.0 self.idle_arm_pending = False @@ -344,6 +412,7 @@ def process_listed_events(self, events: Iterable[Event], reconcile: bool = False pending_ids[call_id] = True elif event.type in (EVENT_TYPE_SESSION_STATUS_TERMINATED, EVENT_TYPE_SESSION_DELETED): raise SessionTerminated("session terminated") + self.reconcile_unmatched_requires_action() self.reconcile_recovered_results() if touched_idle: self.disarm_idle() @@ -394,11 +463,44 @@ def mark_event_seen(self, event: Event) -> bool: if self.seen.get(key): return False self.seen[key] = True + self.record_event(event) return True + def record_event(self, event: Event) -> None: + self.event_count += 1 + now = time.monotonic() + self.last_event_at = now + is_business_event = not event.type.startswith("span.") + if is_business_event: + self.business_event_count += 1 + self.last_business_event_at = now + self.last_progress_warning_at = 0.0 + if self.business_event_count == 1: + self.runner.options.logger.info( + "session first business event session_id=%s work_id=%s event_type=%s event_id=%s", + self.runner.session_id, + self.runner.options.work_id, + event.type, + event.id, + ) + else: + self.runner.options.logger.debug( + "session event received session_id=%s work_id=%s event_type=%s event_id=%s", + self.runner.session_id, + self.runner.options.work_id, + event.type, + event.id, + ) + def mark_answered(self, call_id: str) -> None: if not call_id: return + event = self.tool_use_events.get(call_id) + if event is not None and event.id: + self.pending_blocking_event_ids.discard(event.id) + self.tool_use_call_id_by_event_id.pop(event.id, None) + self.requires_action_fallback.discard(call_id) + self.pending_blocking_event_ids.discard(call_id) self.answered[call_id] = True self.processed[call_id] = True self.pending_results.pop(call_id, None) @@ -435,12 +537,15 @@ def handle_tool_use(self, event: Event, custom: bool) -> None: if not self.owns_tool(event, custom): self.external_tools[call_id] = event self.maybe_arm_pending_idle() - self.runner._results.append(ToolCallResult(call_id, event.name, custom, posted=False, event=event)) + self.record_unposted_tool_use_result_once(call_id, event, custom) return confirmation, allowed = self.permission_allows(event, custom, call_id) if not allowed: - self.runner._results.append( - ToolCallResult(call_id, event.name, custom, confirmation=confirmation, posted=False, event=event) + self.record_unposted_tool_use_result_once( + call_id, + event, + custom, + confirmation=confirmation, ) return if self.runner.options.result_store is not None: @@ -456,6 +561,23 @@ def handle_tool_use(self, event: Event, custom: bool) -> None: self.execution_queue.append(_PendingToolEvent(event, custom, confirmation)) self.start_next_tool_execution() + def record_unposted_tool_use_result_once( + self, call_id: str, event: Event, custom: bool, confirmation: str = "" + ) -> None: + if call_id in self.unposted_tool_use_results: + return + self.unposted_tool_use_results.add(call_id) + self.runner._results.append( + ToolCallResult( + tool_use_id=call_id, + name=event.name, + custom=custom, + confirmation=confirmation, + posted=False, + event=event, + ) + ) + def start_next_tool_execution(self) -> None: if self.active_execution is not None: return @@ -658,6 +780,7 @@ def send_result(self, call_id: str, event: Event, custom: bool, confirmation: st result=out, ) ) + self.maybe_arm_pending_idle() def retry_send_event(self, event: Event, call_id: str) -> bool: last_exc: Optional[BaseException] = None @@ -700,30 +823,149 @@ def observe_session_state(self, event: Event) -> None: self.session_tool_uses.add(call_id) if not self.is_answered(call_id): self.tool_use_events[call_id] = event + if event.id: + self.tool_use_call_id_by_event_id[event.id] = call_id self.tool_uses_since_status.add(call_id) return if event.type == EVENT_TYPE_SESSION_STATUS_IDLE: + self.set_session_status("idle", event) self.blocking_events_known = True self.blocking_event_ids = set() if event.stop_reason_type() == SESSION_STOP_REASON_REQUIRES_ACTION: - self.blocking_event_ids.update(event.stop_reason_event_ids()) + event_ids = set(event.stop_reason_event_ids()) + self.blocking_event_ids.update(event_ids) + self.pending_blocking_event_ids.update(event_ids) + for event_id in event_ids: + call_id = self.tool_use_call_id_by_event_id.get(event_id) + if call_id: + self.blocking_event_ids.add(call_id) + self.pending_blocking_event_ids.add(call_id) + else: + self.clear_unmatched_pending_blockers() self.tool_uses_since_status.clear() return if event.type in (EVENT_TYPE_SESSION_STATUS_RUNNING, EVENT_TYPE_SESSION_STATUS_RESCHEDULED): + self.set_session_status("running", event) self.blocking_events_known = True - self.blocking_event_ids.clear() - self.tool_uses_since_status.clear() + self.clear_resolved_pending_blockers() + elif event.type in (EVENT_TYPE_SESSION_STATUS_TERMINATED, EVENT_TYPE_SESSION_DELETED): + self.set_session_status("terminated", event) + self.clear_pending_blockers() + + def clear_resolved_pending_blockers(self) -> None: + for call_id, tool_event in self.tool_use_events.items(): + if call_id not in self.pending_blocking_event_ids and ( + not tool_event.id or tool_event.id not in self.pending_blocking_event_ids + ): + continue + self.pending_blocking_event_ids.discard(call_id) + if tool_event.id: + self.pending_blocking_event_ids.discard(tool_event.id) + self.tool_use_call_id_by_event_id.pop(tool_event.id, None) + self.blocking_event_ids.clear() + self.tool_uses_since_status.clear() + + def clear_unmatched_pending_blockers(self) -> None: + matched = set() + for call_id, tool_event in self.tool_use_events.items(): + if call_id in self.pending_blocking_event_ids or ( + tool_event.id and tool_event.id in self.pending_blocking_event_ids + ): + matched.add(call_id) + if tool_event.id: + matched.add(tool_event.id) + self.pending_blocking_event_ids.intersection_update(matched) + + def clear_pending_blockers(self) -> None: + self.blocking_event_ids.clear() + self.pending_blocking_event_ids.clear() + self.tool_use_call_id_by_event_id.clear() + self.tool_uses_since_status.clear() + + def set_session_status(self, status: str, event: Event) -> None: + if status == self.session_status: + return + previous = self.session_status or "unknown" + self.session_status = status + if status == "running": + self.last_business_event_at = time.monotonic() + self.last_progress_warning_at = 0.0 + self.runner.options.logger.info( + "session status changed session_id=%s work_id=%s previous=%s current=%s event_type=%s stop_reason=%s", + self.runner.session_id, + self.runner.options.work_id, + previous, + status, + event.type, + event.stop_reason_type() if status == "idle" else "", + ) + + def maybe_log_no_progress(self) -> None: + if self.session_status != "running": + return + now = time.monotonic() + no_progress_seconds = now - self.last_business_event_at + if no_progress_seconds < STREAM_PROGRESS_WARN_SECONDS: + return + if self.last_progress_warning_at and now - self.last_progress_warning_at < STREAM_PROGRESS_WARN_SECONDS: + return + self.last_progress_warning_at = now + self.runner.options.logger.warning( + "session has no business progress session_id=%s work_id=%s status=%s " + "no_progress_seconds=%.1f events=%d stream_attempts=%d", + self.runner.session_id, + self.runner.options.work_id, + self.session_status, + no_progress_seconds, + self.event_count, + self.stream_attempts, + ) def should_handle_tool_use(self, call_id: str) -> bool: if not self.blocking_events_known: return True - return call_id in self.blocking_event_ids or call_id in self.tool_uses_since_status + return self.is_blocking_tool_use(call_id) or call_id in self.tool_uses_since_status + + def is_blocking_tool_use(self, call_id: str) -> bool: + if call_id in self.requires_action_fallback: + return True + # StopReason.EventIDs 里是 agent.tool_use 的事件 id,而 runner 内部以 + # tool_use_id 作为 call id,两者不能直接互相查表。 + if call_id in self.blocking_event_ids or call_id in self.pending_blocking_event_ids: + return True + event = self.tool_use_events.get(call_id) + return event is not None and ( + event.id in self.blocking_event_ids or event.id in self.pending_blocking_event_ids + ) + + def reconcile_unmatched_requires_action(self) -> None: + if not self.blocking_events_known or not self.pending_blocking_event_ids: + return + for call_id, event in self.tool_use_events.items(): + if call_id in self.pending_blocking_event_ids or (event.id and event.id in self.pending_blocking_event_ids): + return + candidates = [] + for call_id, event in self.tool_use_events.items(): + if self.is_answered(call_id) or call_id in self.scheduled: + continue + if not self.owns_tool(event, event.type == EVENT_TYPE_AGENT_CUSTOM_TOOL_USE): + continue + candidates.append(call_id) + if len(candidates) != 1: + return + call_id = candidates[0] + if call_id not in self.requires_action_fallback: + self.runner.options.logger.warning( + "using sole unresolved tool use for unmatched requires_action tool_use_id=%s", + call_id, + ) + self.requires_action_fallback.add(call_id) def reconcile_recovered_results(self) -> None: if not self.blocking_events_known: return for call_id in list(self.recovered_results): - if call_id in self.blocking_event_ids and call_id in self.session_tool_uses: + if self.is_blocking_tool_use(call_id) and call_id in self.session_tool_uses: self.recovered_results.discard(call_id) continue if call_id in self.tool_uses_since_status: @@ -763,7 +1005,7 @@ def maybe_arm_pending_idle(self) -> None: self.idle_armed_at = time.monotonic() def has_idle_blockers(self) -> bool: - return bool(self.pending_ask or self.pending_results or self.external_tools or self.scheduled) + return bool(self.pending_ask or self.external_tools or self.scheduled) def idle_expired(self) -> bool: max_idle = self.max_idle_seconds() diff --git a/src/arkruntime/selfhosted/worker.py b/src/arkruntime/selfhosted/worker.py index 827938c..a67011a 100644 --- a/src/arkruntime/selfhosted/worker.py +++ b/src/arkruntime/selfhosted/worker.py @@ -212,6 +212,8 @@ class EnvironmentWorkerOptions: custom_tools: Dict[str, Tool] = field(default_factory=dict) logger: logging.Logger = logging.getLogger("arkruntime.selfhosted.environment_worker") tool_timeout_seconds: Optional[float] = None + prefer_stream: bool = True + stream_timeout_seconds: float = 30.0 class EnvironmentWorker: @@ -309,8 +311,11 @@ def _handle_item(self, work: _ClaimedWork) -> None: result_store=store, max_idle_seconds=self.options.max_idle_seconds, tool_timeout_seconds=self.options.tool_timeout_seconds, + prefer_stream=self.options.prefer_stream, + stream_timeout_seconds=self.options.stream_timeout_seconds, stop_event=work_stop, logger=self.options.logger, + initial_session_status=str(session.raw.get("status") or ""), ), ) runner.run() diff --git a/src/arkruntime/types/agent/__init__.py b/src/arkruntime/types/agent/__init__.py index 8a62c58..e4d03a3 100644 --- a/src/arkruntime/types/agent/__init__.py +++ b/src/arkruntime/types/agent/__init__.py @@ -10,6 +10,12 @@ from .agent_ref import AgentRef from .agent_ref_type import AgentRefType from .agent_skill_ref import AgentSkillRef +from .compact_threshold import CompactThreshold +from .context_management_instructions import ContextManagementInstructions +from .context_management_policies import ContextManagementPolicies +from .context_management_policies_item import ContextManagementPoliciesItem +from .context_management_policy import ContextManagementPolicy +from .context_management_type import ContextManagementType from .create_agent_request import CreateAgentRequest from .custom_tool_input_schema import CustomToolInputSchema from .delete_agent_response import DeleteAgentResponse @@ -19,6 +25,10 @@ from .model_config import ModelConfig from .model_speed import ModelSpeed from .multiagent_config import MultiagentConfig +from .multiagent_feature_type import MultiagentFeatureType +from .multiagent_inline_agents_config import MultiagentInlineAgentsConfig +from .multiagent_patch import MultiagentPatch +from .multiagent_subagents_config import MultiagentSubagentsConfig from .multiagent_type import MultiagentType from .permission_policy import PermissionPolicy from .permission_policy_type import PermissionPolicyType @@ -35,6 +45,12 @@ "AgentRef", "AgentRefType", "AgentSkillRef", + "CompactThreshold", + "ContextManagementInstructions", + "ContextManagementPolicies", + "ContextManagementPoliciesItem", + "ContextManagementPolicy", + "ContextManagementType", "CreateAgentRequest", "CustomToolInputSchema", "DeleteAgentResponse", @@ -44,6 +60,10 @@ "ModelConfig", "ModelSpeed", "MultiagentConfig", + "MultiagentFeatureType", + "MultiagentInlineAgentsConfig", + "MultiagentPatch", + "MultiagentSubagentsConfig", "MultiagentType", "PermissionPolicy", "PermissionPolicyType", @@ -55,6 +75,30 @@ "ToolItem", "UpdateAgentRequest", ] +from .agent_ref_param import AgentRefParam +from .agent_skill_ref_param import AgentSkillRefParam +from .compact_threshold_param import CompactThresholdParam +from .context_management_instructions_param import ContextManagementInstructionsParam +from .context_management_policies_item_param import ContextManagementPoliciesItemParam +from .context_management_policies_param import ContextManagementPoliciesParam +from .context_management_policy_param import ContextManagementPolicyParam +from .context_management_type_param import ContextManagementTypeParam +from .create_agent_request_param import CreateAgentRequestParam +from .custom_tool_input_schema_param import CustomToolInputSchemaParam +from .mcp_server_param import MCPServerParam +from .mcp_server_type_param import MCPServerTypeParam +from .model_config_param import ModelConfigParam +from .multiagent_config_param import MultiagentConfigParam +from .multiagent_inline_agents_config_param import MultiagentInlineAgentsConfigParam +from .multiagent_patch_param import MultiagentPatchParam +from .multiagent_subagents_config_param import MultiagentSubagentsConfigParam +from .permission_policy_param import PermissionPolicyParam +from .tag_param import TagParam +from .token_limits_param import TokenLimitsParam +from .tool_config_param import ToolConfigParam +from .tool_default_config_param import ToolDefaultConfigParam +from .tool_item_param import ToolItemParam +from .update_agent_request_param import UpdateAgentRequestParam # Hand-written extras (preserved across regen via Makefile rsync --exclude=*_shim.py). from ._init_extras_shim import * # noqa: F401,F403 diff --git a/src/arkruntime/types/agent/agent.py b/src/arkruntime/types/agent/agent.py index 2f5a4f0..7fba248 100644 --- a/src/arkruntime/types/agent/agent.py +++ b/src/arkruntime/types/agent/agent.py @@ -11,6 +11,7 @@ from arkruntime._models import BaseModel from .agent_skill_ref import AgentSkillRef +from .context_management_policies import ContextManagementPolicies from .mcp_server import MCPServer from .model_config import ModelConfig from .multiagent_config import MultiagentConfig @@ -80,6 +81,14 @@ class Agent(BaseModel): """ 展示名。 """ + context_management: Optional[ContextManagementPolicies] = None + """ + 上下文管理策略。服务端可能在公开响应中补展示默认;该默认不代表用户已显式配置。 + """ + archived_at: Optional[str] = None + """ + 首次归档时间(RFC 3339),只读;未归档时省略。归档作用于资源 ID。 + """ created_at: str """ RFC 3339 时间。 diff --git a/src/arkruntime/types/agent/agent_ref.py b/src/arkruntime/types/agent/agent_ref.py index fba8cb0..8a5148d 100644 --- a/src/arkruntime/types/agent/agent_ref.py +++ b/src/arkruntime/types/agent/agent_ref.py @@ -12,6 +12,7 @@ from .agent_ref_type import AgentRefType from .agent_skill_ref import AgentSkillRef +from .context_management_policies_item import ContextManagementPoliciesItem from .mcp_server import MCPServer from .model_config import ModelConfig from .tool_item import ToolItem @@ -62,6 +63,10 @@ class AgentRef(BaseModel): """ Session 响应中冻结的成员 Agent skills。 """ + context_management: Optional[ContextManagementPoliciesItem] = None + """ + Session 响应中冻结的成员 Agent 上下文管理策略。 + """ display_name: Optional[str] = None """ Session 响应中冻结的成员 Agent 展示名。 diff --git a/src/arkruntime/types/agent/agent_ref_param.py b/src/arkruntime/types/agent/agent_ref_param.py new file mode 100644 index 0000000..e2583a1 --- /dev/null +++ b/src/arkruntime/types/agent/agent_ref_param.py @@ -0,0 +1,60 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List, Optional + +from typing_extensions import Required, TypedDict + +from .agent_ref_type import AgentRefType +from .agent_skill_ref_param import AgentSkillRefParam +from .context_management_policies_item_param import ContextManagementPoliciesItemParam +from .mcp_server_param import MCPServerParam +from .model_config_param import ModelConfigParam +from .tool_item_param import ToolItemParam + + +class AgentRefParam(TypedDict, total=False): + """ + coordinator Agent 编排的下属 Agent 引用。 + """ + + type: Required[AgentRefType] + """引用类型。""" + + id: Optional[str] + """被引用 Agent 的 ID。""" + + version: Optional[int] + """被引用 Agent 的版本号。""" + + name: Optional[str] + """Session 响应中冻结的成员 Agent 名称。""" + + description: Optional[str] + """Session 响应中冻结的成员 Agent 描述。""" + + model: Optional[ModelConfigParam] + """Session 响应中冻结的成员 Agent 模型配置。""" + + system: Optional[str] + """Session 响应中冻结的成员 Agent system prompt。""" + + tools: Optional[List[ToolItemParam]] + """Session 响应中冻结的成员 Agent 工具配置。""" + + mcp_servers: Optional[List[MCPServerParam]] + """Session 响应中冻结的成员 Agent MCP servers。""" + + skills: Optional[List[AgentSkillRefParam]] + """Session 响应中冻结的成员 Agent skills。""" + + context_management: Optional[ContextManagementPoliciesItemParam] + """Session 响应中冻结的成员 Agent 上下文管理策略。""" + + display_name: Optional[str] + """Session 响应中冻结的成员 Agent 展示名。""" diff --git a/src/arkruntime/types/agent/agent_skill_ref_param.py b/src/arkruntime/types/agent/agent_skill_ref_param.py new file mode 100644 index 0000000..2f76312 --- /dev/null +++ b/src/arkruntime/types/agent/agent_skill_ref_param.py @@ -0,0 +1,31 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Optional + +from typing_extensions import Required, TypedDict + +from .skill_ref_type import SkillRefType + + +class AgentSkillRefParam(TypedDict, total=False): + """ + Agent 引用的一条 Skill。 + """ + + type: Required[SkillRefType] + """Skill 引用类型。一期仅支持 `skill_hub`。""" + + skill_id: Optional[str] + """Skill ID,形如 `skill-...`。""" + + version: Optional[str] + """Skill 版本号,可选;不传走最新。""" + + use_latest: Optional[bool] + """Session 快照中标识创建时用户选择的是使用最新版本。""" diff --git a/src/arkruntime/types/agent/compact_threshold.py b/src/arkruntime/types/agent/compact_threshold.py new file mode 100644 index 0000000..315a97f --- /dev/null +++ b/src/arkruntime/types/agent/compact_threshold.py @@ -0,0 +1,15 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pydantic import Field +from typing_extensions import Annotated, TypeAliasType + +CompactThreshold = TypeAliasType("CompactThreshold", Annotated[int, Field(ge=50000)]) +""" +上下文压缩阈值;显式配置时至少为 50,000 token。 +""" diff --git a/src/arkruntime/types/agent/compact_threshold_param.py b/src/arkruntime/types/agent/compact_threshold_param.py new file mode 100644 index 0000000..1d81fb1 --- /dev/null +++ b/src/arkruntime/types/agent/compact_threshold_param.py @@ -0,0 +1,15 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing_extensions import TypeAlias + + +CompactThresholdParam: TypeAlias = int +""" +上下文压缩阈值;显式配置时至少为 50,000 token。 +""" diff --git a/src/arkruntime/types/agent/context_management_instructions.py b/src/arkruntime/types/agent/context_management_instructions.py new file mode 100644 index 0000000..627a75c --- /dev/null +++ b/src/arkruntime/types/agent/context_management_instructions.py @@ -0,0 +1,17 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pydantic import Field +from typing_extensions import Annotated, TypeAliasType + +ContextManagementInstructions = TypeAliasType( + "ContextManagementInstructions", Annotated[str, Field(max_length=100000)] +) +""" +自定义摘要指令,最长 100,000 字符。 +""" diff --git a/src/arkruntime/types/agent/context_management_instructions_param.py b/src/arkruntime/types/agent/context_management_instructions_param.py new file mode 100644 index 0000000..53be056 --- /dev/null +++ b/src/arkruntime/types/agent/context_management_instructions_param.py @@ -0,0 +1,15 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing_extensions import TypeAlias + + +ContextManagementInstructionsParam: TypeAlias = str +""" +自定义摘要指令,最长 100,000 字符。 +""" diff --git a/src/arkruntime/types/agent/context_management_policies.py b/src/arkruntime/types/agent/context_management_policies.py new file mode 100644 index 0000000..cbc49a2 --- /dev/null +++ b/src/arkruntime/types/agent/context_management_policies.py @@ -0,0 +1,22 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List + +from pydantic import Field +from typing_extensions import Annotated, TypeAliasType + +from .context_management_policy import ContextManagementPolicy + +ContextManagementPolicies = TypeAliasType( + "ContextManagementPolicies", + Annotated[List[ContextManagementPolicy], Field(max_length=1)], +) +""" +Agent 上下文管理策略列表,当前最多一条策略。 +""" diff --git a/src/arkruntime/types/agent/context_management_policies_item.py b/src/arkruntime/types/agent/context_management_policies_item.py new file mode 100644 index 0000000..908df45 --- /dev/null +++ b/src/arkruntime/types/agent/context_management_policies_item.py @@ -0,0 +1,22 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List + +from pydantic import Field +from typing_extensions import Annotated, TypeAliasType + +from .context_management_policy import ContextManagementPolicy + +ContextManagementPoliciesItem = TypeAliasType( + "ContextManagementPoliciesItem", + Annotated[List[ContextManagementPolicy], Field(max_length=1)], +) +""" +Agent 上下文管理策略列表,当前最多一条策略。 +""" diff --git a/src/arkruntime/types/agent/context_management_policies_item_param.py b/src/arkruntime/types/agent/context_management_policies_item_param.py new file mode 100644 index 0000000..f451366 --- /dev/null +++ b/src/arkruntime/types/agent/context_management_policies_item_param.py @@ -0,0 +1,18 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List + +from typing_extensions import TypeAlias + +from .context_management_policy_param import ContextManagementPolicyParam + +ContextManagementPoliciesItemParam: TypeAlias = List[ContextManagementPolicyParam] +""" +Agent 上下文管理策略列表,当前最多一条策略。 +""" diff --git a/src/arkruntime/types/agent/context_management_policies_param.py b/src/arkruntime/types/agent/context_management_policies_param.py new file mode 100644 index 0000000..b2ad938 --- /dev/null +++ b/src/arkruntime/types/agent/context_management_policies_param.py @@ -0,0 +1,18 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List + +from typing_extensions import TypeAlias + +from .context_management_policy_param import ContextManagementPolicyParam + +ContextManagementPoliciesParam: TypeAlias = List[ContextManagementPolicyParam] +""" +Agent 上下文管理策略列表,当前最多一条策略。 +""" diff --git a/src/arkruntime/types/agent/context_management_policy.py b/src/arkruntime/types/agent/context_management_policy.py new file mode 100644 index 0000000..c347b32 --- /dev/null +++ b/src/arkruntime/types/agent/context_management_policy.py @@ -0,0 +1,34 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Optional + +from arkruntime._models import BaseModel + +from .compact_threshold import CompactThreshold +from .context_management_instructions import ContextManagementInstructions +from .context_management_type import ContextManagementType + + +class ContextManagementPolicy(BaseModel): + """ + Agent 上下文管理策略。当前最多配置一条 compaction 策略。 + """ + + type: ContextManagementType + """ + 策略类型,当前固定为 `compaction`。 + """ + compact_threshold: Optional[CompactThreshold] = None + """ + 触发压缩的上下文 token 阈值;省略时由运行时使用默认值。 + """ + instructions: Optional[ContextManagementInstructions] = None + """ + 自定义摘要指令;省略时由运行时使用内置提示词。 + """ diff --git a/src/arkruntime/types/agent/context_management_policy_param.py b/src/arkruntime/types/agent/context_management_policy_param.py new file mode 100644 index 0000000..bdb1626 --- /dev/null +++ b/src/arkruntime/types/agent/context_management_policy_param.py @@ -0,0 +1,30 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Optional + +from typing_extensions import Required, TypedDict + +from .compact_threshold_param import CompactThresholdParam +from .context_management_instructions_param import ContextManagementInstructionsParam +from .context_management_type_param import ContextManagementTypeParam + + +class ContextManagementPolicyParam(TypedDict, total=False): + """ + Agent 上下文管理策略。当前最多配置一条 compaction 策略。 + """ + + type: Required[ContextManagementTypeParam] + """策略类型,当前固定为 `compaction`。""" + + compact_threshold: Optional[CompactThresholdParam] + """触发压缩的上下文 token 阈值;省略时由运行时使用默认值。""" + + instructions: Optional[ContextManagementInstructionsParam] + """自定义摘要指令;省略时由运行时使用内置提示词。""" diff --git a/src/arkruntime/types/agent/context_management_type.py b/src/arkruntime/types/agent/context_management_type.py new file mode 100644 index 0000000..f9a8306 --- /dev/null +++ b/src/arkruntime/types/agent/context_management_type.py @@ -0,0 +1,16 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAliasType + +ContextManagementType = TypeAliasType("ContextManagementType", Literal["compaction"]) +""" +Agent 上下文管理策略类型。 +""" diff --git a/src/arkruntime/types/agent/context_management_type_param.py b/src/arkruntime/types/agent/context_management_type_param.py new file mode 100644 index 0000000..441ce30 --- /dev/null +++ b/src/arkruntime/types/agent/context_management_type_param.py @@ -0,0 +1,16 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +ContextManagementTypeParam: TypeAlias = Literal["compaction"] +""" +Agent 上下文管理策略类型。 +""" diff --git a/src/arkruntime/types/agent/create_agent_request.py b/src/arkruntime/types/agent/create_agent_request.py index 8d71918..62082e2 100644 --- a/src/arkruntime/types/agent/create_agent_request.py +++ b/src/arkruntime/types/agent/create_agent_request.py @@ -11,6 +11,7 @@ from arkruntime._models import BaseModel from .agent_skill_ref import AgentSkillRef +from .context_management_policies import ContextManagementPolicies from .mcp_server import MCPServer from .model_config import ModelConfig from .multiagent_config import MultiagentConfig @@ -21,6 +22,9 @@ class CreateAgentRequest(BaseModel): """ 创建 Agent 的请求体。 + + multiagent_20261001 必须携带非 null 的 subagents;该条件由服务端校验, + 并通过 dependentSchemas 写入公开契约。 """ name: str @@ -57,7 +61,7 @@ class CreateAgentRequest(BaseModel): """ multiagent: Optional[MultiagentConfig] = None """ - 多 Agent 编排拓扑。 + 多 Agent 编排拓扑。multiagent_20261001 时 subagents 必填且不能为 null。 """ metadata: Optional[Dict[str, str]] = None """ @@ -67,3 +71,8 @@ class CreateAgentRequest(BaseModel): """ 资源标签。 """ + context_management: Optional[ContextManagementPolicies] = None + """ + 上下文管理策略,最多一条 compaction。省略、null 或 [] 均表示不配置自定义策略; + SDK 不会自动填入服务端公开响应中的展示默认阈值。 + """ diff --git a/src/arkruntime/types/agent/create_agent_request_param.py b/src/arkruntime/types/agent/create_agent_request_param.py new file mode 100644 index 0000000..2cda6d2 --- /dev/null +++ b/src/arkruntime/types/agent/create_agent_request_param.py @@ -0,0 +1,67 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Dict, List, Optional + +from typing_extensions import Required, TypedDict + +from .agent_skill_ref_param import AgentSkillRefParam +from .context_management_policies_param import ContextManagementPoliciesParam +from .mcp_server_param import MCPServerParam +from .model_config_param import ModelConfigParam +from .multiagent_config_param import MultiagentConfigParam +from .tag_param import TagParam +from .tool_item_param import ToolItemParam + + +class CreateAgentRequestParam(TypedDict, total=False): + """ + 创建 Agent 的请求体。 + + multiagent_20261001 必须携带非 null 的 subagents;该条件由服务端校验, + 并通过 dependentSchemas 写入公开契约。 + """ + + name: Required[str] + """人类可读名称。""" + + model: Required[ModelConfigParam] + """模型配置。""" + + description: Optional[str] + """描述信息。""" + + display_name: Optional[str] + """展示名。""" + + system: Optional[str] + """System prompt。""" + + mcp_servers: Optional[List[MCPServerParam]] + """MCP 服务器列表。""" + + tools: Optional[List[ToolItemParam]] + """工具配置数组。""" + + skills: Optional[List[AgentSkillRefParam]] + """Skill 引用列表。""" + + multiagent: Optional[MultiagentConfigParam] + """多 Agent 编排拓扑。multiagent_20261001 时 subagents 必填且不能为 null。""" + + metadata: Optional[Dict[str, str]] + """用户自定义键值对元数据。""" + + tags: Optional[List[TagParam]] + """资源标签。""" + + context_management: Optional[ContextManagementPoliciesParam] + """ + 上下文管理策略,最多一条 compaction。省略、null 或 [] 均表示不配置自定义策略; + SDK 不会自动填入服务端公开响应中的展示默认阈值。 + """ diff --git a/src/arkruntime/types/agent/custom_tool_input_schema_param.py b/src/arkruntime/types/agent/custom_tool_input_schema_param.py new file mode 100644 index 0000000..a7ae702 --- /dev/null +++ b/src/arkruntime/types/agent/custom_tool_input_schema_param.py @@ -0,0 +1,26 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Dict, List, Optional + +from typing_extensions import TypedDict + + +class CustomToolInputSchemaParam(TypedDict, total=False): + """ + Custom tool 的输入 JSON Schema。 + """ + + type: Optional[str] + """JSON Schema 顶层类型;缺省按 `"object"` 处理。""" + + properties: Optional[Dict[str, object]] + """JSON Schema properties 对象。""" + + required: Optional[List[str]] + """必填属性名列表。""" diff --git a/src/arkruntime/types/agent/mcp_server_param.py b/src/arkruntime/types/agent/mcp_server_param.py new file mode 100644 index 0000000..5dd9477 --- /dev/null +++ b/src/arkruntime/types/agent/mcp_server_param.py @@ -0,0 +1,27 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + + +from .mcp_server_type_param import MCPServerTypeParam + + +class MCPServerParam(TypedDict, total=False): + """ + 一个外接 MCP 服务器。 + """ + + name: Required[str] + """在本 Agent 内唯一,被 `tools[].mcp_server_name` 引用。""" + + type: Required[MCPServerTypeParam] + """MCP server 类型,当前固定 `"url"`。""" + + url: Required[str] + """MCP server URL。""" diff --git a/src/arkruntime/types/agent/mcp_server_type_param.py b/src/arkruntime/types/agent/mcp_server_type_param.py new file mode 100644 index 0000000..ce79db9 --- /dev/null +++ b/src/arkruntime/types/agent/mcp_server_type_param.py @@ -0,0 +1,16 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +MCPServerTypeParam: TypeAlias = Literal["url"] +""" +MCP Server 类型。 +""" diff --git a/src/arkruntime/types/agent/model_config.py b/src/arkruntime/types/agent/model_config.py index f03f00d..3de90a2 100644 --- a/src/arkruntime/types/agent/model_config.py +++ b/src/arkruntime/types/agent/model_config.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import Dict, List, Optional from arkruntime._models import BaseModel @@ -47,3 +47,29 @@ class ModelConfig(BaseModel): """ 推理努力程度。 """ + service_tier: Optional[str] = None + """ + 服务档位。 + """ + base_url: Optional[str] = None + """ + 自定义模型接入点 base URL。 + + 配合非 `ark` 的 `provider` 使用。非 `ark` provider 必须提供本字段与 + `token_limits`;`ark` provider 指定本字段返回 400。 + """ + protocol: Optional[str] = None + """ + 模型接入协议。 + + 取值须命中服务端维护的接入协议白名单,非法值返回 400。 + """ + headers: Optional[Dict[str, str]] = None + """ + 模型静态请求 header。 + + 仅用于非认证类静态 header,例如 `X-Trial-Id`。`Authorization` / + `X-Api-Key` 等认证 header 不得写入本字段,必须由 SendEvent 请求上的 + `X-User-Model-Header-*` 动态提供。服务端对历史请求的兼容行为不构成新 + 客户端可依赖的公开能力。 + """ diff --git a/src/arkruntime/types/agent/model_config_param.py b/src/arkruntime/types/agent/model_config_param.py new file mode 100644 index 0000000..7eedc15 --- /dev/null +++ b/src/arkruntime/types/agent/model_config_param.py @@ -0,0 +1,69 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Dict, List, Optional + +from typing_extensions import Required, TypedDict + +from .model_speed import ModelSpeed +from .token_limits_param import TokenLimitsParam + + +class ModelConfigParam(TypedDict, total=False): + """ + Agent 引用的模型配置。 + """ + + id: Required[str] + """模型 ID,例如 `doubao-seed-2-1`。""" + + speed: Optional[ModelSpeed] + """速度档位。空字符串走默认(并非所有模型都支持 `fast`)。""" + + token_limits: Optional[TokenLimitsParam] + """模型 token 限制快照。""" + + input_modalities: Optional[List[str]] + """底模支持的输入模态列表。""" + + provider: Optional[str] + """模型提供方。""" + + thinking: Optional[str] + """thinking 配置。""" + + reasoning_effort: Optional[str] + """推理努力程度。""" + + service_tier: Optional[str] + """服务档位。""" + + base_url: Optional[str] + """ + 自定义模型接入点 base URL。 + + 配合非 `ark` 的 `provider` 使用。非 `ark` provider 必须提供本字段与 + `token_limits`;`ark` provider 指定本字段返回 400。 + """ + + protocol: Optional[str] + """ + 模型接入协议。 + + 取值须命中服务端维护的接入协议白名单,非法值返回 400。 + """ + + headers: Optional[Dict[str, str]] + """ + 模型静态请求 header。 + + 仅用于非认证类静态 header,例如 `X-Trial-Id`。`Authorization` / + `X-Api-Key` 等认证 header 不得写入本字段,必须由 SendEvent 请求上的 + `X-User-Model-Header-*` 动态提供。服务端对历史请求的兼容行为不构成新 + 客户端可依赖的公开能力。 + """ diff --git a/src/arkruntime/types/agent/multiagent_config.py b/src/arkruntime/types/agent/multiagent_config.py index 78a581c..aeaaa79 100644 --- a/src/arkruntime/types/agent/multiagent_config.py +++ b/src/arkruntime/types/agent/multiagent_config.py @@ -6,24 +6,53 @@ from __future__ import annotations -from typing import List +from typing import List, Optional + +from pydantic import model_validator from arkruntime._models import BaseModel from .agent_ref import AgentRef +from .multiagent_subagents_config import MultiagentSubagentsConfig from .multiagent_type import MultiagentType class MultiagentConfig(BaseModel): """ - 多 Agent 协调拓扑。 + 多 Agent 协调拓扑。`type` 当前支持 `coordinator` 与 + `multiagent_20261001`。 + + - `coordinator` 使用 `agents`;`subagents` 会被服务端忽略。 + - `multiagent_20261001` 使用 `subagents`;非空 `agents` 返回 400。 + - 拓扑迁移只允许 `coordinator` 或单 Agent 升级到 + `multiagent_20261001`;反之返回 400。 + - `subagents` 表示拓扑配置值;Update 的 presence / null patch 语义由 + `MultiagentPatch` 单独表达。 """ type: MultiagentType """ 拓扑类型。 """ - agents: List[AgentRef] + agents: Optional[List[AgentRef]] = None + """ + coordinator 的下属 Agent 数组,0–20 项;深度限制为 1。 + `multiagent_20261001` 不使用该字段;服务端响应仍可能回显 `[]`。 """ - 下属 Agent 数组,1–20 项;深度限制为 1(被引用的 Agent 自身不能再有 `multiagent`)。 + subagents: Optional[MultiagentSubagentsConfig] = None """ + `multiagent_20261001` 的子代理配置。 + + Create 时必须提供。该属性不接受显式 `null`。 + """ + + # ark-apis generated validation: dynamic multiagent subagents + @model_validator(mode="after") + def _validate_dynamic_subagents(self) -> "MultiagentConfig": + type_value = getattr(self.type, "value", self.type) + if type_value == "multiagent_20261001" and self.subagents is None: + raise ValueError( + "multiagent.subagents is required when multiagent.type is " + '"multiagent_20261001"' + ) + return self diff --git a/src/arkruntime/types/agent/multiagent_config_param.py b/src/arkruntime/types/agent/multiagent_config_param.py new file mode 100644 index 0000000..efa74db --- /dev/null +++ b/src/arkruntime/types/agent/multiagent_config_param.py @@ -0,0 +1,45 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List, Optional + +from typing_extensions import Required, TypedDict + +from .agent_ref_param import AgentRefParam +from .multiagent_subagents_config_param import MultiagentSubagentsConfigParam +from .multiagent_type import MultiagentType + + +class MultiagentConfigParam(TypedDict, total=False): + """ + 多 Agent 协调拓扑。`type` 当前支持 `coordinator` 与 + `multiagent_20261001`。 + + - `coordinator` 使用 `agents`;`subagents` 会被服务端忽略。 + - `multiagent_20261001` 使用 `subagents`;非空 `agents` 返回 400。 + - 拓扑迁移只允许 `coordinator` 或单 Agent 升级到 + `multiagent_20261001`;反之返回 400。 + - `subagents` 表示拓扑配置值;Update 的 presence / null patch 语义由 + `MultiagentPatch` 单独表达。 + """ + + type: Required[MultiagentType] + """拓扑类型。""" + + agents: Optional[List[AgentRefParam]] + """ + coordinator 的下属 Agent 数组,0–20 项;深度限制为 1。 + `multiagent_20261001` 不使用该字段;服务端响应仍可能回显 `[]`。 + """ + + subagents: Optional[MultiagentSubagentsConfigParam] + """ + `multiagent_20261001` 的子代理配置。 + + Create 时必须提供。该属性不接受显式 `null`。 + """ diff --git a/src/arkruntime/types/agent/multiagent_feature_type.py b/src/arkruntime/types/agent/multiagent_feature_type.py new file mode 100644 index 0000000..1e41bae --- /dev/null +++ b/src/arkruntime/types/agent/multiagent_feature_type.py @@ -0,0 +1,18 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from enum import Enum + + +class MultiagentFeatureType(str, Enum): + """ + 多 Agent 子代理能力开关。 + """ + + enabled = "enabled" + disabled = "disabled" diff --git a/src/arkruntime/types/agent/multiagent_inline_agents_config.py b/src/arkruntime/types/agent/multiagent_inline_agents_config.py new file mode 100644 index 0000000..0168161 --- /dev/null +++ b/src/arkruntime/types/agent/multiagent_inline_agents_config.py @@ -0,0 +1,22 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from arkruntime._models import BaseModel + +from .multiagent_feature_type import MultiagentFeatureType + + +class MultiagentInlineAgentsConfig(BaseModel): + """ + `multiagent_20261001` 的 inline 子代理能力开关。 + """ + + type: MultiagentFeatureType + """ + inline 子代理能力开关。 + """ diff --git a/src/arkruntime/types/agent/multiagent_inline_agents_config_param.py b/src/arkruntime/types/agent/multiagent_inline_agents_config_param.py new file mode 100644 index 0000000..fc3596c --- /dev/null +++ b/src/arkruntime/types/agent/multiagent_inline_agents_config_param.py @@ -0,0 +1,21 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + + +from .multiagent_feature_type import MultiagentFeatureType + + +class MultiagentInlineAgentsConfigParam(TypedDict, total=False): + """ + `multiagent_20261001` 的 inline 子代理能力开关。 + """ + + type: Required[MultiagentFeatureType] + """inline 子代理能力开关。""" diff --git a/src/arkruntime/types/agent/multiagent_patch.py b/src/arkruntime/types/agent/multiagent_patch.py new file mode 100644 index 0000000..2a38c0c --- /dev/null +++ b/src/arkruntime/types/agent/multiagent_patch.py @@ -0,0 +1,43 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List, Optional + +from arkruntime._models import BaseModel + +from .agent_ref import AgentRef +from .multiagent_subagents_config import MultiagentSubagentsConfig +from .multiagent_type import MultiagentType + + +class MultiagentPatch(BaseModel): + """ + UpdateAgent 的多 Agent 编排 patch。 + + 顶层省略 = 不改,显式 `null` = 移除。同为 multiagent_20261001 时, + subagents 省略会保留当前配置;subagents 显式 `null` 会重置为默认配置。 + subagents 内层的 inline_agents / predefined_agents 按字段是否显式出现合并, + 省略保留,显式 `null` 按各自重置语义处理,predefined_agents 显式 `[]` + 清空名册。coordinator 的 agents 传值时整体替换名册,省略等价于空名册。 + """ + + type: MultiagentType + """ + 拓扑类型。 + """ + agents: Optional[List[AgentRef]] = None + """ + coordinator 的下属 Agent 数组,0–20 项;深度限制为 1。 + `multiagent_20261001` 不支持该字段。省略或显式 `[]` 都表示清空名册, + 非空数组整体替换名册。 + """ + subagents: Optional[MultiagentSubagentsConfig] = None + """ + `multiagent_20261001` 的子代理 patch。省略保留当前配置,显式 `null` + 重置为 `enabled` + inline `enabled` + 空名册。 + """ diff --git a/src/arkruntime/types/agent/multiagent_patch_param.py b/src/arkruntime/types/agent/multiagent_patch_param.py new file mode 100644 index 0000000..2789ea1 --- /dev/null +++ b/src/arkruntime/types/agent/multiagent_patch_param.py @@ -0,0 +1,43 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List, Optional + +from typing_extensions import Required, TypedDict + +from .agent_ref_param import AgentRefParam +from .multiagent_subagents_config_param import MultiagentSubagentsConfigParam +from .multiagent_type import MultiagentType + + +class MultiagentPatchParam(TypedDict, total=False): + """ + UpdateAgent 的多 Agent 编排 patch。 + + 顶层省略 = 不改,显式 `null` = 移除。同为 multiagent_20261001 时, + subagents 省略会保留当前配置;subagents 显式 `null` 会重置为默认配置。 + subagents 内层的 inline_agents / predefined_agents 按字段是否显式出现合并, + 省略保留,显式 `null` 按各自重置语义处理,predefined_agents 显式 `[]` + 清空名册。coordinator 的 agents 传值时整体替换名册,省略等价于空名册。 + """ + + type: Required[MultiagentType] + """拓扑类型。""" + + agents: Optional[List[AgentRefParam]] + """ + coordinator 的下属 Agent 数组,0–20 项;深度限制为 1。 + `multiagent_20261001` 不支持该字段。省略或显式 `[]` 都表示清空名册, + 非空数组整体替换名册。 + """ + + subagents: Optional[MultiagentSubagentsConfigParam] + """ + `multiagent_20261001` 的子代理 patch。省略保留当前配置,显式 `null` + 重置为 `enabled` + inline `enabled` + 空名册。 + """ diff --git a/src/arkruntime/types/agent/multiagent_subagents_config.py b/src/arkruntime/types/agent/multiagent_subagents_config.py new file mode 100644 index 0000000..42321f3 --- /dev/null +++ b/src/arkruntime/types/agent/multiagent_subagents_config.py @@ -0,0 +1,50 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List, Optional + +from arkruntime._models import BaseModel + +from .agent_ref import AgentRef +from .multiagent_feature_type import MultiagentFeatureType +from .multiagent_inline_agents_config import MultiagentInlineAgentsConfig + + +class MultiagentSubagentsConfig(BaseModel): + """ + `multiagent_20261001` 的子代理配置。 + + 在 UpdateAgent patch 中,`inline_agents` / `predefined_agents` 的显式 + `null` 与省略具有不同语义。Java typed model 将 `null` 序列化为省略; + 需要显式 `null` 的 Java 调用方必须使用 raw request body。 + """ + + type: MultiagentFeatureType + """ + 子代理能力开关。 + """ + inline_agents: Optional[MultiagentInlineAgentsConfig] = None + """ + inline 子代理能力开关。`subagents.type=enabled` 的 Create 中,省略或 + 显式 `null` 按 `enabled` 处理。UpdateAgent patch 中省略表示保留当前值, + 显式 `null` 重置为 `enabled`。 + 服务端响应在 `subagents.type` 为 `disabled` 时可能返回 `null`;客户端 + 必须接受该 wire,并将它视为未配置 inline 能力。 + """ + predefined_agents: Optional[List[AgentRef]] = None + """ + 预定义 Agent 引用数组,最多 20 项;显式传入时整体替换。 + + 服务端查询被引用 Agent 时会按 `id` 去重校验,但不会改写成员名册; + 重复引用、自引用和循环引用按请求中的原顺序保存、冻结和展开,并受 + Session 的最大展开深度限制。 + `inline_agents` 省略时按 `enabled` 处理;此时 Create 省略本字段等价于 + 空名册。UpdateAgent patch 中省略表示保留当前值,显式 `null` 等价于 + `[]`,显式数组整体替换。`inline_agents.type` 显式为 `disabled` 时必须 + 提供本字段(可为 `[]`)。服务端响应可能返回 `null`;客户端须按空名册处理。 + """ diff --git a/src/arkruntime/types/agent/multiagent_subagents_config_param.py b/src/arkruntime/types/agent/multiagent_subagents_config_param.py new file mode 100644 index 0000000..d8f1c06 --- /dev/null +++ b/src/arkruntime/types/agent/multiagent_subagents_config_param.py @@ -0,0 +1,50 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List, Optional + +from typing_extensions import Required, TypedDict + +from .agent_ref_param import AgentRefParam +from .multiagent_feature_type import MultiagentFeatureType +from .multiagent_inline_agents_config_param import MultiagentInlineAgentsConfigParam + + +class MultiagentSubagentsConfigParam(TypedDict, total=False): + """ + `multiagent_20261001` 的子代理配置。 + + 在 UpdateAgent patch 中,`inline_agents` / `predefined_agents` 的显式 + `null` 与省略具有不同语义。Java typed model 将 `null` 序列化为省略; + 需要显式 `null` 的 Java 调用方必须使用 raw request body。 + """ + + type: Required[MultiagentFeatureType] + """子代理能力开关。""" + + inline_agents: Optional[MultiagentInlineAgentsConfigParam] + """ + inline 子代理能力开关。`subagents.type=enabled` 的 Create 中,省略或 + 显式 `null` 按 `enabled` 处理。UpdateAgent patch 中省略表示保留当前值, + 显式 `null` 重置为 `enabled`。 + 服务端响应在 `subagents.type` 为 `disabled` 时可能返回 `null`;客户端 + 必须接受该 wire,并将它视为未配置 inline 能力。 + """ + + predefined_agents: Optional[List[AgentRefParam]] + """ + 预定义 Agent 引用数组,最多 20 项;显式传入时整体替换。 + + 服务端查询被引用 Agent 时会按 `id` 去重校验,但不会改写成员名册; + 重复引用、自引用和循环引用按请求中的原顺序保存、冻结和展开,并受 + Session 的最大展开深度限制。 + `inline_agents` 省略时按 `enabled` 处理;此时 Create 省略本字段等价于 + 空名册。UpdateAgent patch 中省略表示保留当前值,显式 `null` 等价于 + `[]`,显式数组整体替换。`inline_agents.type` 显式为 `disabled` 时必须 + 提供本字段(可为 `[]`)。服务端响应可能返回 `null`;客户端须按空名册处理。 + """ diff --git a/src/arkruntime/types/agent/multiagent_type.py b/src/arkruntime/types/agent/multiagent_type.py index a115213..68111ef 100644 --- a/src/arkruntime/types/agent/multiagent_type.py +++ b/src/arkruntime/types/agent/multiagent_type.py @@ -6,11 +6,13 @@ from __future__ import annotations -from typing import Literal +from enum import Enum -from typing_extensions import TypeAliasType -MultiagentType = TypeAliasType("MultiagentType", Literal["coordinator"]) -""" -多 Agent 拓扑类型。 -""" +class MultiagentType(str, Enum): + """ + 多 Agent 拓扑类型。 + """ + + coordinator = "coordinator" + multiagent_20261001 = "multiagent_20261001" diff --git a/src/arkruntime/types/agent/permission_policy_param.py b/src/arkruntime/types/agent/permission_policy_param.py new file mode 100644 index 0000000..e340a26 --- /dev/null +++ b/src/arkruntime/types/agent/permission_policy_param.py @@ -0,0 +1,21 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + + +from .permission_policy_type import PermissionPolicyType + + +class PermissionPolicyParam(TypedDict, total=False): + """ + 工具调用许可策略。 + """ + + type: Required[PermissionPolicyType] + """策略类型。""" diff --git a/src/arkruntime/types/agent/tag_param.py b/src/arkruntime/types/agent/tag_param.py new file mode 100644 index 0000000..3b717a2 --- /dev/null +++ b/src/arkruntime/types/agent/tag_param.py @@ -0,0 +1,23 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Optional + +from typing_extensions import Required, TypedDict + + +class TagParam(TypedDict, total=False): + """ + 资源标签。key 必填,value 可空。 + """ + + key: Required[str] + """标签 key。""" + + value: Optional[str] + """标签 value;可空。""" diff --git a/src/arkruntime/types/agent/token_limits.py b/src/arkruntime/types/agent/token_limits.py index 61b232d..5d78085 100644 --- a/src/arkruntime/types/agent/token_limits.py +++ b/src/arkruntime/types/agent/token_limits.py @@ -14,6 +14,9 @@ class TokenLimits(BaseModel): """ 模型上下文与输入输出 token 限制。 + + 三个字段保持 optional,是为了兼容 Ark provider 和旧数据;非 `ark` provider + 必须提供 `token_limits`,且三个字段必须全部存在且为正数,由服务端校验。 """ context_window: Optional[int] = None diff --git a/src/arkruntime/types/agent/token_limits_param.py b/src/arkruntime/types/agent/token_limits_param.py new file mode 100644 index 0000000..3858365 --- /dev/null +++ b/src/arkruntime/types/agent/token_limits_param.py @@ -0,0 +1,29 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Optional + +from typing_extensions import TypedDict + + +class TokenLimitsParam(TypedDict, total=False): + """ + 模型上下文与输入输出 token 限制。 + + 三个字段保持 optional,是为了兼容 Ark provider 和旧数据;非 `ark` provider + 必须提供 `token_limits`,且三个字段必须全部存在且为正数,由服务端校验。 + """ + + context_window: Optional[int] + """模型上下文窗口。""" + + max_input_token_length: Optional[int] + """最大输入 token 长度。""" + + max_output_token_length: Optional[int] + """最大输出 token 长度。""" diff --git a/src/arkruntime/types/agent/tool_config_param.py b/src/arkruntime/types/agent/tool_config_param.py new file mode 100644 index 0000000..8d93f9d --- /dev/null +++ b/src/arkruntime/types/agent/tool_config_param.py @@ -0,0 +1,28 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Optional + +from typing_extensions import Required, TypedDict + +from .permission_policy_param import PermissionPolicyParam + + +class ToolConfigParam(TypedDict, total=False): + """ + toolset 类型工具的单条配置项。 + """ + + name: Required[str] + """工具名。""" + + enabled: Optional[bool] + """是否启用;缺省走 `default_config.enabled`。""" + + permission_policy: Optional[PermissionPolicyParam] + """许可策略;缺省走 `default_config.permission_policy`。""" diff --git a/src/arkruntime/types/agent/tool_default_config_param.py b/src/arkruntime/types/agent/tool_default_config_param.py new file mode 100644 index 0000000..b2ad59b --- /dev/null +++ b/src/arkruntime/types/agent/tool_default_config_param.py @@ -0,0 +1,25 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Optional + +from typing_extensions import TypedDict + +from .permission_policy_param import PermissionPolicyParam + + +class ToolDefaultConfigParam(TypedDict, total=False): + """ + toolset 类型工具的默认配置。 + """ + + enabled: Optional[bool] + """默认是否启用。""" + + permission_policy: Optional[PermissionPolicyParam] + """默认许可策略。""" diff --git a/src/arkruntime/types/agent/tool_item_param.py b/src/arkruntime/types/agent/tool_item_param.py new file mode 100644 index 0000000..98bc1e3 --- /dev/null +++ b/src/arkruntime/types/agent/tool_item_param.py @@ -0,0 +1,50 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List, Optional + +from typing_extensions import Required, TypedDict + +from .custom_tool_input_schema_param import CustomToolInputSchemaParam +from .tool_config_param import ToolConfigParam +from .tool_default_config_param import ToolDefaultConfigParam + + +class ToolItemParam(TypedDict, total=False): + """ + 一条工具配置。按 `type` 分三类: + - `agent_toolset_`:内置工具集(当前默认 `agent_toolset_20260701`; + 存量 `agent_toolset_20260401` 仍兼容) + - `mcp_toolset`:来自 `mcp_servers[]` 的工具集 + - `evolution`:自演进类工具 + - `custom`:客户端执行的自定义工具 + + 所有变体字段合并在一个 model 里,未使用的字段留空即可(proto oneof + 风格;wire 上就是同一个 JSON 对象按 `type` 决定语义)。 + """ + + type: Required[str] + """工具 type。""" + + configs: Optional[List[ToolConfigParam]] + """toolset 类专用。""" + + default_config: Optional[ToolDefaultConfigParam] + """toolset 类专用;未在 `configs` 里显式列出的工具走此默认。""" + + mcp_server_name: Optional[str] + """`mcp_toolset` 专用;须匹配 `mcp_servers[].name`。""" + + name: Optional[str] + """`custom` 专用;1–128 字符,`[A-Za-z0-9_-]`。""" + + description: Optional[str] + """`custom` 专用;1–1024 字符。""" + + input_schema: Optional[CustomToolInputSchemaParam] + """`custom` 专用;承载 JSON Schema 对象。""" diff --git a/src/arkruntime/types/agent/update_agent_request.py b/src/arkruntime/types/agent/update_agent_request.py index 08b7940..895feb1 100644 --- a/src/arkruntime/types/agent/update_agent_request.py +++ b/src/arkruntime/types/agent/update_agent_request.py @@ -11,9 +11,10 @@ from arkruntime._models import BaseModel from .agent_skill_ref import AgentSkillRef +from .context_management_policies import ContextManagementPolicies from .mcp_server import MCPServer from .model_config import ModelConfig -from .multiagent_config import MultiagentConfig +from .multiagent_patch import MultiagentPatch from .tag import Tag from .tool_item import ToolItem @@ -22,7 +23,7 @@ class UpdateAgentRequest(BaseModel): """ 更新 Agent 的请求体。语义: - 省略 = 保留 - - 空字符串/null = 清空(仅 description / system / 数组类) + - 空字符串/null = 清空(仅 description / system / 既有数组类;context_management 的 null 表示保留) - `name` / `model` 不可清空 - `metadata` 走 patch - `mcp_servers` / `tools` / `skills` 整体替换 @@ -65,9 +66,20 @@ class UpdateAgentRequest(BaseModel): """ Skill 引用列表(整体替换)。 """ - multiagent: Optional[MultiagentConfig] = None + multiagent: Optional[MultiagentPatch] = None """ - 多 Agent 编排拓扑。 + 多 Agent 编排拓扑(patch):顶层省略 = 不改,显式 `null` = 移除。 + + 同为 multiagent_20261001 时,subagents 省略会保留当前配置;subagents + 显式 `null` 会重置为默认配置。subagents 内层的 inline_agents / + predefined_agents 也按字段是否显式出现进行合并,省略保留,显式 `null` + 按各自重置语义处理,predefined_agents 显式 `[]` 清空名册。coordinator + 的 agents 传值时整体替换名册,省略等价于空名册。 + + 服务端支持显式 `null`,但当前生成器无法让所有语言在强类型模型里统一区分 + “省略”和“显式 null”。当前 SDK 能表达时按其 request API 发送;不能表达时 + 必须使用 raw HTTP/request-body escape hatch。需要补齐的 typed patch API + 由各 SDK 独立交付,不在本契约 MR 中新增。 """ metadata: Optional[Dict[str, str]] = None """ @@ -77,3 +89,8 @@ class UpdateAgentRequest(BaseModel): """ 资源标签(整体替换)。 """ + context_management: Optional[ContextManagementPolicies] = None + """ + 上下文管理策略(整体替换),最多一条 compaction。省略或 null 表示保留; + 显式 [] 表示清除自定义策略,随后由运行时使用默认行为。 + """ diff --git a/src/arkruntime/types/agent/update_agent_request_param.py b/src/arkruntime/types/agent/update_agent_request_param.py new file mode 100644 index 0000000..a069242 --- /dev/null +++ b/src/arkruntime/types/agent/update_agent_request_param.py @@ -0,0 +1,86 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Dict, List, Optional + +from typing_extensions import Required, TypedDict + +from .agent_skill_ref_param import AgentSkillRefParam +from .context_management_policies_param import ContextManagementPoliciesParam +from .mcp_server_param import MCPServerParam +from .model_config_param import ModelConfigParam +from .multiagent_patch_param import MultiagentPatchParam +from .tag_param import TagParam +from .tool_item_param import ToolItemParam + + +class UpdateAgentRequestParam(TypedDict, total=False): + """ + 更新 Agent 的请求体。语义: + - 省略 = 保留 + - 空字符串/null = 清空(仅 description / system / 既有数组类;context_management 的 null 表示保留) + - `name` / `model` 不可清空 + - `metadata` 走 patch + - `mcp_servers` / `tools` / `skills` 整体替换 + - `version` 必填,必须等于服务端当前版本(乐观并发控制) + """ + + version: Required[int] + """期望更新的当前版本号。用于乐观并发控制;不匹配时拒绝写入。""" + + name: Optional[str] + """人类可读名称。""" + + display_name: Optional[str] + """展示名。""" + + model: Optional[ModelConfigParam] + """模型配置。""" + + description: Optional[str] + """描述信息。""" + + system: Optional[str] + """System prompt。""" + + mcp_servers: Optional[List[MCPServerParam]] + """MCP 服务器列表(整体替换)。""" + + tools: Optional[List[ToolItemParam]] + """工具配置数组(整体替换)。""" + + skills: Optional[List[AgentSkillRefParam]] + """Skill 引用列表(整体替换)。""" + + multiagent: Optional[MultiagentPatchParam] + """ + 多 Agent 编排拓扑(patch):顶层省略 = 不改,显式 `null` = 移除。 + + 同为 multiagent_20261001 时,subagents 省略会保留当前配置;subagents + 显式 `null` 会重置为默认配置。subagents 内层的 inline_agents / + predefined_agents 也按字段是否显式出现进行合并,省略保留,显式 `null` + 按各自重置语义处理,predefined_agents 显式 `[]` 清空名册。coordinator + 的 agents 传值时整体替换名册,省略等价于空名册。 + + 服务端支持显式 `null`,但当前生成器无法让所有语言在强类型模型里统一区分 + “省略”和“显式 null”。当前 SDK 能表达时按其 request API 发送;不能表达时 + 必须使用 raw HTTP/request-body escape hatch。需要补齐的 typed patch API + 由各 SDK 独立交付,不在本契约 MR 中新增。 + """ + + metadata: Optional[Dict[str, str]] + """用户自定义键值对元数据(patch)。""" + + tags: Optional[List[TagParam]] + """资源标签(整体替换)。""" + + context_management: Optional[ContextManagementPoliciesParam] + """ + 上下文管理策略(整体替换),最多一条 compaction。省略或 null 表示保留; + 显式 [] 表示清除自定义策略,随后由运行时使用默认行为。 + """ diff --git a/src/arkruntime/types/audio_generation/audio_config_param.py b/src/arkruntime/types/audio_generation/audio_config_param.py index 45cfe4e..0dbc6f3 100644 --- a/src/arkruntime/types/audio_generation/audio_config_param.py +++ b/src/arkruntime/types/audio_generation/audio_config_param.py @@ -16,14 +16,14 @@ class AudioConfigParam(TypedDict, total=False): - sample_rate: Optional[Optional[SampleRate]] + sample_rate: Optional[SampleRate] """Defaults to 44100 for wav/mp3/pcm and 48000 for ogg_opus. ogg_opus only accepts 48000. Explicit zero is invalid.""" - speech_rate: Optional[Optional[RateParam]] + speech_rate: Optional[RateParam] """Reference generation only. Default 0; -50 means 0.5x and 100 means 2x. Must be omitted (or null) for dubbing/separation, even when the value would be 0.""" - loudness_rate: Optional[Optional[RateParam]] + loudness_rate: Optional[RateParam] """Reference generation only. Default 0; -50 means 0.5x and 100 means 2x. Must be omitted (or null) for dubbing/separation.""" - pitch_rate: Optional[Optional[PitchRateParam]] + pitch_rate: Optional[PitchRateParam] """Reference generation only. Default 0. Must be omitted (or null) for dubbing/separation.""" diff --git a/src/arkruntime/types/audio_generation/audio_generation_request_param.py b/src/arkruntime/types/audio_generation/audio_generation_request_param.py index 041ff4d..e12bdc4 100644 --- a/src/arkruntime/types/audio_generation/audio_generation_request_param.py +++ b/src/arkruntime/types/audio_generation/audio_generation_request_param.py @@ -30,17 +30,17 @@ class AudioGenerationRequestParam(TypedDict, total=False): content: Required[List[ContentParam]] - dubbing_config: Optional[Optional[DubbingConfigParam]] + dubbing_config: Optional[DubbingConfigParam] """Required for dubbing; forbidden in reference generation and separation.""" - output_format: Optional[Optional[OutputFormat]] + output_format: Optional[OutputFormat] """Output audio format. Omitted or null resolves to wav.""" - audio_config: Optional[Optional[AudioConfigParam]] + audio_config: Optional[AudioConfigParam] """Optional output parameters. Defaults are applied by the service.""" - include: Optional[Optional[List[Include]]] + include: Optional[List[Include]] """Additional artifacts; omitted/null/empty means none. video_url requires a video input. subtitles_source.* is only allowed for dubbing. Separation forbids a non-empty include.""" - watermark: Optional[Optional[bool]] + watermark: Optional[bool] """Append an audible rhythm watermark at the end. Defaults to false. Only supported in the cn-beijing deployment region; overseas deployments reject true with InvalidParameter (param=watermark).""" diff --git a/src/arkruntime/types/audio_generation/dubbing_config_param.py b/src/arkruntime/types/audio_generation/dubbing_config_param.py index 8a06f3b..ae45e2d 100644 --- a/src/arkruntime/types/audio_generation/dubbing_config_param.py +++ b/src/arkruntime/types/audio_generation/dubbing_config_param.py @@ -22,5 +22,5 @@ class DubbingConfigParam(TypedDict, total=False): target_language: Required[TargetLanguage] """Required for dubbing; accepted values match the dubbing validator.""" - glossaries: Optional[Optional[GlossariesParam]] + glossaries: Optional[GlossariesParam] """Terminology pairs; source == target denotes a hotword. For duplicate sources, terminology overrides hotwords regardless of order; the last terminology target wins.""" diff --git a/src/arkruntime/types/chat/chat_completion_request_assistant_message_param.py b/src/arkruntime/types/chat/chat_completion_request_assistant_message_param.py index 2fa5dea..6b21e94 100644 --- a/src/arkruntime/types/chat/chat_completion_request_assistant_message_param.py +++ b/src/arkruntime/types/chat/chat_completion_request_assistant_message_param.py @@ -20,13 +20,13 @@ class ChatCompletionRequestAssistantMessageParam(TypedDict, total=False): role: Required[Literal["assistant"]] """The role of the messages author. Always `assistant`.""" - content: Optional[Optional[ChatCompletionMessageContentParam]] + content: Optional[ChatCompletionMessageContentParam] """ The contents of the assistant message. Missing, null, or empty content is normalized to an empty string, including when no tools are called. """ - reasoning_content: Optional[Optional[ChatCompletionMessageContentParam]] + reasoning_content: Optional[ChatCompletionMessageContentParam] """The reasoning content emitted by the assistant alongside `content`.""" encrypted_content: Optional[str] diff --git a/src/arkruntime/types/chat/chat_completion_request_message_tool_call_param.py b/src/arkruntime/types/chat/chat_completion_request_message_tool_call_param.py index 9c8247b..53e8318 100644 --- a/src/arkruntime/types/chat/chat_completion_request_message_tool_call_param.py +++ b/src/arkruntime/types/chat/chat_completion_request_message_tool_call_param.py @@ -17,7 +17,7 @@ class ChatCompletionRequestMessageToolCallParam(TypedDict, total=False): - id: Optional[Optional[str]] + id: Optional[str] """ The ID of the tool call. Missing or null IDs are normalized to an empty string. diff --git a/src/arkruntime/types/chat/chat_completion_request_tool_message_param.py b/src/arkruntime/types/chat/chat_completion_request_tool_message_param.py index 4f0bf2e..b8d45ce 100644 --- a/src/arkruntime/types/chat/chat_completion_request_tool_message_param.py +++ b/src/arkruntime/types/chat/chat_completion_request_tool_message_param.py @@ -17,13 +17,13 @@ class ChatCompletionRequestToolMessageParam(TypedDict, total=False): role: Required[Literal["tool"]] """The role of the messages author. Always `tool`.""" - content: Optional[Optional[ChatCompletionMessageContentParam]] + content: Optional[ChatCompletionMessageContentParam] """ The contents of the tool message. Missing or null content is normalized to an empty string. """ - tool_call_id: Optional[Optional[str]] + tool_call_id: Optional[str] """ Tool call that this message is responding to. Missing or null IDs are normalized to an empty string. diff --git a/src/arkruntime/types/chat/chat_completion_request_user_message_param.py b/src/arkruntime/types/chat/chat_completion_request_user_message_param.py index fe36ef4..437f457 100644 --- a/src/arkruntime/types/chat/chat_completion_request_user_message_param.py +++ b/src/arkruntime/types/chat/chat_completion_request_user_message_param.py @@ -17,7 +17,7 @@ class ChatCompletionRequestUserMessageParam(TypedDict, total=False): role: Required[Literal["user"]] """The role of the messages author. Always `user`.""" - content: Optional[Optional[ChatCompletionMessageContentParam]] + content: Optional[ChatCompletionMessageContentParam] """ The contents of the user message. Missing, null, or empty content is normalized to an empty string. diff --git a/src/arkruntime/types/environment/environment.py b/src/arkruntime/types/environment/environment.py index 71c7179..e83156b 100644 --- a/src/arkruntime/types/environment/environment.py +++ b/src/arkruntime/types/environment/environment.py @@ -47,6 +47,10 @@ class Environment(BaseModel): """ 一期未启用;仅协议占位。 """ + archived_at: Optional[str] = None + """ + 首次归档时间(RFC 3339),只读;未归档时省略。归档作用于资源 ID。 + """ created_at: str """ RFC 3339 时间。 diff --git a/src/arkruntime/types/images/__init__.py b/src/arkruntime/types/images/__init__.py index dc9eb8a..5bc356b 100644 --- a/src/arkruntime/types/images/__init__.py +++ b/src/arkruntime/types/images/__init__.py @@ -14,12 +14,18 @@ from .image_generation_response import ImageGenerationResponse from .image_generation_stream_event import ImageGenerationStreamEvent from .image_generation_stream_event_type import ImageGenerationStreamEventType +from .layer_decomposition_options import LayerDecompositionOptions from .optimize_prompt_mode import OptimizePromptMode from .optimize_prompt_options import OptimizePromptOptions +from .output_background import OutputBackground from .output_format import OutputFormat from .response_format import ResponseFormat from .sequential_image_generation_mode import SequentialImageGenerationMode from .sequential_image_generation_options import SequentialImageGenerationOptions +from .text_line import TextLine +from .text_recognition import TextRecognition +from .text_run import TextRun +from .text_style import TextStyle from .tool import Tool from .tool_type import ToolType from .tool_usage import ToolUsage @@ -34,18 +40,25 @@ "ImageGenerationResponse", "ImageGenerationStreamEvent", "ImageGenerationStreamEventType", + "LayerDecompositionOptions", "OptimizePromptMode", "OptimizePromptOptions", + "OutputBackground", "OutputFormat", "ResponseFormat", "SequentialImageGenerationMode", "SequentialImageGenerationOptions", + "TextLine", + "TextRecognition", + "TextRun", + "TextStyle", "Tool", "ToolType", "ToolUsage", "Usage", ] from .create_image_generation_request_param import CreateImageGenerationRequestParam +from .layer_decomposition_options_param import LayerDecompositionOptionsParam from .optimize_prompt_options_param import OptimizePromptOptionsParam from .sequential_image_generation_options_param import ( SequentialImageGenerationOptionsParam, diff --git a/src/arkruntime/types/images/background.py b/src/arkruntime/types/images/background.py index f7edab6..5ae9e76 100644 --- a/src/arkruntime/types/images/background.py +++ b/src/arkruntime/types/images/background.py @@ -14,5 +14,6 @@ class Background(str, Enum): Background of the generated image. """ + auto = "auto" opaque = "opaque" transparent = "transparent" diff --git a/src/arkruntime/types/images/create_image_generation_request.py b/src/arkruntime/types/images/create_image_generation_request.py index 5e310ad..5d59b07 100644 --- a/src/arkruntime/types/images/create_image_generation_request.py +++ b/src/arkruntime/types/images/create_image_generation_request.py @@ -11,6 +11,7 @@ from arkruntime._models import BaseModel from .background import Background +from .layer_decomposition_options import LayerDecompositionOptions from .optimize_prompt_options import OptimizePromptOptions from .output_format import OutputFormat from .response_format import ResponseFormat @@ -24,6 +25,11 @@ class CreateImageGenerationRequest(BaseModel): """ The model identifier to use. """ + safety_identifier: Optional[str] = None + """ + Stable, unique end-user identifier for safety and abuse tracing. + Hash usernames, email addresses, or internal user IDs before sending to protect privacy. + """ prompt: Optional[str] = None """ Text prompt describing the desired image. Required for standard generation; may be omitted for automatic layer decomposition when `layer_decomposition=true`. @@ -54,7 +60,7 @@ class CreateImageGenerationRequest(BaseModel): """ size: Optional[str] = None """ - Output size hint (e.g. `1024x1024`, `1K`, `2K`, or `auto`). `auto` is supported for layer decomposition. + Output size, such as `1024x1024`, `0.5K`, `1K`, `1.5K`, `2K`, `3K`, or `auto`. """ watermark: Optional[bool] = None """ @@ -80,14 +86,18 @@ class CreateImageGenerationRequest(BaseModel): """ output_format: Optional[OutputFormat] = None """ - Container/codec for the generated image bytes. + Output image format. """ background: Optional[Background] = None """ - Output background. Defaults to `opaque`. `transparent` requires exactly one PNG reference image with an alpha channel and PNG output. + Requested background type. """ layer_decomposition: Optional[bool] = None """ Decompose one reference image into a base image and independently composable layers. Supported models return layer metadata in `data`. """ + layer_decomposition_options: Optional[LayerDecompositionOptions] = None + """ + Layer count and OCR options. + """ diff --git a/src/arkruntime/types/images/create_image_generation_request_param.py b/src/arkruntime/types/images/create_image_generation_request_param.py index 20932a9..d98153a 100644 --- a/src/arkruntime/types/images/create_image_generation_request_param.py +++ b/src/arkruntime/types/images/create_image_generation_request_param.py @@ -11,6 +11,7 @@ from typing_extensions import Required, TypedDict from .background import Background +from .layer_decomposition_options_param import LayerDecompositionOptionsParam from .optimize_prompt_options_param import OptimizePromptOptionsParam from .output_format import OutputFormat from .response_format import ResponseFormat @@ -25,6 +26,12 @@ class CreateImageGenerationRequestParam(TypedDict, total=False): model: Required[str] """The model identifier to use.""" + safety_identifier: Optional[str] + """ + Stable, unique end-user identifier for safety and abuse tracing. + Hash usernames, email addresses, or internal user IDs before sending to protect privacy. + """ + prompt: Optional[str] """Text prompt describing the desired image. Required for standard generation; may be omitted for automatic layer decomposition when `layer_decomposition=true`.""" @@ -47,7 +54,7 @@ class CreateImageGenerationRequestParam(TypedDict, total=False): """Classifier-free guidance scale for reference images (edit / R2I).""" size: Optional[str] - """Output size hint (e.g. `1024x1024`, `1K`, `2K`, or `auto`). `auto` is supported for layer decomposition.""" + """Output size, such as `1024x1024`, `0.5K`, `1K`, `1.5K`, `2K`, `3K`, or `auto`.""" watermark: Optional[bool] """Embed a watermark in the generated image.""" @@ -65,13 +72,16 @@ class CreateImageGenerationRequestParam(TypedDict, total=False): """Tools the model may invoke during generation.""" output_format: Optional[OutputFormat] - """Container/codec for the generated image bytes.""" + """Output image format.""" background: Optional[Background] - """Output background. Defaults to `opaque`. `transparent` requires exactly one PNG reference image with an alpha channel and PNG output.""" + """Requested background type.""" layer_decomposition: Optional[bool] """ Decompose one reference image into a base image and independently composable layers. Supported models return layer metadata in `data`. """ + + layer_decomposition_options: Optional[LayerDecompositionOptionsParam] + """Layer count and OCR options.""" diff --git a/src/arkruntime/types/images/image_data_item.py b/src/arkruntime/types/images/image_data_item.py index 3d9fafe..0c3acde 100644 --- a/src/arkruntime/types/images/image_data_item.py +++ b/src/arkruntime/types/images/image_data_item.py @@ -12,7 +12,9 @@ from .bounding_box import BoundingBox from .image_error import ImageError +from .output_background import OutputBackground from .output_format import OutputFormat +from .text_recognition import TextRecognition class ImageDataItem(BaseModel): @@ -40,6 +42,14 @@ class ImageDataItem(BaseModel): """ Actual container/codec used for this image. """ + background: Optional[OutputBackground] = None + """ + Actual output background. + """ + text_recognition: Optional[TextRecognition] = None + """ + Recognized text and styles for this layer. + """ z_index: Optional[int] = None """ Layer stacking order. The base image is `0`; layers follow bottom-up. diff --git a/src/arkruntime/types/images/image_generation_response.py b/src/arkruntime/types/images/image_generation_response.py index a51b969..9887340 100644 --- a/src/arkruntime/types/images/image_generation_response.py +++ b/src/arkruntime/types/images/image_generation_response.py @@ -21,6 +21,10 @@ class ImageGenerationResponse(BaseModel): """ Model that produced the result. """ + safety_identifier: Optional[str] = None + """ + Echoed safety identifier. + """ created: int """ Unix timestamp (seconds) when the response was produced. diff --git a/src/arkruntime/types/images/image_generation_stream_event.py b/src/arkruntime/types/images/image_generation_stream_event.py index a2c8e27..6b1aefe 100644 --- a/src/arkruntime/types/images/image_generation_stream_event.py +++ b/src/arkruntime/types/images/image_generation_stream_event.py @@ -40,6 +40,10 @@ class ImageGenerationStreamEvent(BaseModel): """ Model that produced the result. """ + safety_identifier: Optional[str] = None + """ + Echoed safety identifier. + """ created: int """ Unix timestamp (seconds) when the event was produced. diff --git a/src/arkruntime/types/images/layer_decomposition_options.py b/src/arkruntime/types/images/layer_decomposition_options.py new file mode 100644 index 0000000..f5e060d --- /dev/null +++ b/src/arkruntime/types/images/layer_decomposition_options.py @@ -0,0 +1,26 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Optional + +from arkruntime._models import BaseModel + + +class LayerDecompositionOptions(BaseModel): + """ + Layer decomposition options. + """ + + max_images: Optional[int] = None + """ + Maximum output images, including the base image. + """ + text_recognition: Optional[bool] = None + """ + Return structured OCR metadata. + """ diff --git a/src/arkruntime/types/images/layer_decomposition_options_param.py b/src/arkruntime/types/images/layer_decomposition_options_param.py new file mode 100644 index 0000000..a08978e --- /dev/null +++ b/src/arkruntime/types/images/layer_decomposition_options_param.py @@ -0,0 +1,23 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Optional + +from typing_extensions import TypedDict + + +class LayerDecompositionOptionsParam(TypedDict, total=False): + """ + Layer decomposition options. + """ + + max_images: Optional[int] + """Maximum output images, including the base image.""" + + text_recognition: Optional[bool] + """Return structured OCR metadata.""" diff --git a/src/arkruntime/types/images/output_background.py b/src/arkruntime/types/images/output_background.py new file mode 100644 index 0000000..20fe93f --- /dev/null +++ b/src/arkruntime/types/images/output_background.py @@ -0,0 +1,18 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from enum import Enum + + +class OutputBackground(str, Enum): + """ + Actual background of a generated image; never the request-only auto value. + """ + + opaque = "opaque" + transparent = "transparent" diff --git a/src/arkruntime/types/images/text_line.py b/src/arkruntime/types/images/text_line.py new file mode 100644 index 0000000..5831016 --- /dev/null +++ b/src/arkruntime/types/images/text_line.py @@ -0,0 +1,25 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List, Optional + +from arkruntime._models import BaseModel + +from .bounding_box import BoundingBox +from .text_run import TextRun + + +class TextLine(BaseModel): + bounding_box: Optional[BoundingBox] = None + """ + Text-line bounds in base-image coordinates. + """ + runs: Optional[List[TextRun]] = None + """ + Text segments with individual styles. + """ diff --git a/src/arkruntime/types/images/text_recognition.py b/src/arkruntime/types/images/text_recognition.py new file mode 100644 index 0000000..4238183 --- /dev/null +++ b/src/arkruntime/types/images/text_recognition.py @@ -0,0 +1,28 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List, Optional + +from arkruntime._models import BaseModel + +from .text_line import TextLine + + +class TextRecognition(BaseModel): + """ + Recognized text for one layer. + """ + + text: Optional[str] = None + """ + Full text, with runs joined in order and lines separated by newlines. + """ + lines: Optional[List[TextLine]] = None + """ + Text lines in visual reading order. + """ diff --git a/src/arkruntime/types/images/text_run.py b/src/arkruntime/types/images/text_run.py new file mode 100644 index 0000000..2914814 --- /dev/null +++ b/src/arkruntime/types/images/text_run.py @@ -0,0 +1,21 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Optional + +from arkruntime._models import BaseModel + +from .text_style import TextStyle + + +class TextRun(BaseModel): + text: Optional[str] = None + """ + Text for this segment, without a newline. + """ + style: Optional[TextStyle] = None diff --git a/src/arkruntime/types/images/text_style.py b/src/arkruntime/types/images/text_style.py new file mode 100644 index 0000000..e36b80a --- /dev/null +++ b/src/arkruntime/types/images/text_style.py @@ -0,0 +1,32 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Optional + +from arkruntime._models import BaseModel + + +class TextStyle(BaseModel): + """ + Recognized text style. + """ + + similar_font_family: Optional[str] = None + """ + Closest supported font name, or `unknown`. + """ + foreground_color: Optional[str] = None + """ + Text color in #RRGGBB format. + """ + bold: Optional[bool] = None + italic: Optional[bool] = None + underline: Optional[bool] = None + strikethrough: Optional[bool] = None + shadow: Optional[bool] = None + translucent: Optional[bool] = None diff --git a/src/arkruntime/types/responses/item_tool_search_call_param.py b/src/arkruntime/types/responses/item_tool_search_call_param.py index 65a62e8..658f00d 100644 --- a/src/arkruntime/types/responses/item_tool_search_call_param.py +++ b/src/arkruntime/types/responses/item_tool_search_call_param.py @@ -24,7 +24,7 @@ class ItemToolSearchCallParam(TypedDict, total=False): arguments: Required[Dict[str, object]] """JSON object (not an encoded string). Completed calls contain a nonempty paths array; added streaming items initially contain {}.""" - call_id: Optional[Optional[str]] + call_id: Optional[str] """Hosted search emits null. Replayed hosted history must omit this field or set it to null.""" execution: Optional[ToolSearchExecution] diff --git a/src/arkruntime/types/responses/item_tool_search_output_param.py b/src/arkruntime/types/responses/item_tool_search_output_param.py index 3956260..9e2330e 100644 --- a/src/arkruntime/types/responses/item_tool_search_output_param.py +++ b/src/arkruntime/types/responses/item_tool_search_output_param.py @@ -22,7 +22,7 @@ class ItemToolSearchOutputParam(TypedDict, total=False): status: Optional[ItemStatus] - call_id: Optional[Optional[str]] + call_id: Optional[str] """Hosted search emits null. Replayed hosted history must omit this field or set it to null.""" execution: Optional[ToolSearchExecution] diff --git a/src/arkruntime/types/responses/tool_search_tool_param.py b/src/arkruntime/types/responses/tool_search_tool_param.py index 997df1e..4f27736 100644 --- a/src/arkruntime/types/responses/tool_search_tool_param.py +++ b/src/arkruntime/types/responses/tool_search_tool_param.py @@ -16,7 +16,7 @@ class ToolSearchToolParam(TypedDict, total=False): type: Required[Literal["tool_search"]] - execution: Optional[Optional[ToolSearchExecution]] + execution: Optional[ToolSearchExecution] """Omitted, null or empty means server. Client execution uses full-schema fallback.""" description: Optional[str] diff --git a/src/arkruntime/types/session/__init__.py b/src/arkruntime/types/session/__init__.py index 2770de0..e687af5 100644 --- a/src/arkruntime/types/session/__init__.py +++ b/src/arkruntime/types/session/__init__.py @@ -6,6 +6,10 @@ from __future__ import annotations +from .agent_compact_threshold import AgentCompactThreshold +from .agent_context_management_instructions import AgentContextManagementInstructions +from .agent_context_management_policies import AgentContextManagementPolicies +from .agent_context_management_policy import AgentContextManagementPolicy from .agent_identifier import AgentIdentifier from .agent_ref import AgentRef from .agent_with_upgrades import AgentWithUpgrades @@ -93,6 +97,7 @@ ManagedAgentsUserToolResultEventParams, ) from .managed_agents_video_block import ManagedAgentsVideoBlock +from .memory_store_instructions import MemoryStoreInstructions from .model_overrides import ModelOverrides from .model_upgrades import ModelUpgrades from .override_token_limits import OverrideTokenLimits @@ -119,6 +124,10 @@ from .video_source import VideoSource __all__ = [ + "AgentCompactThreshold", + "AgentContextManagementInstructions", + "AgentContextManagementPolicies", + "AgentContextManagementPolicy", "AgentIdentifier", "AgentRef", "AgentWithUpgrades", @@ -188,6 +197,7 @@ "ManagedAgentsUserToolConfirmationEventParams", "ManagedAgentsUserToolResultEventParams", "ManagedAgentsVideoBlock", + "MemoryStoreInstructions", "ModelOverrides", "ModelUpgrades", "OverrideTokenLimits", diff --git a/src/arkruntime/types/session/_init_extras_shim.py b/src/arkruntime/types/session/_init_extras_shim.py index f7e02ab..67a4ea0 100644 --- a/src/arkruntime/types/session/_init_extras_shim.py +++ b/src/arkruntime/types/session/_init_extras_shim.py @@ -13,6 +13,7 @@ from .session_stream_shim import ( ListSessionEventsResponse, + ManagedAgentsAgentCustomToolUseEvent, ManagedAgentsAgentMCPToolResultEvent, ManagedAgentsAgentMCPToolUseEvent, ManagedAgentsAgentMessageEvent, @@ -51,6 +52,7 @@ __all__ = [ "ListSessionEventsResponse", + "ManagedAgentsAgentCustomToolUseEvent", "ManagedAgentsAgentMCPToolResultEvent", "ManagedAgentsAgentMCPToolUseEvent", "ManagedAgentsAgentMessageEvent", diff --git a/src/arkruntime/types/session/agent_compact_threshold.py b/src/arkruntime/types/session/agent_compact_threshold.py new file mode 100644 index 0000000..bbedf8d --- /dev/null +++ b/src/arkruntime/types/session/agent_compact_threshold.py @@ -0,0 +1,17 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pydantic import Field +from typing_extensions import Annotated, TypeAliasType + +AgentCompactThreshold = TypeAliasType( + "AgentCompactThreshold", Annotated[int, Field(ge=50000)] +) +""" +上下文压缩阈值;显式配置时至少为 50,000 token。 +""" diff --git a/src/arkruntime/types/session/agent_context_management_instructions.py b/src/arkruntime/types/session/agent_context_management_instructions.py new file mode 100644 index 0000000..206721f --- /dev/null +++ b/src/arkruntime/types/session/agent_context_management_instructions.py @@ -0,0 +1,17 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pydantic import Field +from typing_extensions import Annotated, TypeAliasType + +AgentContextManagementInstructions = TypeAliasType( + "AgentContextManagementInstructions", Annotated[str, Field(max_length=100000)] +) +""" +自定义摘要指令,最长 100,000 字符。 +""" diff --git a/src/arkruntime/types/session/agent_context_management_policies.py b/src/arkruntime/types/session/agent_context_management_policies.py new file mode 100644 index 0000000..20b027d --- /dev/null +++ b/src/arkruntime/types/session/agent_context_management_policies.py @@ -0,0 +1,22 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import List + +from pydantic import Field +from typing_extensions import Annotated, TypeAliasType + +from .agent_context_management_policy import AgentContextManagementPolicy + +AgentContextManagementPolicies = TypeAliasType( + "AgentContextManagementPolicies", + Annotated[List[AgentContextManagementPolicy], Field(max_length=1)], +) +""" +Agent 上下文管理策略列表,当前最多一条策略。 +""" diff --git a/src/arkruntime/types/session/agent_context_management_policy.py b/src/arkruntime/types/session/agent_context_management_policy.py new file mode 100644 index 0000000..bab2f03 --- /dev/null +++ b/src/arkruntime/types/session/agent_context_management_policy.py @@ -0,0 +1,33 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Literal, Optional + +from arkruntime._models import BaseModel + +from .agent_compact_threshold import AgentCompactThreshold +from .agent_context_management_instructions import AgentContextManagementInstructions + + +class AgentContextManagementPolicy(BaseModel): + """ + Session 创建期可覆写的 Agent 上下文管理策略。 + """ + + type: Literal["compaction"] + """ + 策略类型,当前固定为 `compaction`。 + """ + compact_threshold: Optional[AgentCompactThreshold] = None + """ + 触发压缩的上下文 token 阈值;省略时由运行时使用默认值。 + """ + instructions: Optional[AgentContextManagementInstructions] = None + """ + 自定义摘要指令;省略时由运行时使用内置提示词。 + """ diff --git a/src/arkruntime/types/session/agent_ref.py b/src/arkruntime/types/session/agent_ref.py index 7eae635..ced97c1 100644 --- a/src/arkruntime/types/session/agent_ref.py +++ b/src/arkruntime/types/session/agent_ref.py @@ -10,6 +10,7 @@ from arkruntime._models import BaseModel +from .agent_context_management_policies import AgentContextManagementPolicies from .model_overrides import ModelOverrides @@ -51,6 +52,11 @@ class AgentRef(BaseModel): """ 多 Agent 配置覆写。 """ + context_management: Optional[AgentContextManagementPolicies] = None + """ + 上下文管理策略覆写(整体替换)。省略或 null 表示继承;显式 [] 表示清除 + 自定义策略,随后由运行时使用默认行为。 + """ display_name: Optional[str] = None """ Session 响应中冻结的 Agent 展示名。 diff --git a/src/arkruntime/types/session/managed_agents_user_custom_tool_result_event_params.py b/src/arkruntime/types/session/managed_agents_user_custom_tool_result_event_params.py index 13531de..e5c1bd5 100644 --- a/src/arkruntime/types/session/managed_agents_user_custom_tool_result_event_params.py +++ b/src/arkruntime/types/session/managed_agents_user_custom_tool_result_event_params.py @@ -39,6 +39,13 @@ class ManagedAgentsUserCustomToolResultEventParams(BaseModel): """ session_thread_id: Optional[str] = None """ - 目标 session_thread_id。 + 目标 session_thread_id;可选,且不要求客户端回填。 + + 缺省(字段缺失或空串)不是错误:服务端按 `custom_tool_use_id` 反查该次工具 + 调用的权威归属 thread(multi-agent 下可能是子 thread),把结果路由回它。显式 + 传入时必须与服务端反查结果一致,不一致是 typed error。 + + self-hosted 宿主按事件投递顺序串行执行工具,不需要 thread 身份,正确路由不依赖 + 本字段。 """ type: Literal["user.custom_tool_result"] diff --git a/src/arkruntime/types/session/managed_agents_user_tool_confirmation_event_params.py b/src/arkruntime/types/session/managed_agents_user_tool_confirmation_event_params.py index 69c82a5..e08be35 100644 --- a/src/arkruntime/types/session/managed_agents_user_tool_confirmation_event_params.py +++ b/src/arkruntime/types/session/managed_agents_user_tool_confirmation_event_params.py @@ -37,7 +37,12 @@ class ManagedAgentsUserToolConfirmationEventParams(BaseModel): """ session_thread_id: Optional[str] = None """ - 目标 session_thread_id。 + 目标 session_thread_id;可选,且不要求客户端回填。 + + 缺省(字段缺失或空串)不是错误:服务端按 `tool_use_id` 反查该次工具调用的权威 + 归属 thread(multi-agent 下可能是子 thread),把授权结果路由回它。显式传入时 + 必须与服务端反查结果一致,不一致是 typed error;客户端不得用该字段把结果投到 + 别的 thread。 """ turn_id: Optional[str] = None """ diff --git a/src/arkruntime/types/session/managed_agents_user_tool_result_event_params.py b/src/arkruntime/types/session/managed_agents_user_tool_result_event_params.py index e951259..53d8e0c 100644 --- a/src/arkruntime/types/session/managed_agents_user_tool_result_event_params.py +++ b/src/arkruntime/types/session/managed_agents_user_tool_result_event_params.py @@ -39,6 +39,14 @@ class ManagedAgentsUserToolResultEventParams(BaseModel): """ session_thread_id: Optional[str] = None """ - 目标 session_thread_id。 + 目标 session_thread_id;可选,且不要求客户端回填。 + + 缺省(字段缺失或空串)不是错误:服务端按 `tool_use_id` 反查该次工具调用的权威 + 归属 thread(multi-agent 下可能是子 thread),把结果路由回它。显式传入时必须与 + 服务端反查结果一致,不一致是 typed error。 + + self-hosted 宿主按事件投递顺序串行执行工具,不需要 thread 身份; + `agent.tool_use` 的 wire 上可能不带 `session_thread_id`,也可能由服务端 + 为投影补全。宿主不得依赖该字段,正确路由始终不依赖本字段。 """ type: Literal["user.tool_result"] diff --git a/src/arkruntime/types/session/memory_store_instructions.py b/src/arkruntime/types/session/memory_store_instructions.py new file mode 100644 index 0000000..2743a04 --- /dev/null +++ b/src/arkruntime/types/session/memory_store_instructions.py @@ -0,0 +1,17 @@ +# Generated by datamodel-code-generator from ark-apis typespec/openapi. +# DO NOT EDIT — regenerate with `make gen-py-` in ark-apis. +# +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from pydantic import Field +from typing_extensions import Annotated, TypeAliasType + +MemoryStoreInstructions = TypeAliasType( + "MemoryStoreInstructions", Annotated[str, Field(max_length=4096)] +) +""" +MemoryStore 会话级使用指导,最长 4096 个 Unicode 字符。 +""" diff --git a/src/arkruntime/types/session/model_overrides.py b/src/arkruntime/types/session/model_overrides.py index 533258a..e6b0285 100644 --- a/src/arkruntime/types/session/model_overrides.py +++ b/src/arkruntime/types/session/model_overrides.py @@ -61,8 +61,12 @@ class ModelOverrides(BaseModel): """ headers: Optional[Dict[str, str]] = None """ - 自定义模型请求 header(整组覆写),例如 `Authorization` 或 - `X-Trial-Id`;key / value 均为字符串。 + 自定义模型请求 header(整组覆写),例如 `X-Trial-Id`;key / value 均为 + 字符串。 + + 新接入只允许非认证类静态 header。认证 header 必须通过 SendEvent 请求上的 + `X-User-Model-Header-*` 按请求提供,不得写入本字段。服务端对历史请求的 + 兼容行为不构成新客户端可依赖的公开能力。 """ token_limits: Optional[OverrideTokenLimits] = None """ diff --git a/src/arkruntime/types/session/model_upgrades.py b/src/arkruntime/types/session/model_upgrades.py index 546890f..8f5d20c 100644 --- a/src/arkruntime/types/session/model_upgrades.py +++ b/src/arkruntime/types/session/model_upgrades.py @@ -6,10 +6,12 @@ from __future__ import annotations -from typing import Optional +from typing import Dict, Optional from arkruntime._models import BaseModel +from .override_token_limits import OverrideTokenLimits + class ModelUpgrades(BaseModel): """ @@ -50,3 +52,33 @@ class ModelUpgrades(BaseModel): """ 服务档位。 """ + provider: Optional[str] = None + """ + 目标模型提供方。 + + 切到非 `ark` provider 时,`base_url` 与 `token_limits` 必填(可继承 + 当前 Session 快照中已有的值,否则返回 400);`ark` provider 不允许 + 指定 `token_limits` 或非空 `base_url`。 + """ + protocol: Optional[str] = None + """ + 目标模型接入协议;取值须命中服务端维护的接入协议白名单,非法值返回 + 400。 + """ + base_url: Optional[str] = None + """ + 自定义模型接入点 base URL。配合非 `ark` 的 `provider` 使用; + `ark` provider 指定非空值时返回 400。 + """ + headers: Optional[Dict[str, str]] = None + """ + 模型静态请求 header(整组覆写)。 + + 仅用于非认证类静态 header;认证 header 必须由 SendEvent 请求上的 + `X-User-Model-Header-*` 动态提供。 + """ + token_limits: Optional[OverrideTokenLimits] = None + """ + 模型 token 限制。非 `ark` provider 提供时必须包含三个字段;可继承 + 当前 Session 快照中已有的值。 + """ diff --git a/src/arkruntime/types/session/session_resource.py b/src/arkruntime/types/session/session_resource.py index d37416c..306b73c 100644 --- a/src/arkruntime/types/session/session_resource.py +++ b/src/arkruntime/types/session/session_resource.py @@ -10,6 +10,7 @@ from arkruntime._models import BaseModel +from .memory_store_instructions import MemoryStoreInstructions from .session_resource_access import SessionResourceAccess from .session_resource_type import SessionResourceType @@ -44,6 +45,12 @@ class SessionResource(BaseModel): """ `memory_store` 类型:访问权限。 """ + instructions: Optional[MemoryStoreInstructions] = None + """ + `memory_store` 类型:会话使用该记忆库时遵循的指导,最长 4096 个 Unicode 字符。 + 创建会话时可指定;省略、null 或空字符串均表示无使用指导,响应中省略空值。 + 不适用于其他资源类型;与 Agent 上下文压缩策略的摘要指令不同。 + """ mount_path: Optional[str] = None """ 在 sandbox 中的挂载路径。 diff --git a/src/arkruntime/types/session/session_stream_shim.py b/src/arkruntime/types/session/session_stream_shim.py index 9dde9d8..85898ad 100644 --- a/src/arkruntime/types/session/session_stream_shim.py +++ b/src/arkruntime/types/session/session_stream_shim.py @@ -258,6 +258,17 @@ class ManagedAgentsAgentToolUseEvent(_SessionEventBase): input: Any = None +class ManagedAgentsAgentCustomToolUseEvent(_SessionEventBase): + """Agent invoked a custom tool owned by the caller. The runtime waits + for a matching ``user.custom_tool_result`` before continuing. + """ + + session_thread_id: str = "" + custom_tool_use_id: str = "" + name: str = "" + input: Any = None + + class ManagedAgentsAgentToolResultEvent(_SessionEventBase): """Tool's result (post ``user.tool_result`` if the tool required client-side execution, or runtime-owned if it was a builtin/MCP tool). @@ -405,6 +416,7 @@ class ManagedAgentsUnknownSessionEvent(_SessionEventBase): "agent.message": ManagedAgentsAgentMessageEvent, "agent.thinking": ManagedAgentsAgentThinkingEvent, "agent.tool_use": ManagedAgentsAgentToolUseEvent, + "agent.custom_tool_use": ManagedAgentsAgentCustomToolUseEvent, "agent.tool_result": ManagedAgentsAgentToolResultEvent, "agent.mcp_tool_use": ManagedAgentsAgentMCPToolUseEvent, "agent.mcp_tool_result": ManagedAgentsAgentMCPToolResultEvent, diff --git a/tests/selfhosted/test_session_tool_runner.py b/tests/selfhosted/test_session_tool_runner.py index e62ea04..4380486 100644 --- a/tests/selfhosted/test_session_tool_runner.py +++ b/tests/selfhosted/test_session_tool_runner.py @@ -8,7 +8,14 @@ import pytest -from arkruntime.selfhosted import Event, ListEventsResponse, SessionToolRunner, SessionToolRunnerOptions +import arkruntime.selfhosted.session_tool_runner as session_tool_runner_module +from arkruntime.selfhosted import ( + Event, + ListEventsResponse, + SessionTerminated, + SessionToolRunner, + SessionToolRunnerOptions, +) from arkruntime.selfhosted.tool_result_store import ToolCallStoreDecision from arkruntime.selfhosted.tools import FunctionTool, ToolContext, ToolSet, text_result @@ -158,6 +165,179 @@ def test_duplicate_stream_idle_event_does_not_reset_idle_deadline(tmp_path) -> N assert runner._state.idle_armed_at == armed_at +def test_stream_runner_logs_lifecycle_and_timeout(tmp_path, caplog) -> None: + class StreamAPI: + timeout = None + + def list_events(self, _session_id, **_kwargs): + return ListEventsResponse() + + def stream_events(self, _session_id, *, timeout): + self.timeout = timeout + yield Event(id="terminated", type="session.status_terminated") + + api = StreamAPI() + runner = SessionToolRunner( + api, + "session-1", + SessionToolRunnerOptions( + work_id="work-1", + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + stream_timeout_seconds=12.5, + initial_session_status="running", + ), + ) + + with caplog.at_level("INFO"), pytest.raises(SessionTerminated): + runner.run() + + assert api.timeout == 12.5 + assert "session runner started" in caplog.text + assert "session stream connecting" in caplog.text + assert "attempt=1 timeout=12.500s" in caplog.text + assert "session first business event" in caplog.text + assert "session status changed" in caplog.text + assert "session runner stopped" in caplog.text + + +def test_stream_runner_logs_reconnect_after_transport_error(tmp_path, caplog, monkeypatch) -> None: + class StreamAPI: + attempts = 0 + + def list_events(self, _session_id, **_kwargs): + return ListEventsResponse() + + def stream_events(self, _session_id, *, timeout): + self.attempts += 1 + if self.attempts == 1: + raise RuntimeError("temporary stream failure") + yield Event(id="terminated", type="session.status_terminated") + + monkeypatch.setattr(session_tool_runner_module, "_jitter", lambda _seconds: 0.0) + api = StreamAPI() + runner = SessionToolRunner( + api, + "session-1", + SessionToolRunnerOptions( + work_id="work-1", + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + ), + ) + + with caplog.at_level("INFO"), pytest.raises(SessionTerminated): + runner.run() + + assert api.attempts == 2 + assert "session stream disconnected" in caplog.text + assert "temporary stream failure" in caplog.text + assert "attempt=2" in caplog.text + + +def test_running_session_logs_no_progress_warning(tmp_path, caplog, monkeypatch) -> None: + monkeypatch.setattr(session_tool_runner_module, "STREAM_PROGRESS_WARN_SECONDS", 0.0) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + work_id="work-1", + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + initial_session_status="running", + ), + ) + + with caplog.at_level("WARNING"): + runner._state.maybe_log_no_progress() + + assert "session has no business progress" in caplog.text + assert "events=0" in caplog.text + + +def test_entering_running_resets_no_progress_baseline(tmp_path, caplog, monkeypatch) -> None: + monkeypatch.setattr(session_tool_runner_module, "STREAM_PROGRESS_WARN_SECONDS", 60.0) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + work_id="work-1", + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + initial_session_status="idle", + ), + ) + runner._state.last_business_event_at -= 61 + + with caplog.at_level("WARNING"): + runner._state.handle_stream_event(Event(type="session.status_running")) + runner._state.maybe_log_no_progress() + + assert "session has no business progress" not in caplog.text + + +def test_span_progress_frame_does_not_hide_business_no_progress_warning(tmp_path, caplog, monkeypatch) -> None: + monkeypatch.setattr(session_tool_runner_module, "STREAM_PROGRESS_WARN_SECONDS", 60.0) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + work_id="work-1", + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + initial_session_status="running", + ), + ) + runner._state.last_business_event_at -= 61 + runner._state.last_progress_warning_at = 0.0 + + with caplog.at_level("WARNING"): + runner._state.handle_stream_event(Event(id="span-1", type="span.outcome_evaluation_ongoing")) + runner._state.maybe_log_no_progress() + + assert "session has no business progress" in caplog.text + + +def test_keyless_event_does_not_refresh_progress_or_event_count(tmp_path, caplog, monkeypatch) -> None: + monkeypatch.setattr(session_tool_runner_module, "STREAM_PROGRESS_WARN_SECONDS", 0.0) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + work_id="work-1", + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + initial_session_status="running", + ), + ) + + with caplog.at_level("WARNING"): + runner._state.handle_stream_event(Event(type="agent.message")) + runner._state.maybe_log_no_progress() + + assert runner._state.event_count == 0 + assert "session has no business progress" in caplog.text + + +def test_idle_session_does_not_log_no_progress_warning(tmp_path, caplog, monkeypatch) -> None: + monkeypatch.setattr(session_tool_runner_module, "STREAM_PROGRESS_WARN_SECONDS", 0.0) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + work_id="work-1", + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + initial_session_status="idle", + ), + ) + + with caplog.at_level("WARNING"): + runner._state.maybe_log_no_progress() + + assert "session has no business progress" not in caplog.text + + def test_reconcile_does_not_reset_idle_deadline_for_seen_history(tmp_path) -> None: runner = SessionToolRunner( object(), @@ -642,6 +822,448 @@ def test_current_blocker_reuses_recovered_result_without_reexecution(tmp_path) - assert not store.discarded +def test_earlier_requires_action_survives_later_idle(tmp_path) -> None: + executions = [] + tool = FunctionTool( + "custom", + lambda value, _context: executions.append(value) or text_result("ok"), + ) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + custom_tools={"custom": tool}, + max_idle_seconds=60, + ), + ) + runner._state.retry_send_event = lambda _event, _call_id: True + + runner._state.process_listed_events( + [ + Event( + id="use-1", + type="agent.custom_tool_use", + name="custom", + custom_tool_use_id="call-1", + input={"value": "one"}, + ), + Event( + id="idle-1", + type="session.status_idle", + stop_reason={"type": "requires_action", "event_ids": ["use-1"]}, + ), + Event(id="user-2", type="user.message", content=[]), + Event( + id="use-2", + type="agent.custom_tool_use", + name="custom", + custom_tool_use_id="call-2", + input={"value": "two"}, + ), + Event( + id="idle-2", + type="session.status_idle", + stop_reason={"type": "requires_action", "event_ids": ["use-2"]}, + ), + ], + reconcile=False, + ) + + _finish_execution(runner) + _finish_execution(runner) + + assert executions == [{"value": "one"}, {"value": "two"}] + + +def test_known_remote_blocker_does_not_trigger_sole_tool_fallback(tmp_path) -> None: + executions = [] + tool = FunctionTool( + "local", + lambda _input, _context: executions.append(True) or text_result("ok"), + ) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + custom_tools={"local": tool}, + max_idle_seconds=60, + ), + ) + runner._state.retry_send_event = lambda _event, _call_id: True + + runner._state.process_listed_events( + [ + Event( + id="local-use", + type="agent.custom_tool_use", + name="local", + custom_tool_use_id="local-call", + input={}, + ), + Event(id="idle-end", type="session.status_idle", stop_reason={"type": "end_turn"}), + Event(id="running", type="session.status_running"), + Event( + id="remote-use", + type="agent.custom_tool_use", + name="remote", + custom_tool_use_id="remote-call", + session_thread_id="child", + input={}, + ), + Event( + id="idle-requires-action", + type="session.status_idle", + stop_reason={"type": "requires_action", "event_ids": ["remote-use"]}, + ), + ], + reconcile=True, + ) + + assert not executions + assert [result.tool_use_id for result in runner.results] == ["remote-call"] + assert runner.results[0].posted is False + + +def test_running_status_clears_abandoned_pending_blockers(tmp_path) -> None: + executions = [] + tool = FunctionTool( + "custom", + lambda _input, _context: executions.append(True) or text_result("ok"), + ) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + custom_tools={"custom": tool}, + max_idle_seconds=60, + ), + ) + runner._state.retry_send_event = lambda _event, _call_id: True + + runner._state.process_listed_events( + [ + Event( + id="tool-use", + type="agent.custom_tool_use", + name="custom", + custom_tool_use_id="call-1", + input={}, + ), + Event( + id="idle-requires-action", + type="session.status_idle", + stop_reason={"type": "requires_action", "event_ids": ["tool-use"]}, + ), + Event(id="user-message", type="user.message", content=[]), + Event(id="running", type="session.status_running"), + Event(id="idle-end-turn", type="session.status_idle", stop_reason={"type": "end_turn"}), + ], + reconcile=True, + ) + + assert not executions + assert not runner._state.pending_blocking_event_ids + assert not runner._state.tool_use_call_id_by_event_id + + +def test_later_idle_drops_unmatched_pending_blockers(tmp_path) -> None: + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions(tools=ToolSet(), tool_context=ToolContext(workdir=str(tmp_path))), + ) + + runner._state.process_listed_events( + [ + Event( + id="idle-requires-action", + type="session.status_idle", + stop_reason={"type": "requires_action", "event_ids": ["foreign-use"]}, + ), + Event(id="running", type="session.status_running"), + Event(id="idle-end-turn", type="session.status_idle", stop_reason={"type": "end_turn"}), + ], + reconcile=True, + ) + + assert not runner._state.pending_blocking_event_ids + + +def test_reconcile_records_external_tool_use_only_once(tmp_path) -> None: + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions(tools=ToolSet(), tool_context=ToolContext(workdir=str(tmp_path))), + ) + events = [ + Event( + id="tool-use", + type="agent.custom_tool_use", + name="remote", + custom_tool_use_id="call-1", + input={}, + ), + Event( + id="idle", + type="session.status_idle", + stop_reason={"type": "requires_action", "event_ids": ["tool-use"]}, + ), + ] + + for _ in range(4): + runner._state.process_listed_events(events, reconcile=True) + + assert [result.tool_use_id for result in runner.results] == ["call-1"] + + +def test_reconcile_records_pending_confirmation_tool_use_only_once(tmp_path) -> None: + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + tools=ToolSet([FunctionTool("approval", lambda _input, _context: text_result("ok"))]), + tool_context=ToolContext(workdir=str(tmp_path)), + ), + ) + events = [ + Event( + id="tool-use", + type="agent.tool_use", + name="approval", + tool_use_id="call-1", + evaluated_permission="ask", + input={}, + ), + Event( + id="idle", + type="session.status_idle", + stop_reason={"type": "requires_action", "event_ids": ["tool-use"]}, + ), + ] + + for _ in range(4): + runner._state.process_listed_events(events, reconcile=True) + + assert [result.tool_use_id for result in runner.results] == ["call-1"] + assert runner.results[0].posted is False + + +def test_failed_result_delivery_still_arms_idle_deadline(tmp_path) -> None: + executions = [] + store = _RecordingStore() + tool = FunctionTool( + "custom", + lambda _input, _context: executions.append(True) or text_result("ok"), + ) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + custom_tools={"custom": tool}, + result_store=store, + max_idle_seconds=0.01, + ), + ) + runner._state.retry_send_event = lambda _event, _call_id: False + + runner._state.process_listed_events( + [ + Event( + id="tool-use", + type="agent.custom_tool_use", + name="custom", + custom_tool_use_id="call-1", + input={}, + ), + Event( + id="idle-requires-action", + type="session.status_idle", + stop_reason={"type": "requires_action", "event_ids": ["tool-use"]}, + ), + Event(id="idle-end-turn", type="session.status_idle", stop_reason={"type": "end_turn"}), + ], + reconcile=True, + ) + _finish_execution(runner) + + assert executions == [True] + assert "call-1" in runner._state.pending_results + assert runner._state.idle_armed_at + time.sleep(0.02) + assert runner._state.idle_expired() + + +def test_empty_ledger_replays_unresolved_tool_use(tmp_path) -> None: + executions = [] + store = _RecordingStore() + tool = FunctionTool( + "custom", + lambda _input, _context: executions.append(True) or text_result("ok"), + ) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + custom_tools={"custom": tool}, + result_store=store, + ), + ) + sent = [] + runner._state.retry_send_event = lambda event, _call_id: sent.append(event) or True + + runner._state.process_listed_events( + [ + Event(id="running", type="session.status_running"), + Event( + id="sevt-use-1", + type="agent.custom_tool_use", + name="custom", + custom_tool_use_id="call-1", + input={}, + ), + Event( + id="idle", + type="session.status_idle", + stop_reason={"type": "requires_action", "event_ids": ["sevt-use-1"]}, + ), + ], + reconcile=True, + ) + _finish_execution(runner) + + assert executions == [True] + assert [event.custom_tool_use_id for event in sent] == ["call-1"] + assert store.marked == ["call-1"] + + +def test_unmatched_requires_action_replays_sole_owned_tool(tmp_path) -> None: + executions = [] + tool = FunctionTool( + "custom", + lambda _input, _context: executions.append(True) or text_result("ok"), + ) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + custom_tools={"custom": tool}, + ), + ) + sent = [] + runner._state.retry_send_event = lambda event, _call_id: sent.append(event) or True + + runner._state.process_listed_events( + [ + Event( + id="idle", type="session.status_idle", stop_reason={"type": "requires_action", "event_ids": ["opaque"]} + ), + Event( + id="sevt-use-1", + type="agent.custom_tool_use", + name="custom", + custom_tool_use_id="call-1", + input={}, + ), + Event(id="running", type="session.status_running"), + ], + reconcile=True, + ) + _finish_execution(runner) + + assert executions == [True] + assert [event.custom_tool_use_id for event in sent] == ["call-1"] + assert not runner._state.requires_action_fallback + + +def test_unmatched_requires_action_does_not_guess_among_multiple_tools(tmp_path) -> None: + executions = [] + tool = FunctionTool( + "custom", + lambda _input, _context: executions.append(True) or text_result("ok"), + ) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + custom_tools={"custom": tool}, + ), + ) + runner._state.retry_send_event = lambda _event, _call_id: True + + runner._state.process_listed_events( + [ + Event(id="running", type="session.status_running"), + Event(id="sevt-use-1", type="agent.custom_tool_use", name="custom", custom_tool_use_id="call-1", input={}), + Event(id="sevt-use-2", type="agent.custom_tool_use", name="custom", custom_tool_use_id="call-2", input={}), + Event( + id="idle", type="session.status_idle", stop_reason={"type": "requires_action", "event_ids": ["opaque"]} + ), + ], + reconcile=True, + ) + + assert not executions + assert not runner._state.requires_action_fallback + + +def test_current_blocker_matches_event_id_to_tool_use_id(tmp_path) -> None: + executions = [] + store = _RecordingStore() + tool = FunctionTool("custom", lambda _input, _context: executions.append(True) or text_result("ok")) + runner = SessionToolRunner( + object(), + "session-1", + SessionToolRunnerOptions( + tools=ToolSet(), + tool_context=ToolContext(workdir=str(tmp_path)), + custom_tools={"custom": tool}, + result_store=store, + ), + ) + sent = [] + result = Event(type="user.custom_tool_result", custom_tool_use_id="call-1") + runner._state.pending_results["call-1"] = result + runner._state.recovered_results.add("call-1") + runner._state.retry_send_event = lambda event, _call_id: sent.append(event) or True + + runner._state.process_listed_events( + [ + Event( + id="sevt-use-1", + type="agent.custom_tool_use", + name="custom", + custom_tool_use_id="call-1", + input={}, + ), + Event( + id="idle", + type="session.status_idle", + stop_reason={"type": "requires_action", "event_ids": ["sevt-use-1"]}, + ), + ], + reconcile=True, + ) + + assert not executions + assert sent == [result] + assert store.marked == ["call-1"] + assert not store.discarded + + def test_recovered_result_waits_for_authoritative_status(tmp_path) -> None: executions = [] tool = FunctionTool("custom", lambda _input, _context: executions.append(True) or text_result("ok")) diff --git a/tests/selfhosted/test_worker.py b/tests/selfhosted/test_worker.py index 6fcd3ef..590c85a 100644 --- a/tests/selfhosted/test_worker.py +++ b/tests/selfhosted/test_worker.py @@ -254,6 +254,39 @@ def test_worker_tool_timeout_overrides_tool_context(tmp_path) -> None: assert context.tool_timeout_seconds == 0.02 +def test_worker_passes_stream_configuration_and_initial_status(tmp_path, monkeypatch) -> None: + captured = {} + + class Runner: + def __init__(self, _api, _session_id, options): + captured["options"] = options + + def run(self): + return None + + class API(_ActiveHeartbeatAPI): + def get_session(self, session_id): + return Session(id=session_id, raw={"status": "running"}) + + monkeypatch.setattr("arkruntime.selfhosted.worker.SessionToolRunner", Runner) + worker = EnvironmentWorker( + API(), + EnvironmentWorkerOptions( + environment_id="env-1", + workdir=str(tmp_path), + prefer_stream=False, + stream_timeout_seconds=12.5, + ), + ) + + worker.handle_item(HandleItemOptions(work_id="work-1", environment_id="env-1", session_id="session-1")) + + options = captured["options"] + assert options.prefer_stream is False + assert options.stream_timeout_seconds == 12.5 + assert options.initial_session_status == "running" + + @pytest.mark.parametrize("timeout", [None, 0, -1]) def test_worker_nonpositive_tool_timeout_preserves_tool_context(tmp_path, timeout) -> None: worker = EnvironmentWorker( @@ -279,6 +312,8 @@ def test_worker_options_preserve_legacy_positional_order() -> None: assert options.custom_tools is custom_tools assert options.logger is logger assert options.tool_timeout_seconds is None + assert options.prefer_stream is True + assert options.stream_timeout_seconds == 30.0 def test_worker_uses_configured_workdir(tmp_path) -> None: diff --git a/tests/test_ark_apis_agent_archive.py b/tests/test_ark_apis_agent_archive.py new file mode 100644 index 0000000..053f1d5 --- /dev/null +++ b/tests/test_ark_apis_agent_archive.py @@ -0,0 +1,103 @@ +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +import asyncio +import secrets + +import httpx +import pytest + +from arkruntime import Ark, AsyncArk + + +@pytest.mark.parametrize("include", [None, False, True]) +def test_agent_archive_transport(include): + requests = [] + + def respond(request): + requests.append(request) + data = ( + { + "id": "resource/1", + "type": "agent", + "archived_at": "2026-10-29T00:00:00Z", + "created_at": "2026-10-29T00:00:00Z", + "updated_at": "2026-10-29T00:00:00Z", + "name": "archive test", + "version": 1, + } + if request.method == "POST" + else {"data": [], "total": 0} + ) + return httpx.Response(200, json=data) + + with Ark( + api_key=secrets.token_hex(16), + base_url="https://example.test/api/v3", + http_client=httpx.Client(transport=httpx.MockTransport(respond)), + ) as client: + resource = client.agents + result = resource.archive("resource/1") + assert result.id == "resource/1" + assert result.archived_at == "2026-10-29T00:00:00Z" + request = requests[-1] + assert request.method == "POST" + assert request.url.raw_path == b"/api/v3/agents/resource%2F1/archive" + assert request.content == b"" + if include is None: + resource.list() + else: + resource.list(include_archived=include) + request = requests[-1] + assert request.method == "GET" + assert request.url.params.get("include_archived") == (None if include is None else str(include).lower()) + with pytest.raises(ValueError, match="agent_id"): + resource.archive("") + + +@pytest.mark.parametrize("include", [None, False, True]) +def test_async_agent_archive_transport(include): + async def run(): + requests = [] + + def respond(request): + requests.append(request) + data = ( + { + "id": "resource/1", + "type": "agent", + "archived_at": "2026-10-29T00:00:00Z", + "created_at": "2026-10-29T00:00:00Z", + "updated_at": "2026-10-29T00:00:00Z", + "name": "archive test", + "version": 1, + } + if request.method == "POST" + else {"data": [], "total": 0} + ) + return httpx.Response(200, json=data) + + async with AsyncArk( + api_key=secrets.token_hex(16), + base_url="https://example.test/api/v3", + http_client=httpx.AsyncClient(transport=httpx.MockTransport(respond)), + ) as client: + resource = client.agents + result = await resource.archive("resource/1") + assert result.id == "resource/1" + assert result.archived_at == "2026-10-29T00:00:00Z" + assert requests[-1].method == "POST" + assert requests[-1].url.raw_path == b"/api/v3/agents/resource%2F1/archive" + assert requests[-1].content == b"" + if include is None: + await resource.list() + else: + await resource.list(include_archived=include) + assert requests[-1].method == "GET" + assert requests[-1].url.params.get("include_archived") == ( + None if include is None else str(include).lower() + ) + with pytest.raises(ValueError, match="agent_id"): + await resource.archive("") + + asyncio.run(run()) diff --git a/tests/test_ark_apis_environment_archive.py b/tests/test_ark_apis_environment_archive.py new file mode 100644 index 0000000..146e420 --- /dev/null +++ b/tests/test_ark_apis_environment_archive.py @@ -0,0 +1,101 @@ +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +import asyncio +import secrets + +import httpx +import pytest + +from arkruntime import Ark, AsyncArk + + +@pytest.mark.parametrize("include", [None, False, True]) +def test_environment_archive_transport(include): + requests = [] + + def respond(request): + requests.append(request) + data = ( + { + "id": "resource/1", + "type": "environment", + "archived_at": "2026-10-29T00:00:00Z", + "created_at": "2026-10-29T00:00:00Z", + "updated_at": "2026-10-29T00:00:00Z", + "name": "archive test", + } + if request.method == "POST" + else {"data": []} + ) + return httpx.Response(200, json=data) + + with Ark( + api_key=secrets.token_hex(16), + base_url="https://example.test/api/v3", + http_client=httpx.Client(transport=httpx.MockTransport(respond)), + ) as client: + resource = client.environments + result = resource.archive("resource/1") + assert result.id == "resource/1" + assert result.archived_at == "2026-10-29T00:00:00Z" + request = requests[-1] + assert request.method == "POST" + assert request.url.raw_path == b"/api/v3/environments/resource%2F1/archive" + assert request.content == b"" + if include is None: + resource.list() + else: + resource.list(include_archived=include) + request = requests[-1] + assert request.method == "GET" + assert request.url.params.get("include_archived") == (None if include is None else str(include).lower()) + with pytest.raises(ValueError, match="environment_id"): + resource.archive("") + + +@pytest.mark.parametrize("include", [None, False, True]) +def test_async_environment_archive_transport(include): + async def run(): + requests = [] + + def respond(request): + requests.append(request) + data = ( + { + "id": "resource/1", + "type": "environment", + "archived_at": "2026-10-29T00:00:00Z", + "created_at": "2026-10-29T00:00:00Z", + "updated_at": "2026-10-29T00:00:00Z", + "name": "archive test", + } + if request.method == "POST" + else {"data": []} + ) + return httpx.Response(200, json=data) + + async with AsyncArk( + api_key=secrets.token_hex(16), + base_url="https://example.test/api/v3", + http_client=httpx.AsyncClient(transport=httpx.MockTransport(respond)), + ) as client: + resource = client.environments + result = await resource.archive("resource/1") + assert result.id == "resource/1" + assert result.archived_at == "2026-10-29T00:00:00Z" + assert requests[-1].method == "POST" + assert requests[-1].url.raw_path == b"/api/v3/environments/resource%2F1/archive" + assert requests[-1].content == b"" + if include is None: + await resource.list() + else: + await resource.list(include_archived=include) + assert requests[-1].method == "GET" + assert requests[-1].url.params.get("include_archived") == ( + None if include is None else str(include).lower() + ) + with pytest.raises(ValueError, match="environment_id"): + await resource.archive("") + + asyncio.run(run()) diff --git a/tests/test_ark_apis_session_archive.py b/tests/test_ark_apis_session_archive.py new file mode 100644 index 0000000..aefd36c --- /dev/null +++ b/tests/test_ark_apis_session_archive.py @@ -0,0 +1,105 @@ +# Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +# SPDX-License-Identifier: Apache-2.0 + +import asyncio +import secrets + +import httpx +import pytest + +from arkruntime import Ark, AsyncArk + + +@pytest.mark.parametrize("include", [None, False, True]) +def test_session_archive_transport(include): + requests = [] + + def respond(request): + requests.append(request) + data = ( + { + "id": "resource/1", + "type": "session", + "archived_at": "2026-10-29T00:00:00Z", + "created_at": "2026-10-29T00:00:00Z", + "updated_at": "2026-10-29T00:00:00Z", + "status": "terminated", + "environment_id": "env-1", + "agent": {}, + } + if request.method == "POST" + else {"data": []} + ) + return httpx.Response(200, json=data) + + with Ark( + api_key=secrets.token_hex(16), + base_url="https://example.test/api/v3", + http_client=httpx.Client(transport=httpx.MockTransport(respond)), + ) as client: + resource = client.sessions + result = resource.archive("resource/1") + assert result.id == "resource/1" + assert result.archived_at == "2026-10-29T00:00:00Z" + request = requests[-1] + assert request.method == "POST" + assert request.url.raw_path == b"/api/v3/sessions/resource%2F1/archive" + assert request.content == b"" + if include is None: + resource.list() + else: + resource.list(include_archived=include) + request = requests[-1] + assert request.method == "GET" + assert request.url.params.get("include_archived") == (None if include is None else str(include).lower()) + with pytest.raises(ValueError, match="session_id"): + resource.archive("") + + +@pytest.mark.parametrize("include", [None, False, True]) +def test_async_session_archive_transport(include): + async def run(): + requests = [] + + def respond(request): + requests.append(request) + data = ( + { + "id": "resource/1", + "type": "session", + "archived_at": "2026-10-29T00:00:00Z", + "created_at": "2026-10-29T00:00:00Z", + "updated_at": "2026-10-29T00:00:00Z", + "status": "terminated", + "environment_id": "env-1", + "agent": {}, + } + if request.method == "POST" + else {"data": []} + ) + return httpx.Response(200, json=data) + + async with AsyncArk( + api_key=secrets.token_hex(16), + base_url="https://example.test/api/v3", + http_client=httpx.AsyncClient(transport=httpx.MockTransport(respond)), + ) as client: + resource = client.sessions + result = await resource.archive("resource/1") + assert result.id == "resource/1" + assert result.archived_at == "2026-10-29T00:00:00Z" + assert requests[-1].method == "POST" + assert requests[-1].url.raw_path == b"/api/v3/sessions/resource%2F1/archive" + assert requests[-1].content == b"" + if include is None: + await resource.list() + else: + await resource.list(include_archived=include) + assert requests[-1].method == "GET" + assert requests[-1].url.params.get("include_archived") == ( + None if include is None else str(include).lower() + ) + with pytest.raises(ValueError, match="session_id"): + await resource.archive("") + + asyncio.run(run()) diff --git a/tests/test_managed_agents_serialize.py b/tests/test_managed_agents_serialize.py new file mode 100644 index 0000000..ad4726f --- /dev/null +++ b/tests/test_managed_agents_serialize.py @@ -0,0 +1,93 @@ +import json + +import httpx +import pytest +from pydantic import ValidationError + +from arkruntime import Ark +from arkruntime._managed_agents_serialize import dump_body +from arkruntime.types.agent import ( + ModelConfig, + MultiagentPatch, + MultiagentSubagentsConfig, +) + + +def test_multiagent_update_preserves_explicit_null_and_empty_array() -> None: + body = dump_body( + { + "version": 1, + "multiagent": MultiagentPatch( + type="multiagent_20261001", + subagents=MultiagentSubagentsConfig( + type="enabled", + inline_agents=None, + predefined_agents=[], + ), + ), + } + ) + + assert json.loads(json.dumps(body)) == { + "version": 1, + "multiagent": { + "type": "multiagent_20261001", + "subagents": { + "type": "enabled", + "inline_agents": None, + "predefined_agents": [], + }, + }, + } + + +def test_multiagent_update_explicit_null_is_sent() -> None: + assert dump_body({"version": 1, "multiagent": None}) == { + "version": 1, + "multiagent": None, + } + + +def test_agent_update_wrapper_sends_explicit_multiagent_null() -> None: + bodies = [] + + def handle(request: httpx.Request) -> httpx.Response: + bodies.append(json.loads(request.content)) + return httpx.Response( + 200, + json={ + "id": "agent-1", + "type": "agent", + "name": "agent", + "version": 2, + "created_at": "2026-10-28T00:00:00Z", + "updated_at": "2026-10-28T00:00:00Z", + }, + ) + + with Ark( + api_key="placeholder", + http_client=httpx.Client(transport=httpx.MockTransport(handle)), + ) as client: + client.agents.update("agent-1", version=1, multiagent=None) + + assert bodies == [{"version": 1, "multiagent": None}] + + +@pytest.mark.parametrize( + "multiagent", + [ + {"type": "multiagent_20261001"}, + {"type": "multiagent_20261001", "subagents": None}, + ], +) +def test_agent_create_rejects_dynamic_multiagent_without_subagents( + multiagent: dict, +) -> None: + with Ark(api_key="placeholder") as client: + with pytest.raises(ValidationError, match="multiagent.subagents"): + client.agents.create( + name="agent", + model=ModelConfig(id="model-id"), + multiagent=multiagent, + ) diff --git a/tests/test_multiagent_gate_e2e.py b/tests/test_multiagent_gate_e2e.py new file mode 100644 index 0000000..512eb93 --- /dev/null +++ b/tests/test_multiagent_gate_e2e.py @@ -0,0 +1,78 @@ +import os +import time + +import pytest + +from arkruntime import Ark +from arkruntime.types.agent import ( + AgentRef, + ModelConfig, + MultiagentConfig, + MultiagentPatch, + MultiagentSubagentsConfig, +) + + +@pytest.mark.skipif( + not os.environ.get("ARK_STG_TOKEN") or not os.environ.get("ARK_E2E_BASE_URL"), + reason="ARK_STG_TOKEN and ARK_E2E_BASE_URL are required for Managed Agents E2E", +) +def test_multiagent_client_gate_against_stg() -> None: + client = Ark( + api_key=os.environ["ARK_STG_TOKEN"], + base_url=os.environ["ARK_E2E_BASE_URL"], + max_retries=0, + ) + name = f"sdk-gate-py-{int(time.time() * 1000)}" + created = None + try: + created = client.agents.create( + name=name, + model=ModelConfig(id=os.environ.get("ARK_E2E_MODEL_ID", "doubao-seed-1-6-250615")), + multiagent=MultiagentConfig( + type="multiagent_20261001", + subagents=MultiagentSubagentsConfig( + type="enabled", + predefined_agents=[AgentRef(type="self")], + ), + ), + ) + assert created.multiagent is not None + assert created.multiagent.subagents is not None + + fetched = client.agents.retrieve(created.id) + assert fetched.id == created.id + + updated = client.agents.update( + created.id, + version=created.version, + multiagent=MultiagentPatch( + type="multiagent_20261001", + subagents=MultiagentSubagentsConfig( + type="enabled", + predefined_agents=[], + ), + ), + ) + assert updated.version > created.version + + listed = client.agents.list(limit=100) + assert any(item.id == created.id for item in listed.data) + + legacy_headers = {"User-Agent": "ark-runtime-python/0.11.0"} + with pytest.raises(Exception, match="MULTIAGENT_UNSUPPORTED_BY_CLIENT"): + client.agents.retrieve(created.id, extra_headers=legacy_headers) + with pytest.raises(Exception, match="MULTIAGENT_UNSUPPORTED_BY_CLIENT"): + client.agents.update( + created.id, + version=updated.version, + description="must be rejected", + extra_headers=legacy_headers, + ) + + legacy_list = client.agents.list(limit=100, extra_headers=legacy_headers) + legacy_item = next(item for item in legacy_list.data if item.id == created.id) + assert legacy_item.multiagent is None + finally: + if created is not None: + client.agents.delete(created.id) diff --git a/tests/test_session_stream_shim.py b/tests/test_session_stream_shim.py new file mode 100644 index 0000000..04553a3 --- /dev/null +++ b/tests/test_session_stream_shim.py @@ -0,0 +1,79 @@ +import json +from importlib.metadata import version + +import httpx +import pytest + +from arkruntime import Ark, AsyncArk +from arkruntime._constants import CLIENT_REQUEST_HEADER +from arkruntime.resources.sessions.session_events import _decode_stream_frame +from arkruntime.types.session import ManagedAgentsAgentCustomToolUseEvent + + +def _assert_stream_headers(request: httpx.Request) -> None: + assert request.headers["Accept"] == "text/event-stream" + assert request.headers["Authorization"] == "Bearer placeholder" + assert request.headers["X-Ark-Env"] == "lane-test" + assert request.headers["X-Maas-Env"] == "multiagent" + assert request.headers["User-Agent"] == "ark-runtime-python/" + version("arkruntime") + assert request.headers[CLIENT_REQUEST_HEADER] + + +def test_session_stream_propagates_sdk_default_headers() -> None: + requests = [] + + def handle(request: httpx.Request) -> httpx.Response: + requests.append(request) + return httpx.Response(200, headers={"content-type": "text/event-stream"}, text="") + + with Ark( + api_key="placeholder", + http_client=httpx.Client(transport=httpx.MockTransport(handle)), + ) as client: + client._custom_headers.update({"X-Ark-Env": "lane-test", "X-Maas-Env": "multiagent"}) + assert list(client.sessions.events.stream("session-1")) == [] + + assert len(requests) == 1 + _assert_stream_headers(requests[0]) + + +@pytest.mark.asyncio +async def test_async_session_stream_propagates_sdk_default_headers() -> None: + requests = [] + + def handle(request: httpx.Request) -> httpx.Response: + requests.append(request) + return httpx.Response(200, headers={"content-type": "text/event-stream"}, text="") + + async with AsyncArk( + api_key="placeholder", + http_client=httpx.AsyncClient(transport=httpx.MockTransport(handle)), + ) as client: + client._custom_headers.update({"X-Ark-Env": "lane-test", "X-Maas-Env": "multiagent"}) + assert [frame async for frame in client.sessions.events.stream("session-1")] == [] + + assert len(requests) == 1 + _assert_stream_headers(requests[0]) + + +def test_session_stream_decodes_child_custom_tool_use_event() -> None: + payload = { + "type": "agent.custom_tool_use", + "id": "event-1", + "processed_at": "2026-10-11T00:00:00Z", + "session_thread_id": "thread-child", + "custom_tool_use_id": "call-1", + "name": "submit", + "input": {"text": "hello"}, + } + + frame = _decode_stream_frame("", json.dumps(payload)) + event = frame.data + + assert frame.type == "agent.custom_tool_use" + assert isinstance(event, ManagedAgentsAgentCustomToolUseEvent) + assert event.id == "event-1" + assert event.custom_tool_use_id == "call-1" + assert event.session_thread_id == "thread-child" + assert event.name == "submit" + assert event.input == {"text": "hello"} diff --git a/tests/test_session_thread_stream_headers.py b/tests/test_session_thread_stream_headers.py new file mode 100644 index 0000000..76bc257 --- /dev/null +++ b/tests/test_session_thread_stream_headers.py @@ -0,0 +1,100 @@ +from uuid import uuid4 + +import httpx +import pytest + +from arkruntime.resources.sessions.session_threads import ( + AsyncSessionThreads, + SessionThreads, +) + + +class _SyncClient: + def __init__(self, transport) -> None: + self._client = httpx.Client(transport=transport) + self._base_url = "https://api.example.com" + + def default_headers(self) -> dict: + return { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": "ark-runtime-test", + "X-Client-Request-Id": "request-id", + "X-Ark-Env": "lane-test", + "X-Maas-Env": "multiagent", + } + + def post(self, *args, **kwargs): + raise AssertionError("stream tests do not use JSON requests") + + get = post + delete = post + post_without_retry = post + get_api_list = post + + +class _AsyncClient: + def __init__(self, transport) -> None: + self._client = httpx.AsyncClient(transport=transport) + self._base_url = "https://api.example.com" + + def default_headers(self) -> dict: + return { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": "ark-runtime-test", + "X-Client-Request-Id": "request-id", + "X-Ark-Env": "lane-test", + "X-Maas-Env": "multiagent", + } + + def post(self, *args, **kwargs): + raise AssertionError("stream tests do not use JSON requests") + + get = post + delete = post + post_without_retry = post + get_api_list = post + + +def _assert_stream_headers(request: httpx.Request) -> None: + assert request.headers["Accept"] == "text/event-stream" + assert request.headers["User-Agent"] == "ark-runtime-test" + assert request.headers["X-Client-Request-Id"] == "request-id" + assert request.headers["X-Ark-Env"] == "lane-test" + assert request.headers["X-Maas-Env"] == "multiagent" + + +def test_session_thread_stream_propagates_default_headers() -> None: + session_id = str(uuid4()) + thread_id = str(uuid4()) + requests = [] + + def handle(request: httpx.Request) -> httpx.Response: + requests.append(request) + return httpx.Response(200, headers={"content-type": "text/event-stream"}, text="") + + client = _SyncClient(httpx.MockTransport(handle)) + assert list(SessionThreads(client).stream(session_id, thread_id)) == [] + client._client.close() + + assert len(requests) == 1 + _assert_stream_headers(requests[0]) + + +@pytest.mark.asyncio +async def test_async_session_thread_stream_propagates_default_headers() -> None: + session_id = str(uuid4()) + thread_id = str(uuid4()) + requests = [] + + def handle(request: httpx.Request) -> httpx.Response: + requests.append(request) + return httpx.Response(200, headers={"content-type": "text/event-stream"}, text="") + + client = _AsyncClient(httpx.MockTransport(handle)) + assert [frame async for frame in AsyncSessionThreads(client).stream(session_id, thread_id)] == [] + await client._client.aclose() + + assert len(requests) == 1 + _assert_stream_headers(requests[0]) diff --git a/uv.lock b/uv.lock index 572a22c..18f7d24 100644 --- a/uv.lock +++ b/uv.lock @@ -79,7 +79,7 @@ wheels = [ [[package]] name = "arkruntime" -version = "0.17.0" +version = "0.18.0" source = { editable = "." } dependencies = [ { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },