agentkit-sdk-python 0.7.2__py3-none-any.whl → 0.7.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. agentkit/auth/model_login.py +87 -105
  2. agentkit/frameworks/__init__.py +29 -0
  3. agentkit/frameworks/_common.py +136 -0
  4. agentkit/frameworks/langchain.py +170 -0
  5. agentkit/frameworks/langgraph.py +349 -0
  6. agentkit/frameworks/serving/__init__.py +6 -0
  7. agentkit/frameworks/serving/fastapi_mount.py +65 -0
  8. agentkit/frameworks/serving/langserve.py +502 -0
  9. agentkit/toolkit/cli/sandbox/a2a_client.py +407 -0
  10. agentkit/toolkit/cli/sandbox/agentkit_client.py +248 -0
  11. agentkit/toolkit/cli/sandbox/cli.py +5 -0
  12. agentkit/toolkit/cli/sandbox/cli_create.py +219 -165
  13. agentkit/toolkit/cli/sandbox/cli_exec.py +2 -70
  14. agentkit/toolkit/cli/sandbox/cli_file.py +1 -1
  15. agentkit/toolkit/cli/sandbox/cli_get.py +2 -6
  16. agentkit/toolkit/cli/sandbox/cli_invoke.py +375 -0
  17. agentkit/toolkit/cli/sandbox/cli_model_login.py +57 -70
  18. agentkit/toolkit/cli/sandbox/cli_mount.py +1 -1
  19. agentkit/toolkit/cli/sandbox/env_config.py +798 -0
  20. agentkit/toolkit/cli/sandbox/model_config.py +90 -29
  21. agentkit/toolkit/cli/sandbox/session_create.py +94 -168
  22. agentkit/toolkit/cli/sandbox/session_sync.py +1 -1
  23. agentkit/toolkit/cli/sandbox/tool_resolve.py +37 -73
  24. agentkit/version.py +1 -1
  25. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/METADATA +8 -1
  26. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/RECORD +30 -19
  27. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/WHEEL +0 -0
  28. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/entry_points.txt +0 -0
  29. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/licenses/LICENSE +0 -0
  30. {agentkit_sdk_python-0.7.2.dist-info → agentkit_sdk_python-0.7.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,407 @@
1
+ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """A2A JSON-RPC helpers for sandbox CLI commands."""
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ import json
21
+ import sys
22
+ import time
23
+ from typing import Any
24
+ from urllib.parse import urlsplit, urlunsplit
25
+ import uuid
26
+
27
+ import requests
28
+
29
+ DEFAULT_A2A_PATH = "/a2a"
30
+ DEFAULT_A2A_TIMEOUT_SECONDS = 1200
31
+ DEFAULT_A2A_HISTORY_LENGTH = 20
32
+ DEFAULT_A2A_POLL_INTERVAL_SECONDS = 2.0
33
+ DEFAULT_READY_RETRIES = 12
34
+ DEFAULT_READY_RETRY_DELAY = 5.0
35
+ RETRYABLE_A2A_STATUS_CODES = {502, 503, 504}
36
+ TERMINAL_STATES = {
37
+ "completed",
38
+ "failed",
39
+ "canceled",
40
+ "rejected",
41
+ "input-required",
42
+ "auth-required",
43
+ }
44
+
45
+
46
+ class A2AApiError(RuntimeError):
47
+ """Raised when a sandbox A2A request fails."""
48
+
49
+ def __init__(
50
+ self,
51
+ operation: str,
52
+ message: str,
53
+ *,
54
+ status_code: int | None = None,
55
+ response_text: str | None = None,
56
+ response_json: Any = None,
57
+ ) -> None:
58
+ super().__init__(message)
59
+ self.operation = operation
60
+ self.status_code = status_code
61
+ self.response_text = response_text
62
+ self.response_json = response_json
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class A2ATaskStart:
67
+ task: dict[str, Any]
68
+ task_id: str
69
+ context_id: str | None
70
+
71
+
72
+ def send_message_nonblocking(
73
+ *,
74
+ endpoint: object,
75
+ prompt: str,
76
+ a2a_path: str = DEFAULT_A2A_PATH,
77
+ context_id: str | None = None,
78
+ request_metadata: dict[str, str] | None = None,
79
+ history_length: int | None = DEFAULT_A2A_HISTORY_LENGTH,
80
+ timeout: int = 60,
81
+ readiness_retries: int = DEFAULT_READY_RETRIES,
82
+ readiness_retry_delay: float = DEFAULT_READY_RETRY_DELAY,
83
+ ) -> A2ATaskStart:
84
+ message: dict[str, Any] = {
85
+ "kind": "message",
86
+ "messageId": str(uuid.uuid4()),
87
+ "role": "user",
88
+ "parts": [{"kind": "text", "text": prompt}],
89
+ }
90
+ if context_id:
91
+ message["contextId"] = context_id
92
+
93
+ configuration: dict[str, Any] = {"blocking": False}
94
+ if history_length is not None:
95
+ configuration["historyLength"] = history_length
96
+
97
+ params: dict[str, Any] = {
98
+ "message": message,
99
+ "configuration": configuration,
100
+ }
101
+ if request_metadata:
102
+ params["metadata"] = request_metadata
103
+
104
+ response = _post_jsonrpc(
105
+ endpoint=endpoint,
106
+ a2a_path=a2a_path,
107
+ payload={
108
+ "jsonrpc": "2.0",
109
+ "id": str(uuid.uuid4()),
110
+ "method": "message/send",
111
+ "params": params,
112
+ },
113
+ timeout=timeout,
114
+ operation="A2ASendMessage",
115
+ readiness_retries=readiness_retries,
116
+ readiness_retry_delay=readiness_retry_delay,
117
+ )
118
+ task = _jsonrpc_result_task("A2ASendMessage", response)
119
+ task_id = _task_id(task)
120
+ if not task_id:
121
+ raise A2AApiError(
122
+ "A2ASendMessage",
123
+ "response task does not contain id",
124
+ response_json=response,
125
+ )
126
+ return A2ATaskStart(
127
+ task=task,
128
+ task_id=task_id,
129
+ context_id=task_context_id(task),
130
+ )
131
+
132
+
133
+ def get_task(
134
+ *,
135
+ endpoint: object,
136
+ task_id: str,
137
+ a2a_path: str = DEFAULT_A2A_PATH,
138
+ history_length: int | None = DEFAULT_A2A_HISTORY_LENGTH,
139
+ timeout: int = 60,
140
+ readiness_retries: int = DEFAULT_READY_RETRIES,
141
+ readiness_retry_delay: float = DEFAULT_READY_RETRY_DELAY,
142
+ ) -> dict[str, Any]:
143
+ params: dict[str, Any] = {"id": task_id}
144
+ if history_length is not None:
145
+ params["historyLength"] = history_length
146
+
147
+ response = _post_jsonrpc(
148
+ endpoint=endpoint,
149
+ a2a_path=a2a_path,
150
+ payload={
151
+ "jsonrpc": "2.0",
152
+ "id": str(uuid.uuid4()),
153
+ "method": "tasks/get",
154
+ "params": params,
155
+ },
156
+ timeout=timeout,
157
+ operation="A2AGetTask",
158
+ readiness_retries=readiness_retries,
159
+ readiness_retry_delay=readiness_retry_delay,
160
+ )
161
+ return _jsonrpc_result_task("A2AGetTask", response)
162
+
163
+
164
+ def poll_task_until_terminal(
165
+ *,
166
+ endpoint: object,
167
+ task_id: str,
168
+ a2a_path: str = DEFAULT_A2A_PATH,
169
+ history_length: int | None = DEFAULT_A2A_HISTORY_LENGTH,
170
+ timeout: int = DEFAULT_A2A_TIMEOUT_SECONDS,
171
+ interval: float = DEFAULT_A2A_POLL_INTERVAL_SECONDS,
172
+ print_events: bool = False,
173
+ ) -> dict[str, Any]:
174
+ deadline = time.monotonic() + timeout
175
+ latest_task = get_task(
176
+ endpoint=endpoint,
177
+ task_id=task_id,
178
+ a2a_path=a2a_path,
179
+ history_length=history_length,
180
+ timeout=min(60, timeout),
181
+ )
182
+ while task_state(latest_task) not in TERMINAL_STATES:
183
+ if print_events:
184
+ print(json.dumps(latest_task, ensure_ascii=False), file=sys.stderr)
185
+ if time.monotonic() >= deadline:
186
+ raise TimeoutError(f"Timed out while waiting for A2A task {task_id}")
187
+ time.sleep(interval)
188
+ latest_task = get_task(
189
+ endpoint=endpoint,
190
+ task_id=task_id,
191
+ a2a_path=a2a_path,
192
+ history_length=history_length,
193
+ timeout=min(60, timeout),
194
+ )
195
+ if print_events:
196
+ print(json.dumps(latest_task, ensure_ascii=False), file=sys.stderr)
197
+ return latest_task
198
+
199
+
200
+ def task_result_parts(task: dict[str, Any] | None) -> list[dict[str, str]]:
201
+ if not task:
202
+ return []
203
+ for source in (
204
+ _artifact_parts(task),
205
+ _message_parts(_get_nested(task, ("status", "message"))),
206
+ _history_agent_parts(task),
207
+ ):
208
+ parts = _text_parts(source)
209
+ if parts:
210
+ return parts
211
+ return []
212
+
213
+
214
+ def task_result_text(task: dict[str, Any] | None) -> str:
215
+ return "\n".join(part["text"] for part in task_result_parts(task))
216
+
217
+
218
+ def task_state(task: dict[str, Any] | None) -> str | None:
219
+ value = _get_nested(task, ("status", "state"))
220
+ return value if isinstance(value, str) else None
221
+
222
+
223
+ def task_context_id(task: dict[str, Any] | None) -> str | None:
224
+ if not isinstance(task, dict):
225
+ return None
226
+ value = task.get("contextId") or task.get("context_id")
227
+ return value if isinstance(value, str) and value else None
228
+
229
+
230
+ def build_a2a_url(endpoint: object, a2a_path: str = DEFAULT_A2A_PATH) -> str:
231
+ if not isinstance(endpoint, str) or not endpoint.strip():
232
+ raise A2AApiError("A2ARequest", "Sandbox session endpoint is missing")
233
+
234
+ parts = urlsplit(endpoint.strip())
235
+ base_path = parts.path.rstrip("/")
236
+ if not a2a_path or a2a_path == "/":
237
+ resolved_path = f"{base_path}/" if base_path else "/"
238
+ else:
239
+ suffix = "/" + a2a_path.strip("/")
240
+ resolved_path = f"{base_path}{suffix}" if base_path else suffix
241
+ return urlunsplit(
242
+ (parts.scheme, parts.netloc, resolved_path, parts.query, parts.fragment)
243
+ )
244
+
245
+
246
+ def _post_jsonrpc(
247
+ *,
248
+ endpoint: object,
249
+ a2a_path: str,
250
+ payload: dict[str, Any],
251
+ timeout: int,
252
+ operation: str,
253
+ readiness_retries: int,
254
+ readiness_retry_delay: float,
255
+ ) -> dict[str, Any]:
256
+ url = build_a2a_url(endpoint, a2a_path)
257
+ response: requests.Response | None = None
258
+ for attempt in range(readiness_retries + 1):
259
+ try:
260
+ response = requests.post(url, json=payload, timeout=timeout)
261
+ except requests.RequestException as exc:
262
+ if attempt >= readiness_retries:
263
+ raise A2AApiError(operation, str(exc)) from exc
264
+ time.sleep(readiness_retry_delay)
265
+ continue
266
+
267
+ if not _is_retryable_a2a_response(response) or attempt >= readiness_retries:
268
+ break
269
+ time.sleep(readiness_retry_delay)
270
+
271
+ if response is None:
272
+ raise A2AApiError(operation, "request was not sent")
273
+
274
+ body = response.text
275
+ if response.status_code < 200 or response.status_code >= 300:
276
+ raise A2AApiError(
277
+ operation,
278
+ _failure_hint(response.status_code, body),
279
+ status_code=response.status_code,
280
+ response_text=body,
281
+ )
282
+
283
+ try:
284
+ parsed = response.json()
285
+ except ValueError as exc:
286
+ raise A2AApiError(
287
+ operation,
288
+ "response is not valid JSON",
289
+ status_code=response.status_code,
290
+ response_text=body,
291
+ ) from exc
292
+ if not isinstance(parsed, dict):
293
+ raise A2AApiError(
294
+ operation,
295
+ "response JSON is not an object",
296
+ status_code=response.status_code,
297
+ response_json={"response": parsed},
298
+ )
299
+ if parsed.get("error") is not None:
300
+ raise A2AApiError(
301
+ operation,
302
+ "A2A JSON-RPC returned error",
303
+ status_code=response.status_code,
304
+ response_json=parsed,
305
+ )
306
+ return parsed
307
+
308
+
309
+ def _is_retryable_a2a_response(response: requests.Response) -> bool:
310
+ if response.status_code in RETRYABLE_A2A_STATUS_CODES:
311
+ return True
312
+ if response.status_code != 500:
313
+ return False
314
+
315
+ body = response.text.lower()
316
+ return "function_proxy_error" in body and "connection refused" in body
317
+
318
+
319
+ def _jsonrpc_result_task(operation: str, response: dict[str, Any]) -> dict[str, Any]:
320
+ result = response.get("result")
321
+ if not isinstance(result, dict):
322
+ raise A2AApiError(
323
+ operation,
324
+ "response does not contain result task",
325
+ response_json=response,
326
+ )
327
+ if result.get("kind") != "task" and "status" not in result:
328
+ raise A2AApiError(
329
+ operation,
330
+ "response result is not an A2A task",
331
+ response_json=response,
332
+ )
333
+ return result
334
+
335
+
336
+ def _task_id(task: dict[str, Any] | None) -> str | None:
337
+ if not isinstance(task, dict):
338
+ return None
339
+ value = task.get("id")
340
+ return value if isinstance(value, str) and value else None
341
+
342
+
343
+ def _artifact_parts(task: dict[str, Any]) -> list[Any]:
344
+ artifacts = task.get("artifacts")
345
+ if not isinstance(artifacts, list):
346
+ return []
347
+ parts: list[Any] = []
348
+ for artifact in artifacts:
349
+ if not isinstance(artifact, dict):
350
+ continue
351
+ artifact_parts = artifact.get("parts")
352
+ if isinstance(artifact_parts, list):
353
+ parts.extend(artifact_parts)
354
+ return parts
355
+
356
+
357
+ def _message_parts(message: Any) -> list[Any]:
358
+ if not isinstance(message, dict):
359
+ return []
360
+ parts = message.get("parts")
361
+ return parts if isinstance(parts, list) else []
362
+
363
+
364
+ def _history_agent_parts(task: dict[str, Any]) -> list[Any]:
365
+ history = task.get("history")
366
+ if not isinstance(history, list):
367
+ return []
368
+ parts: list[Any] = []
369
+ for message in reversed(history):
370
+ if not isinstance(message, dict) or message.get("role") != "agent":
371
+ continue
372
+ message_parts = message.get("parts")
373
+ if isinstance(message_parts, list):
374
+ text_parts = _text_parts(message_parts)
375
+ if text_parts:
376
+ parts.extend(message_parts)
377
+ break
378
+ return parts
379
+
380
+
381
+ def _text_parts(parts: list[Any]) -> list[dict[str, str]]:
382
+ return [
383
+ {"text": part["text"]}
384
+ for part in parts
385
+ if isinstance(part, dict)
386
+ and part.get("kind") == "text"
387
+ and isinstance(part.get("text"), str)
388
+ and part["text"]
389
+ ]
390
+
391
+
392
+ def _get_nested(value: Any, path: tuple[str, ...]) -> Any:
393
+ current = value
394
+ for key in path:
395
+ if not isinstance(current, dict):
396
+ return None
397
+ current = current.get(key)
398
+ return current
399
+
400
+
401
+ def _failure_hint(status_code: int, body: str) -> str:
402
+ lower_body = body.lower()
403
+ if status_code in (404, 410) or "expired" in lower_body or "deleted" in lower_body:
404
+ return "Sandbox session or A2A task may have expired or been deleted"
405
+ if status_code in (401, 403):
406
+ return "Sandbox access credentials may have expired or become invalid"
407
+ return "A2A request returned non-2xx status"
@@ -0,0 +1,248 @@
1
+ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Sandbox-specific AgentKit tools client helpers."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ from typing import Any, Type, TypeVar
22
+ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
23
+
24
+ import requests
25
+
26
+ from agentkit.platform.constants import SERVICE_METADATA
27
+ from agentkit.sdk.tools.client import AgentkitToolsClient as _OpenapiAgentkitToolsClient
28
+ from agentkit.sdk.tools.types import (
29
+ CreateSessionRequest,
30
+ CreateSessionResponse,
31
+ GetSessionRequest,
32
+ GetSessionResponse,
33
+ ListSessionsRequest,
34
+ ListSessionsResponse,
35
+ )
36
+
37
+ SANDBOX_APIG_ENDPOINT_ENV = "SANDBOX_APIG_ENDPOINT"
38
+ TIP_TOKEN_ENV = "TIP_TOKEN"
39
+ _AGENTKIT_API_VERSION = SERVICE_METADATA["agentkit"].default_version
40
+
41
+ T = TypeVar("T")
42
+
43
+
44
+ def _env_value(name: str) -> str:
45
+ return (os.getenv(name) or "").strip()
46
+
47
+
48
+ def tip_auth_env_enabled() -> bool:
49
+ return bool(_env_value(SANDBOX_APIG_ENDPOINT_ENV) and _env_value(TIP_TOKEN_ENV))
50
+
51
+
52
+ def _with_action_query(endpoint: str, action: str) -> str:
53
+ split = urlsplit(endpoint)
54
+ query = dict(parse_qsl(split.query, keep_blank_values=True))
55
+ query.setdefault("Action", action)
56
+ query.setdefault("Version", _AGENTKIT_API_VERSION)
57
+ return urlunsplit(
58
+ (
59
+ split.scheme,
60
+ split.netloc,
61
+ split.path,
62
+ urlencode(query),
63
+ split.fragment,
64
+ )
65
+ )
66
+
67
+
68
+ def _tip_endpoint_url(endpoint: str, action: str) -> str:
69
+ if "{Action}" in endpoint or "{action}" in endpoint:
70
+ return endpoint.replace("{Action}", action).replace("{action}", action)
71
+ return _with_action_query(endpoint, action)
72
+
73
+
74
+ def _extract_error_message(payload: object, default: str) -> str:
75
+ if not isinstance(payload, dict):
76
+ return default
77
+
78
+ metadata = payload.get("ResponseMetadata")
79
+ if isinstance(metadata, dict):
80
+ api_error = metadata.get("Error")
81
+ if isinstance(api_error, dict):
82
+ message = api_error.get("Message")
83
+ if isinstance(message, str) and message:
84
+ return message
85
+
86
+ for key in ("message", "Message", "error", "Error"):
87
+ value = payload.get(key)
88
+ if isinstance(value, str) and value:
89
+ return value
90
+ if isinstance(value, dict):
91
+ message = value.get("message") or value.get("Message")
92
+ if isinstance(message, str) and message:
93
+ return message
94
+ return default
95
+
96
+
97
+ def _tip_result_payload(payload: object) -> object:
98
+ if not isinstance(payload, dict):
99
+ return payload
100
+ if "Result" in payload:
101
+ return payload.get("Result") or {}
102
+ data = payload.get("data")
103
+ if isinstance(data, dict):
104
+ return data
105
+ return payload
106
+
107
+
108
+ class TipAgentkitToolsClient(_OpenapiAgentkitToolsClient):
109
+ """AgentKit tools client with optional TIP bearer-token session routing.
110
+
111
+ When both SANDBOX_APIG_ENDPOINT and TIP_TOKEN are present, session APIs are
112
+ sent directly to the APIG endpoint. Otherwise this behaves exactly like the
113
+ generated AgentkitToolsClient.
114
+ """
115
+
116
+ def __init__(
117
+ self,
118
+ access_key: str = "",
119
+ secret_key: str = "",
120
+ region: str = "",
121
+ session_token: str = "",
122
+ timeout: int = 30,
123
+ ) -> None:
124
+ self._tip_endpoint = _env_value(SANDBOX_APIG_ENDPOINT_ENV).rstrip("/")
125
+ self._tip_token = _env_value(TIP_TOKEN_ENV)
126
+ self._tip_timeout = timeout
127
+ self._tip_session: requests.Session | None = None
128
+ if self.uses_tip_auth:
129
+ self._tip_session = requests.Session()
130
+ return
131
+
132
+ super().__init__(
133
+ access_key=access_key,
134
+ secret_key=secret_key,
135
+ region=region,
136
+ session_token=session_token,
137
+ )
138
+
139
+ @property
140
+ def uses_tip_auth(self) -> bool:
141
+ return bool(self._tip_endpoint and self._tip_token)
142
+
143
+ def _invoke_tip_api(
144
+ self,
145
+ api_action: str,
146
+ request: Any,
147
+ response_type: Type[T],
148
+ ) -> T:
149
+ if not self.uses_tip_auth or self._tip_session is None:
150
+ return self._invoke_api(
151
+ api_action=api_action,
152
+ request=request,
153
+ response_type=response_type,
154
+ )
155
+
156
+ url = _tip_endpoint_url(self._tip_endpoint, api_action)
157
+ body = request.model_dump(by_alias=True, exclude_none=True)
158
+ try:
159
+ response = self._tip_session.post(
160
+ url,
161
+ json=body,
162
+ headers={
163
+ "Accept": "application/json",
164
+ "Content-Type": "application/json",
165
+ "Authorization": f"Bearer {self._tip_token}",
166
+ },
167
+ timeout=self._tip_timeout,
168
+ )
169
+ except requests.RequestException as exc:
170
+ raise Exception(f"Failed to {api_action}: {exc}") from exc
171
+
172
+ try:
173
+ payload = response.json()
174
+ except ValueError as exc:
175
+ raise Exception(
176
+ f"Failed to {api_action}: invalid JSON response: {response.text}"
177
+ ) from exc
178
+
179
+ if response.status_code >= 400:
180
+ raise Exception(
181
+ f"Failed to {api_action}: "
182
+ f"{_extract_error_message(payload, response.text)}"
183
+ )
184
+
185
+ if isinstance(payload, dict):
186
+ metadata = payload.get("ResponseMetadata")
187
+ if isinstance(metadata, dict) and metadata.get("Error"):
188
+ raise Exception(
189
+ f"Failed to {api_action}: "
190
+ f"{_extract_error_message(payload, json.dumps(payload))}"
191
+ )
192
+
193
+ result = _tip_result_payload(payload)
194
+ if not isinstance(result, dict):
195
+ raise Exception(f"Failed to {api_action}: invalid response payload")
196
+ return response_type(**result)
197
+
198
+ def create_session(self, request: CreateSessionRequest) -> CreateSessionResponse:
199
+ return self._invoke_tip_api(
200
+ api_action="CreateSession",
201
+ request=request,
202
+ response_type=CreateSessionResponse,
203
+ )
204
+
205
+ def get_session(self, request: GetSessionRequest) -> GetSessionResponse:
206
+ return self._invoke_tip_api(
207
+ api_action="GetSession",
208
+ request=request,
209
+ response_type=GetSessionResponse,
210
+ )
211
+
212
+ def list_sessions(self, request: ListSessionsRequest) -> ListSessionsResponse:
213
+ return self._invoke_tip_api(
214
+ api_action="ListSessions",
215
+ request=request,
216
+ response_type=ListSessionsResponse,
217
+ )
218
+
219
+ def _raise_tip_unsupported(self, api_action: str) -> None:
220
+ raise Exception(
221
+ f"{api_action} is not available with TIP sandbox auth. "
222
+ f"Unset {SANDBOX_APIG_ENDPOINT_ENV}/{TIP_TOKEN_ENV} to use the "
223
+ "standard AgentKit OpenAPI client."
224
+ )
225
+
226
+ def create_tool(self, request: Any) -> Any:
227
+ if self.uses_tip_auth:
228
+ self._raise_tip_unsupported("CreateTool")
229
+ return super().create_tool(request)
230
+
231
+ def get_tool(self, request: Any) -> Any:
232
+ if self.uses_tip_auth:
233
+ self._raise_tip_unsupported("GetTool")
234
+ return super().get_tool(request)
235
+
236
+ def list_tools(self, request: Any) -> Any:
237
+ if self.uses_tip_auth:
238
+ self._raise_tip_unsupported("ListTools")
239
+ return super().list_tools(request)
240
+
241
+
242
+ def is_tip_agentkit_client(client: object) -> bool:
243
+ return bool(getattr(client, "uses_tip_auth", False))
244
+
245
+
246
+ # Keep sandbox modules/tests able to patch a local AgentkitToolsClient symbol,
247
+ # while routing construction through the sandbox-aware implementation.
248
+ AgentkitToolsClient = TipAgentkitToolsClient
@@ -22,6 +22,7 @@ from agentkit.toolkit.cli.sandbox.cli_create import create_command
22
22
  from agentkit.toolkit.cli.sandbox.cli_exec import exec_command
23
23
  from agentkit.toolkit.cli.sandbox.cli_file import file_command
24
24
  from agentkit.toolkit.cli.sandbox.cli_get import get_command
25
+ from agentkit.toolkit.cli.sandbox.cli_invoke import invoke_command
25
26
  from agentkit.toolkit.cli.sandbox.cli_model_login import codex_login_command
26
27
  from agentkit.toolkit.cli.sandbox.cli_mount import mount_command
27
28
  from agentkit.toolkit.cli.sandbox.cli_run import run_command
@@ -44,6 +45,10 @@ sandbox_app.command(
44
45
  name="exec",
45
46
  context_settings={"allow_extra_args": True},
46
47
  )(exec_command)
48
+ sandbox_app.command(
49
+ name="invoke",
50
+ context_settings={"allow_extra_args": True},
51
+ )(invoke_command)
47
52
  sandbox_app.command(name="run")(run_command)
48
53
  sandbox_app.command(
49
54
  name="shell",