toolplane-python-client 0.1.0__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 (81) hide show
  1. toolplane/__init__.py +106 -0
  2. toolplane/common/__init__.py +93 -0
  3. toolplane/common/base_config.py +129 -0
  4. toolplane/common/base_connection_manager.py +171 -0
  5. toolplane/common/base_session_manager.py +321 -0
  6. toolplane/common/base_tool_manager.py +347 -0
  7. toolplane/common/constants.py +47 -0
  8. toolplane/common/utils.py +310 -0
  9. toolplane/core/__init__.py +67 -0
  10. toolplane/core/config.py +107 -0
  11. toolplane/core/connection.py +285 -0
  12. toolplane/core/errors.py +298 -0
  13. toolplane/core/machine.py +480 -0
  14. toolplane/core/request.py +775 -0
  15. toolplane/core/session.py +332 -0
  16. toolplane/core/session_context.py +514 -0
  17. toolplane/core/task.py +130 -0
  18. toolplane/core/tool.py +329 -0
  19. toolplane/http_core/__init__.py +37 -0
  20. toolplane/http_core/http_config.py +97 -0
  21. toolplane/http_core/http_connection.py +409 -0
  22. toolplane/http_core/http_machine.py +298 -0
  23. toolplane/http_core/http_request.py +748 -0
  24. toolplane/http_core/http_session.py +348 -0
  25. toolplane/http_core/http_session_context.py +491 -0
  26. toolplane/http_core/http_task.py +101 -0
  27. toolplane/http_core/http_tool.py +400 -0
  28. toolplane/interfaces/__init__.py +27 -0
  29. toolplane/interfaces/client_interface.py +122 -0
  30. toolplane/interfaces/connection_interface.py +193 -0
  31. toolplane/interfaces/event_interface.py +290 -0
  32. toolplane/interfaces/request_interface.py +439 -0
  33. toolplane/interfaces/session_interface.py +288 -0
  34. toolplane/interfaces/tool_interface.py +441 -0
  35. toolplane/proto/__init__.py +0 -0
  36. toolplane/proto/service_pb2.py +315 -0
  37. toolplane/proto/service_pb2_grpc.py +2240 -0
  38. toolplane/provider_cli.py +268 -0
  39. toolplane/provider_registry.py +77 -0
  40. toolplane/provider_runtime.py +302 -0
  41. toolplane/toolkits/__init__.py +0 -0
  42. toolplane/toolkits/standalone_tools/__init__.py +0 -0
  43. toolplane/toolkits/standalone_tools/create_directory.py +94 -0
  44. toolplane/toolkits/standalone_tools/create_file.py +124 -0
  45. toolplane/toolkits/standalone_tools/file_search.py +229 -0
  46. toolplane/toolkits/standalone_tools/grep_search.py +372 -0
  47. toolplane/toolkits/standalone_tools/launcher.py +146 -0
  48. toolplane/toolkits/standalone_tools/list_dir.py +395 -0
  49. toolplane/toolkits/standalone_tools/read_file.py +346 -0
  50. toolplane/toolkits/standalone_tools/replace_string_in_file.py +407 -0
  51. toolplane/toolkits/standalone_tools/run_tests.py +66 -0
  52. toolplane/toolkits/standalone_tools/semantic_search.py +485 -0
  53. toolplane/toolkits/standalone_tools/standalone_toolkit.py +979 -0
  54. toolplane/toolkits/standalone_tools/test_failure_analysis.py +618 -0
  55. toolplane/toolkits/standalone_tools/test_standalone_toolkit.py +517 -0
  56. toolplane/toolkits/swe/__init__.py +35 -0
  57. toolplane/toolkits/swe/create_directory.py +15 -0
  58. toolplane/toolkits/swe/create_file.py +15 -0
  59. toolplane/toolkits/swe/descriptions.py +273 -0
  60. toolplane/toolkits/swe/execute_bash.py +93 -0
  61. toolplane/toolkits/swe/file_editor.py +775 -0
  62. toolplane/toolkits/swe/file_search.py +16 -0
  63. toolplane/toolkits/swe/finish.py +50 -0
  64. toolplane/toolkits/swe/grep_search.py +19 -0
  65. toolplane/toolkits/swe/list_dir.py +407 -0
  66. toolplane/toolkits/swe/read_file.py +18 -0
  67. toolplane/toolkits/swe/replace_string_in_file.py +17 -0
  68. toolplane/toolkits/swe/search.py +260 -0
  69. toolplane/toolkits/swe/semantic_search.py +20 -0
  70. toolplane/toolkits/swe/str_replace_editor.py +647 -0
  71. toolplane/toolkits/swe/submit.py +29 -0
  72. toolplane/toolkits/swe/swe_toolkit.py +1296 -0
  73. toolplane/toolplane_client.py +686 -0
  74. toolplane/toolplane_http_client.py +681 -0
  75. toolplane/utils/__init__.py +3 -0
  76. toolplane/utils/schema.py +146 -0
  77. toolplane_python_client-0.1.0.dist-info/METADATA +543 -0
  78. toolplane_python_client-0.1.0.dist-info/RECORD +81 -0
  79. toolplane_python_client-0.1.0.dist-info/WHEEL +5 -0
  80. toolplane_python_client-0.1.0.dist-info/entry_points.txt +2 -0
  81. toolplane_python_client-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,686 @@
1
+ """Modular Toolplane client implementation."""
2
+
3
+ import logging
4
+ import threading
5
+ import time
6
+ from typing import Any, Callable, Dict, List, Optional
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ try:
11
+ from .common import (
12
+ DEFAULT_HEARTBEAT_INTERVAL,
13
+ DEFAULT_MAX_WORKERS,
14
+ DEFAULT_POLL_INTERVAL,
15
+ DEFAULT_REQUEST_TIMEOUT,
16
+ generate_session_id,
17
+ validate_session_name,
18
+ )
19
+ from .core import (
20
+ ClientConfig,
21
+ ConnectionError,
22
+ ConnectionManager,
23
+ MachineManager,
24
+ RequestManager,
25
+ SessionContext,
26
+ SessionManager,
27
+ TaskManager,
28
+ ToolManager,
29
+ ToolplaneError,
30
+ )
31
+ except ImportError:
32
+ # Fallback for direct execution
33
+ from common import (
34
+ DEFAULT_HEARTBEAT_INTERVAL,
35
+ DEFAULT_MAX_WORKERS,
36
+ DEFAULT_POLL_INTERVAL,
37
+ DEFAULT_REQUEST_TIMEOUT,
38
+ generate_session_id,
39
+ validate_session_name,
40
+ )
41
+ from core import (
42
+ ClientConfig,
43
+ ConnectionError,
44
+ ConnectionManager,
45
+ MachineManager,
46
+ RequestManager,
47
+ SessionContext,
48
+ SessionManager,
49
+ TaskManager,
50
+ ToolManager,
51
+ ToolplaneError,
52
+ )
53
+
54
+
55
+ class Toolplane:
56
+ """
57
+ Modular Toolplane client for registering and executing tools.
58
+ Supports multi-session mode with explicit session management.
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ server_host: str = "localhost",
64
+ server_port: int = 9001,
65
+ use_tls: bool = False,
66
+ tls_cert_path: Optional[str] = None,
67
+ tls_key_path: Optional[str] = None,
68
+ tls_ca_cert_path: Optional[str] = None,
69
+ tls_server_name: Optional[str] = None,
70
+ session_ids: Optional[List[str]] = None,
71
+ api_key: Optional[str] = None,
72
+ user_id: Optional[str] = None,
73
+ session_name: Optional[str] = None,
74
+ session_description: Optional[str] = None,
75
+ session_namespace: Optional[str] = None,
76
+ heartbeat_interval: int = DEFAULT_HEARTBEAT_INTERVAL,
77
+ max_workers: int = DEFAULT_MAX_WORKERS,
78
+ request_timeout: int = DEFAULT_REQUEST_TIMEOUT,
79
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
80
+ # Retry configuration
81
+ max_retries: int = 3,
82
+ retry_base_delay: float = 1.0,
83
+ retry_max_delay: float = 60.0,
84
+ retry_backoff_factor: float = 2.0,
85
+ event_emitter: Optional[Any] = None,
86
+ ):
87
+ """Initialize Toolplane client."""
88
+ # Create configuration
89
+ self.config = ClientConfig(
90
+ server_host=server_host,
91
+ server_port=server_port,
92
+ use_tls=use_tls,
93
+ tls_cert_path=tls_cert_path,
94
+ tls_key_path=tls_key_path,
95
+ tls_ca_cert_path=tls_ca_cert_path,
96
+ tls_server_name=tls_server_name,
97
+ api_key=api_key,
98
+ user_id=user_id,
99
+ session_name=session_name,
100
+ session_description=session_description,
101
+ session_namespace=session_namespace,
102
+ heartbeat_interval=heartbeat_interval,
103
+ max_workers=max_workers,
104
+ request_timeout=request_timeout,
105
+ poll_interval=poll_interval,
106
+ # Pass retry configuration to config so ConnectionManager can access it
107
+ max_retries=max_retries,
108
+ retry_base_delay=retry_base_delay,
109
+ retry_max_delay=retry_max_delay,
110
+ retry_backoff_factor=retry_backoff_factor,
111
+ )
112
+
113
+ # Initialize managers
114
+ self.connection_manager = ConnectionManager(self.config)
115
+ self.machine_manager = MachineManager(self.connection_manager, event_emitter)
116
+ self.tool_manager = ToolManager(self.connection_manager)
117
+ self.request_manager = RequestManager(self.connection_manager, max_workers)
118
+ self.task_manager = TaskManager(self.connection_manager)
119
+ self.session_manager = SessionManager(self.connection_manager)
120
+
121
+ # Wire managers that coordinate during recovery flows
122
+ self.machine_manager.attach_tool_manager(self.tool_manager)
123
+ self.machine_manager.attach_session_manager(self.session_manager)
124
+
125
+ # Client state
126
+ self.running = False
127
+ self.session_ids = session_ids or []
128
+ self._main_thread: Optional[threading.Thread] = None
129
+ self._provider_runtime = None
130
+
131
+ def connect(self) -> bool:
132
+ """Connect to Toolplane server."""
133
+ try:
134
+ success = self.connection_manager.connect()
135
+ if success:
136
+ self._initialize_sessions(register_machine=False)
137
+ return success
138
+ except Exception as e:
139
+ raise ConnectionError(f"Failed to connect: {e}")
140
+
141
+ def disconnect(self) -> None:
142
+ """Disconnect from Toolplane server."""
143
+ self.stop()
144
+ try:
145
+ self.request_manager.stop_polling()
146
+ except Exception:
147
+ pass
148
+ try:
149
+ self.request_manager.stop_lease_renewal()
150
+ except Exception:
151
+ pass
152
+ try:
153
+ self.machine_manager.stop_heartbeat()
154
+ except Exception:
155
+ pass
156
+ self.machine_manager.cleanup_all()
157
+ self.tool_manager.cleanup_all()
158
+ self.session_manager.cleanup_all()
159
+ self.connection_manager.disconnect()
160
+
161
+ def _initialize_sessions(self, register_machine: bool = False) -> None:
162
+ """Initialize specified sessions."""
163
+ for session_id in list(self.session_ids):
164
+ try:
165
+ self.ensure_session_context(
166
+ session_id,
167
+ create_if_missing=True,
168
+ register_machine=register_machine,
169
+ )
170
+ except Exception as e:
171
+ logger.warning("Failed to initialize session %s: %s", session_id, e)
172
+
173
+ def ensure_session_context(
174
+ self,
175
+ session_id: str,
176
+ create_if_missing: bool = False,
177
+ register_machine: bool = False,
178
+ ) -> SessionContext:
179
+ """Ensure a local session context exists without implying provider startup."""
180
+ if not session_id or not isinstance(session_id, str):
181
+ raise ToolplaneError(f"Invalid session ID: {session_id}")
182
+
183
+ existing_context = self.session_manager.get_session_context(session_id)
184
+ if existing_context:
185
+ if register_machine and not getattr(existing_context, "machine_id", None):
186
+ if not existing_context.register_machine():
187
+ raise ToolplaneError(
188
+ f"Failed to register machine for session {session_id}"
189
+ )
190
+ if session_id not in self.session_ids:
191
+ self.session_ids.append(session_id)
192
+ return existing_context
193
+
194
+ resolved_session_id = session_id
195
+ if not self.session_manager.get_session(session_id):
196
+ if not create_if_missing:
197
+ raise ToolplaneError(f"Session {session_id} does not exist")
198
+ resolved_session_id = self.session_manager.create_session(
199
+ session_id=session_id,
200
+ user_id=self.config.user_id,
201
+ name=self.config.session_name,
202
+ description=self.config.session_description,
203
+ namespace=self.config.session_namespace,
204
+ )
205
+
206
+ context = SessionContext(
207
+ resolved_session_id,
208
+ self.connection_manager,
209
+ self.machine_manager,
210
+ self.tool_manager,
211
+ self.request_manager,
212
+ self.session_manager,
213
+ )
214
+ self.session_manager.register_session_context(resolved_session_id, context)
215
+ if resolved_session_id not in self.session_ids:
216
+ self.session_ids.append(resolved_session_id)
217
+
218
+ if register_machine and not context.register_machine():
219
+ raise ToolplaneError(
220
+ f"Failed to register machine for session {resolved_session_id}"
221
+ )
222
+
223
+ return context
224
+
225
+ def create_session(
226
+ self,
227
+ session_id: Optional[str] = None,
228
+ user_id: Optional[str] = None,
229
+ name: Optional[str] = None,
230
+ description: Optional[str] = None,
231
+ namespace: Optional[str] = None,
232
+ register_machine: bool = False,
233
+ ) -> SessionContext:
234
+ """Create a new session."""
235
+ if not self.connection_manager.connected:
236
+ if not self.connect():
237
+ raise ConnectionError("Failed to connect to server")
238
+
239
+ # Validate session name if provided
240
+ if name and not validate_session_name(name):
241
+ raise ToolplaneError(f"Invalid session name: {name}")
242
+
243
+ # Generate session ID if not provided
244
+ if not session_id:
245
+ session_id = generate_session_id()
246
+
247
+ try:
248
+ # Create session on server
249
+ created_session_id = self.session_manager.create_session(
250
+ session_id=session_id,
251
+ user_id=user_id or self.config.user_id,
252
+ name=name or self.config.session_name,
253
+ description=description or self.config.session_description,
254
+ namespace=namespace or self.config.session_namespace,
255
+ )
256
+
257
+ logger.info("Created new session: %s", created_session_id)
258
+
259
+ context = self.ensure_session_context(
260
+ created_session_id,
261
+ create_if_missing=False,
262
+ register_machine=register_machine,
263
+ )
264
+ return context
265
+
266
+ except Exception as e:
267
+ raise ToolplaneError(f"Failed to create session: {e}")
268
+
269
+ def get_session(self, session_id: str) -> Optional[SessionContext]:
270
+ """Get session context by ID."""
271
+ return self.session_manager.get_session_context(session_id)
272
+
273
+ def list_sessions(self) -> List[SessionContext]:
274
+ """List all session contexts."""
275
+ return self.session_manager.list_session_contexts()
276
+
277
+ # Session admin helpers (admin scope — Python-only; not portable across SDKs)
278
+ def list_user_sessions(
279
+ self, user_id: str, page_size: int = 10, page_token: str = "", filter: str = ""
280
+ ) -> Dict[str, Any]:
281
+ """List user sessions with pagination and filtering.
282
+
283
+ page_token is the opaque cursor returned by the previous page.
284
+ """
285
+ if not self.connection_manager.connected:
286
+ if not self.connect():
287
+ raise ConnectionError("Failed to connect to server")
288
+ return self.session_manager.list_user_sessions(
289
+ user_id, page_size, page_token, filter
290
+ )
291
+
292
+ def bulk_delete_sessions(
293
+ self, user_id: str, session_ids: Optional[List[str]] = None, filter: str = ""
294
+ ) -> Dict[str, Any]:
295
+ """Bulk delete sessions for a user."""
296
+ if not self.connection_manager.connected:
297
+ if not self.connect():
298
+ raise ConnectionError("Failed to connect to server")
299
+ return self.session_manager.bulk_delete_sessions(
300
+ user_id, session_ids or [], filter
301
+ )
302
+
303
+ def get_session_stats(self, user_id: str) -> Dict[str, int]:
304
+ """Get session statistics for a user."""
305
+ if not self.connection_manager.connected:
306
+ if not self.connect():
307
+ raise ConnectionError("Failed to connect to server")
308
+ return self.session_manager.get_session_stats(user_id)
309
+
310
+ def invalidate_session(self, session_id: str, reason: str = "") -> bool:
311
+ """Invalidate a session."""
312
+ if not self.connection_manager.connected:
313
+ if not self.connect():
314
+ raise ConnectionError("Failed to connect to server")
315
+ return self.session_manager.invalidate_session(session_id, reason)
316
+
317
+ def update_session(
318
+ self,
319
+ session_id: str,
320
+ name: Optional[str] = None,
321
+ description: Optional[str] = None,
322
+ namespace: Optional[str] = None,
323
+ ) -> Dict[str, Any]:
324
+ """Update session metadata."""
325
+ if not self.connection_manager.connected:
326
+ if not self.connect():
327
+ raise ConnectionError("Failed to connect to server")
328
+ return self.session_manager.update_session(
329
+ session_id=session_id,
330
+ name=name,
331
+ description=description,
332
+ namespace=namespace,
333
+ )
334
+
335
+ def create_request(
336
+ self,
337
+ session_id: str,
338
+ tool_name: str,
339
+ input_data: str,
340
+ idempotency_key: str = "",
341
+ ) -> str:
342
+ """Create a new request in a session.
343
+
344
+ idempotency_key, when set, dedups creates within the session:
345
+ retrying with the same key returns the original request.
346
+ """
347
+ if not self.connection_manager.connected:
348
+ if not self.connect():
349
+ raise ConnectionError("Failed to connect to server")
350
+ return self.request_manager.create_request(
351
+ session_id, tool_name, input_data, idempotency_key=idempotency_key
352
+ )
353
+
354
+ def list_requests(
355
+ self,
356
+ session_id: str,
357
+ status: str = "",
358
+ tool_name: str = "",
359
+ limit: int = 10,
360
+ page_token: str = "",
361
+ ) -> List[Dict[str, Any]]:
362
+ """List requests in a session.
363
+
364
+ page_token is the opaque cursor returned by the previous page.
365
+ """
366
+ if not self.connection_manager.connected:
367
+ if not self.connect():
368
+ raise ConnectionError("Failed to connect to server")
369
+ return self.request_manager.list_requests(
370
+ session_id=session_id,
371
+ status=status,
372
+ tool_name=tool_name,
373
+ limit=limit,
374
+ page_token=page_token,
375
+ )
376
+
377
+ def list_requests_page(
378
+ self,
379
+ session_id: str,
380
+ status: str = "",
381
+ tool_name: str = "",
382
+ limit: int = 10,
383
+ page_token: str = "",
384
+ ) -> Dict[str, Any]:
385
+ """List one page of requests, with the continuation cursor.
386
+
387
+ Returns {"requests", "next_page_token", "total_size"};
388
+ next_page_token is empty on the last page.
389
+ """
390
+ if not self.connection_manager.connected:
391
+ if not self.connect():
392
+ raise ConnectionError("Failed to connect to server")
393
+ return self.request_manager.list_requests_page(
394
+ session_id=session_id,
395
+ status=status,
396
+ tool_name=tool_name,
397
+ limit=limit,
398
+ page_token=page_token,
399
+ )
400
+
401
+ def cancel_request(self, session_id: str, request_id: str) -> bool:
402
+ """Cancel a request in a session."""
403
+ if not self.connection_manager.connected:
404
+ if not self.connect():
405
+ raise ConnectionError("Failed to connect to server")
406
+ return self.request_manager.cancel_request(session_id, request_id)
407
+
408
+ def create_task(
409
+ self,
410
+ session_id: str,
411
+ tool_name: str,
412
+ input_data: str,
413
+ ) -> Dict[str, Any]:
414
+ """Create a new task in a session."""
415
+ if not self.connection_manager.connected:
416
+ if not self.connect():
417
+ raise ConnectionError("Failed to connect to server")
418
+ return self.task_manager.create_task(session_id, tool_name, input_data)
419
+
420
+ def get_task(self, session_id: str, task_id: str) -> Dict[str, Any]:
421
+ """Get a task by ID in a session."""
422
+ if not self.connection_manager.connected:
423
+ if not self.connect():
424
+ raise ConnectionError("Failed to connect to server")
425
+ return self.task_manager.get_task(session_id, task_id)
426
+
427
+ def list_tasks(self, session_id: str) -> List[Dict[str, Any]]:
428
+ """List tasks in a session."""
429
+ if not self.connection_manager.connected:
430
+ if not self.connect():
431
+ raise ConnectionError("Failed to connect to server")
432
+ return self.task_manager.list_tasks(session_id)
433
+
434
+ def cancel_task(self, session_id: str, task_id: str) -> bool:
435
+ """Cancel a task in a session."""
436
+ if not self.connection_manager.connected:
437
+ if not self.connect():
438
+ raise ConnectionError("Failed to connect to server")
439
+ return self.task_manager.cancel_task(session_id, task_id)
440
+
441
+ def list_machines(self, session_id: str) -> List[Dict[str, Any]]:
442
+ """List machines in a session."""
443
+ if not self.connection_manager.connected:
444
+ if not self.connect():
445
+ raise ConnectionError("Failed to connect to server")
446
+ return self.machine_manager.list_machines(session_id)
447
+
448
+ def get_machine(self, session_id: str, machine_id: str) -> Dict[str, Any]:
449
+ """Get a machine by ID."""
450
+ if not self.connection_manager.connected:
451
+ if not self.connect():
452
+ raise ConnectionError("Failed to connect to server")
453
+ return self.machine_manager.get_machine(session_id, machine_id)
454
+
455
+ def unregister_machine(
456
+ self, session_id: str, machine_id: Optional[str] = None
457
+ ) -> bool:
458
+ """Unregister a machine from a session."""
459
+ if not self.connection_manager.connected:
460
+ if not self.connect():
461
+ raise ConnectionError("Failed to connect to server")
462
+ return self.machine_manager.unregister_machine(session_id, machine_id)
463
+
464
+ def drain_machine(self, session_id: str, machine_id: Optional[str] = None) -> bool:
465
+ """Drain a machine from a session."""
466
+ if not self.connection_manager.connected:
467
+ if not self.connect():
468
+ raise ConnectionError("Failed to connect to server")
469
+ return self.machine_manager.drain_machine(session_id, machine_id)
470
+
471
+ def create_api_key(
472
+ self,
473
+ session_id: str,
474
+ name: str,
475
+ capabilities: List[str],
476
+ ) -> Dict[str, Any]:
477
+ """Create a new API key for a session.
478
+
479
+ capabilities is required and must be non-empty (least-privilege,
480
+ explicit key minting).
481
+ """
482
+ if not self.connection_manager.connected:
483
+ if not self.connect():
484
+ raise ConnectionError("Failed to connect to server")
485
+ return self.session_manager.create_api_key(session_id, name, capabilities)
486
+
487
+ def list_api_keys(self, session_id: str) -> List[Dict[str, Any]]:
488
+ """List active API keys for a session."""
489
+ if not self.connection_manager.connected:
490
+ if not self.connect():
491
+ raise ConnectionError("Failed to connect to server")
492
+ return self.session_manager.list_api_keys(session_id)
493
+
494
+ def revoke_api_key(self, session_id: str, key_id: str) -> bool:
495
+ """Revoke an API key for a session."""
496
+ if not self.connection_manager.connected:
497
+ if not self.connect():
498
+ raise ConnectionError("Failed to connect to server")
499
+ return self.session_manager.revoke_api_key(session_id, key_id)
500
+
501
+ def tool(
502
+ self,
503
+ session_id: str,
504
+ name: Optional[str] = None,
505
+ description: Optional[str] = None,
506
+ stream: bool = False,
507
+ tags: Optional[List[str]] = None,
508
+ ) -> Callable[[Callable], Callable]:
509
+ """Decorator to register a tool for a session."""
510
+
511
+ def decorator(func):
512
+ context = self.get_session(session_id)
513
+ if not context:
514
+ raise ToolplaneError(f"Session {session_id} not found")
515
+
516
+ context.register_tool(
517
+ name or func.__name__,
518
+ func,
519
+ description=description,
520
+ stream=stream,
521
+ tags=tags or [],
522
+ )
523
+ return func
524
+
525
+ return decorator
526
+
527
+ def invoke(
528
+ self,
529
+ tool_name: str,
530
+ session_id: str,
531
+ timeout_seconds: int = 0,
532
+ wait_timeout: Optional[int] = None,
533
+ **params,
534
+ ) -> Any:
535
+ """Invoke a tool in a session and return the tool result value.
536
+
537
+ timeout_seconds sets the request's absolute per-attempt execution
538
+ timeout on the wire (0 keeps the server default); wait_timeout bounds
539
+ the local wait (default: timeout_seconds + 15, else 60).
540
+ """
541
+ context = self.get_session(session_id)
542
+ if not context:
543
+ raise ToolplaneError(f"Session {session_id} not found")
544
+
545
+ return context.invoke(
546
+ tool_name,
547
+ timeout_seconds=timeout_seconds,
548
+ wait_timeout=wait_timeout,
549
+ **params,
550
+ )
551
+
552
+ async def ainvoke(
553
+ self, tool_name: str, session_id: str, timeout_seconds: int = 0, **params
554
+ ) -> str:
555
+ """Submit a tool invocation without blocking; awaits the request ID."""
556
+ context = self.get_session(session_id)
557
+ if not context:
558
+ raise ToolplaneError(f"Session {session_id} not found")
559
+
560
+ return await context.ainvoke(
561
+ tool_name, timeout_seconds=timeout_seconds, **params
562
+ )
563
+
564
+ def stream(
565
+ self,
566
+ tool_name: str,
567
+ callback: Callable[[Any, bool], None],
568
+ session_id: str,
569
+ timeout_seconds: int = 0,
570
+ **params,
571
+ ) -> List[Any]:
572
+ """Stream tool execution."""
573
+ context = self.get_session(session_id)
574
+ if not context:
575
+ raise ToolplaneError(f"Session {session_id} not found")
576
+
577
+ return context.stream(
578
+ tool_name, callback, timeout_seconds=timeout_seconds, **params
579
+ )
580
+
581
+ async def astream(
582
+ self,
583
+ tool_name: str,
584
+ callback: Callable[[Any, bool], None],
585
+ session_id: str,
586
+ timeout_seconds: int = 0,
587
+ **params,
588
+ ) -> List[Any]:
589
+ """Awaitable stream: resolves with the collected chunks."""
590
+ context = self.get_session(session_id)
591
+ if not context:
592
+ raise ToolplaneError(f"Session {session_id} not found")
593
+
594
+ return await context.astream(
595
+ tool_name, callback, timeout_seconds=timeout_seconds, **params
596
+ )
597
+
598
+ def get_available_tools(self, session_id: str) -> Dict[str, Any]:
599
+ """Get available tools for a session."""
600
+ context = self.get_session(session_id)
601
+ if not context:
602
+ raise ToolplaneError(f"Session {session_id} not found")
603
+
604
+ return context.get_available_tools()
605
+
606
+ def list_tools(self, session_id: str) -> List[Dict[str, Any]]:
607
+ """List tools for a session."""
608
+ if not self.connection_manager.connected:
609
+ if not self.connect():
610
+ raise ConnectionError("Failed to connect to server")
611
+ return self.tool_manager.list_tools(session_id)
612
+
613
+ def get_tool_by_id(self, session_id: str, tool_id: str) -> Dict[str, Any]:
614
+ """Get a tool by ID."""
615
+ if not self.connection_manager.connected:
616
+ if not self.connect():
617
+ raise ConnectionError("Failed to connect to server")
618
+ return self.tool_manager.get_tool_by_id(session_id, tool_id)
619
+
620
+ def get_tool_by_name(self, session_id: str, tool_name: str) -> Dict[str, Any]:
621
+ """Get a tool by name."""
622
+ if not self.connection_manager.connected:
623
+ if not self.connect():
624
+ raise ConnectionError("Failed to connect to server")
625
+ return self.tool_manager.get_tool_by_name(session_id, tool_name)
626
+
627
+ def delete_tool(self, session_id: str, tool_id: str) -> bool:
628
+ """Delete a tool by ID."""
629
+ if not self.connection_manager.connected:
630
+ if not self.connect():
631
+ raise ConnectionError("Failed to connect to server")
632
+ return self.tool_manager.delete_tool(session_id, tool_id)
633
+
634
+ def get_request_status(self, session_id: str, request_id: str) -> Dict[str, Any]:
635
+ """Get request status (session-scoped, like every other facade method)."""
636
+ context = self.get_session(session_id)
637
+ if not context:
638
+ raise ToolplaneError(f"Session {session_id} not found")
639
+
640
+ return context.get_request_status(request_id)
641
+
642
+ def provider_runtime(self, session_ids: Optional[List[str]] = None):
643
+ """Return the explicit provider runtime for this client."""
644
+ try:
645
+ from .provider_runtime import ProviderRuntime
646
+ except ImportError:
647
+ from provider_runtime import ProviderRuntime
648
+
649
+ if self._provider_runtime is None:
650
+ self._provider_runtime = ProviderRuntime(self, session_ids=session_ids)
651
+ elif session_ids:
652
+ self._provider_runtime.add_sessions(session_ids)
653
+ return self._provider_runtime
654
+
655
+ def start(self) -> None:
656
+ """Backward-compatible alias for the explicit provider runtime."""
657
+ self.provider_runtime().run_forever()
658
+
659
+ def stop(self) -> None:
660
+ """Stop the explicit provider runtime if it is active."""
661
+ self.running = False
662
+ if self._provider_runtime is not None:
663
+ self._provider_runtime.stop()
664
+
665
+ def _main_loop(self) -> None:
666
+ """Main polling loop for all sessions."""
667
+ while self.running:
668
+ try:
669
+ for context in self.list_sessions():
670
+ if context.machine_id:
671
+ context.poll_requests()
672
+
673
+ time.sleep(self.config.poll_interval)
674
+
675
+ except Exception as e:
676
+ logger.warning("Error in main loop: %s", e)
677
+ time.sleep(1)
678
+
679
+ def __enter__(self):
680
+ """Context manager entry."""
681
+ self.connect()
682
+ return self
683
+
684
+ def __exit__(self, exc_type, exc_val, exc_tb):
685
+ """Context manager exit."""
686
+ self.disconnect()