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,491 @@
1
+ """HTTP session context implementation."""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import time
7
+ import uuid
8
+ from typing import Any, Callable, Dict, List, Optional
9
+
10
+ from toolplane.utils.schema import generate_schema_from_function
11
+
12
+ from ..core.errors import (
13
+ ToolplaneAPIError,
14
+ ToolplaneCancelledError,
15
+ ToolplaneError,
16
+ ToolplaneInvalidArgumentError,
17
+ ToolplaneTimeoutError,
18
+ normalize_status_name,
19
+ )
20
+ from ..core.session_context import derive_wait_for
21
+ from .http_connection import HTTPConnectionManager
22
+ from .http_machine import HTTPMachineManager
23
+ from .http_request import HTTPRequestManager
24
+ from .http_session import HTTPSessionManager
25
+ from .http_tool import HTTPToolManager
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class HTTPSessionContext:
31
+ """
32
+ Represents an HTTP session context with its own tools and machine registration.
33
+ Encapsulates all session-specific state and operations.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ session_id: str,
39
+ connection_manager: HTTPConnectionManager,
40
+ machine_manager: HTTPMachineManager,
41
+ tool_manager: HTTPToolManager,
42
+ request_manager: HTTPRequestManager,
43
+ session_manager: HTTPSessionManager,
44
+ ):
45
+ """Initialize HTTP session context."""
46
+ self.session_id = session_id
47
+ self.connection_manager = connection_manager
48
+ self.machine_manager = machine_manager
49
+ self.tool_manager = tool_manager
50
+ self.request_manager = request_manager
51
+ self.session_manager = session_manager
52
+
53
+ self.machine_id: Optional[str] = None
54
+
55
+ def register_machine(self) -> bool:
56
+ """Register a machine for this session."""
57
+ try:
58
+ self.machine_id = self.machine_manager.register_machine(self.session_id)
59
+ logger.debug(
60
+ "Registered machine %s for session %s", self.machine_id, self.session_id
61
+ )
62
+ return True
63
+ except Exception as e:
64
+ logger.warning(
65
+ "Error registering machine for session %s: %s", self.session_id, e
66
+ )
67
+ return False
68
+
69
+ def register_tool(
70
+ self,
71
+ name: str,
72
+ func: Callable,
73
+ schema: Optional[Dict] = None,
74
+ description: Optional[str] = None,
75
+ stream: bool = False,
76
+ tags: Optional[List[str]] = None,
77
+ ):
78
+ """Register a tool for this session."""
79
+ if not self.machine_id:
80
+ raise ToolplaneError(
81
+ f"Session {self.session_id} has no machine registration. Use ProviderRuntime.attach_session(...) or register_machine() before registering tools."
82
+ )
83
+
84
+ try:
85
+ self.tool_manager.register_tool(
86
+ self.session_id,
87
+ self.machine_id,
88
+ name,
89
+ func,
90
+ schema,
91
+ description,
92
+ stream,
93
+ tags,
94
+ )
95
+ logger.debug(
96
+ "Registered tool %s%s for session %s",
97
+ name,
98
+ " (streaming)" if stream else "",
99
+ self.session_id,
100
+ )
101
+ except Exception as e:
102
+ raise ToolplaneError(
103
+ f"Failed to register tool {name} for session {self.session_id}: {e}"
104
+ )
105
+
106
+ def invoke(
107
+ self,
108
+ tool_name: str,
109
+ timeout_seconds: int = 0,
110
+ wait_timeout: Optional[int] = None,
111
+ **params,
112
+ ) -> Any:
113
+ """Invoke a tool in this session and return the tool's result value.
114
+
115
+ timeout_seconds sets the request's absolute per-attempt execution
116
+ timeout on the wire (0 keeps the server default). The local wait ends
117
+ after wait_timeout seconds (default: timeout_seconds + 15, else 60).
118
+ """
119
+ try:
120
+ # Wait budget: also drives the server-side long-poll. The POST
121
+ # must complete inside the transport deadline — a wait that
122
+ # outlives it times out, gets retried, and duplicates the
123
+ # invocation — so the long-poll is clamped to the transport
124
+ # budget and the local poller covers the remainder. The shared
125
+ # derivation clamps derived waits to the server's wait ceiling.
126
+ wait_for = derive_wait_for(timeout_seconds, wait_timeout)
127
+
128
+ deadline = time.monotonic() + wait_for
129
+
130
+ transport_budget = max(
131
+ 5,
132
+ int(getattr(self.connection_manager.config, "request_timeout", 30)) - 5,
133
+ )
134
+ http_wait = min(wait_for, transport_budget)
135
+
136
+ request_id, terminal_status, result = self.tool_manager.execute_tool(
137
+ self.session_id,
138
+ tool_name,
139
+ params,
140
+ timeout_seconds=timeout_seconds,
141
+ wait_timeout_seconds=http_wait,
142
+ )
143
+
144
+ # Poll for completion; the HTTP wait already returns the unwrapped
145
+ # tool result.
146
+ if terminal_status == "done":
147
+ return result
148
+ if terminal_status == "cancelled":
149
+ raise ToolplaneCancelledError(
150
+ f"Request was cancelled (request_id={request_id})"
151
+ )
152
+ if terminal_status == "failed":
153
+ raise ToolplaneError(f"Tool execution failed (request_id={request_id})")
154
+ remaining = max(1, int(deadline - time.monotonic()))
155
+ return self._wait_for_completion(request_id, timeout=remaining)
156
+
157
+ except ToolplaneTimeoutError:
158
+ raise
159
+ except ToolplaneAPIError:
160
+ # Typed server errors keep their identity for the caller.
161
+ raise
162
+
163
+ async def ainvoke(
164
+ self,
165
+ tool_name: str,
166
+ timeout_seconds: int = 0,
167
+ **params,
168
+ ) -> str:
169
+ """Submit a tool invocation without blocking the caller.
170
+
171
+ Returns the request ID once the gateway accepts the work; poll
172
+ get_request_status (or await astream) for the outcome. The blocking
173
+ submission runs in a worker thread, so this is safe to await from a
174
+ running event loop.
175
+ """
176
+ try:
177
+ payload = json.dumps(params)
178
+ return await asyncio.to_thread(
179
+ self.request_manager.create_request,
180
+ self.session_id,
181
+ tool_name,
182
+ payload,
183
+ timeout_seconds=timeout_seconds,
184
+ )
185
+ except Exception as e:
186
+ raise ToolplaneError(f"Failed to async invoke tool {tool_name}: {e}")
187
+
188
+ def stream(self, tool_name: str, callback: Callable[[Any, bool], None], **params):
189
+ """Stream tool execution.
190
+
191
+ The stream is resumable: if the direct stream fails mid-flight, the
192
+ fallback resumes from the last received sequence number (or, when it
193
+ died before the first chunk, re-invokes under the same idempotency
194
+ key so the server returns the original request). A tool that
195
+ completed with an error is never re-executed.
196
+ """
197
+ idempotency_key = uuid.uuid4().hex
198
+ all_chunks = []
199
+ request_id = None
200
+ last_seq = 0
201
+
202
+ try:
203
+ try:
204
+ for chunk in self.tool_manager.stream_tool(
205
+ self.session_id, tool_name, params, idempotency_key
206
+ ):
207
+ chunk_data = chunk.get("result", {})
208
+ is_final = chunk_data.get("isFinal", False)
209
+ chunk_content = chunk_data.get("chunk", "")
210
+
211
+ if chunk_data.get("requestId"):
212
+ request_id = chunk_data.get("requestId")
213
+ if chunk_data.get("seq"):
214
+ last_seq = max(last_seq, int(chunk_data.get("seq", 0)))
215
+
216
+ callback(chunk_content, is_final)
217
+ all_chunks.append(chunk_content)
218
+
219
+ stream_error = chunk_data.get("error") or chunk.get("error")
220
+ if stream_error:
221
+ raise ToolplaneError(f"Streaming error: {stream_error}")
222
+
223
+ if is_final:
224
+ break
225
+
226
+ return all_chunks
227
+
228
+ except ToolplaneError:
229
+ # The tool itself failed: re-executing it would duplicate the
230
+ # side effect, so surface the failure.
231
+ raise
232
+ except Exception:
233
+ # Transport failure mid-stream: resume from the last sequence
234
+ # number the server acknowledges.
235
+ if request_id:
236
+ return self._resume_stream(
237
+ request_id,
238
+ last_seq,
239
+ callback,
240
+ all_chunks,
241
+ tool_name,
242
+ params,
243
+ idempotency_key,
244
+ )
245
+ # The stream died before any chunk arrived. The server may
246
+ # already have created the request; re-invoking under the same
247
+ # idempotency key returns that request instead of executing
248
+ # the tool a second time.
249
+ return self._stream_via_polling(
250
+ tool_name, callback, params, idempotency_key
251
+ )
252
+
253
+ except ToolplaneError:
254
+ raise
255
+ except Exception as e:
256
+ raise ToolplaneError(f"Failed to stream tool {tool_name}: {e}")
257
+
258
+ def _resume_stream(
259
+ self,
260
+ request_id: str,
261
+ last_seq: int,
262
+ callback: Callable,
263
+ all_chunks: List,
264
+ tool_name: str,
265
+ params: Dict,
266
+ idempotency_key: str,
267
+ ):
268
+ """Continue a broken stream from the last acknowledged sequence."""
269
+ try:
270
+ for chunk in self.request_manager.resume_stream(
271
+ self.session_id, request_id, last_seq
272
+ ):
273
+ value = chunk["chunk"]
274
+ if value not in ("", None):
275
+ callback(value, chunk["is_final"])
276
+ all_chunks.append(value)
277
+ if chunk["error"]:
278
+ raise ToolplaneError(f"Streaming error: {chunk['error']}")
279
+ if chunk["is_final"]:
280
+ return all_chunks
281
+ except ToolplaneInvalidArgumentError:
282
+ # The retained window moved past our position; the full result is
283
+ # still fetchable by polling the original request.
284
+ self._stream_via_polling(
285
+ tool_name,
286
+ callback,
287
+ params,
288
+ idempotency_key,
289
+ request_id=request_id,
290
+ skip=len(all_chunks),
291
+ accumulate=all_chunks,
292
+ )
293
+ return all_chunks
294
+
295
+ def _stream_via_polling(
296
+ self,
297
+ tool_name: str,
298
+ callback: Callable,
299
+ params: Dict,
300
+ idempotency_key: str = "",
301
+ request_id: Optional[str] = None,
302
+ skip: int = 0,
303
+ accumulate: Optional[List] = None,
304
+ ):
305
+ """Stream via polling fallback.
306
+
307
+ When request_id is given, polls that request; otherwise invokes the
308
+ tool (under idempotency_key when provided) and polls the result.
309
+ skip suppresses the first skip chunks, which the caller already
310
+ delivered. When accumulate is given, polled chunks append to it so
311
+ callers keep the chunks they already delivered.
312
+ """
313
+ if request_id is None:
314
+ request_id = self.tool_manager.execute_tool(
315
+ self.session_id, tool_name, params, idempotency_key
316
+ )[0]
317
+
318
+ all_chunks = accumulate if accumulate is not None else []
319
+ last_chunk_count = skip
320
+
321
+ while True:
322
+ status = self.get_request_status(request_id)
323
+
324
+ if "streamResults" in status:
325
+ chunks = status["streamResults"]
326
+
327
+ # Process new chunks
328
+ for i in range(last_chunk_count, len(chunks)):
329
+ callback(chunks[i], False)
330
+ all_chunks.append(chunks[i])
331
+
332
+ last_chunk_count = len(chunks)
333
+
334
+ if normalize_status_name(status["status"]) == "done":
335
+ callback("", True)
336
+ break
337
+
338
+ if normalize_status_name(status["status"]) == "cancelled":
339
+ raise ToolplaneCancelledError(
340
+ "Request was cancelled "
341
+ f"(request_id={status.get('requestId', request_id)})"
342
+ )
343
+
344
+ if normalize_status_name(status["status"]) == "failure":
345
+ raise ToolplaneError(
346
+ f"Streaming failed: {status.get('error', 'Unknown error')}"
347
+ )
348
+
349
+ time.sleep(0.5)
350
+
351
+ return all_chunks
352
+
353
+ async def astream(
354
+ self, tool_name: str, callback: Callable[[Any, bool], None], **params
355
+ ):
356
+ """Awaitable stream: runs the blocking stream loop in a worker
357
+ thread and resolves with the collected chunks."""
358
+ return await asyncio.to_thread(self.stream, tool_name, callback, **params)
359
+
360
+ def get_request_status(self, request_id: str) -> Dict[str, Any]:
361
+ """Get request status."""
362
+ return self.request_manager.get_request_status(self.session_id, request_id)
363
+
364
+ def get_available_tools(self) -> Dict[str, Any]:
365
+ """Get available tools for this session."""
366
+ return self.tool_manager.get_available_tools(self.session_id)
367
+
368
+ def list_tools(self) -> List[Dict[str, Any]]:
369
+ """List tools for this session."""
370
+ return self.tool_manager.list_tools(self.session_id)
371
+
372
+ def get_tool_by_id(self, tool_id: str) -> Dict[str, Any]:
373
+ """Get a tool by ID for this session."""
374
+ return self.tool_manager.get_tool_by_id(self.session_id, tool_id)
375
+
376
+ def get_tool_by_name(self, tool_name: str) -> Dict[str, Any]:
377
+ """Get a tool by name for this session."""
378
+ return self.tool_manager.get_tool_by_name(self.session_id, tool_name)
379
+
380
+ def delete_tool(self, tool_id: str) -> bool:
381
+ """Delete a tool by ID for this session."""
382
+ return self.tool_manager.delete_tool(self.session_id, tool_id)
383
+
384
+ def tool(self, name=None, description=None, stream=False, tags=None):
385
+ """Decorator for registering tools."""
386
+ if tags is None:
387
+ tags = []
388
+
389
+ def decorator(func):
390
+ tool_name = name or func.__name__
391
+ tool_schema = generate_schema_from_function(func)
392
+
393
+ if description:
394
+ tool_schema["description"] = description
395
+
396
+ self.register_tool(tool_name, func, tool_schema, description, stream, tags)
397
+ return func
398
+
399
+ return decorator
400
+
401
+ def _wait_for_completion(self, request_id: str, timeout: int = 60) -> Any:
402
+ """Wait for request completion."""
403
+ start_time = time.time()
404
+ last_status: Dict[str, Any] = {}
405
+
406
+ while time.time() - start_time < timeout:
407
+ status = self.get_request_status(request_id)
408
+ last_status = status
409
+
410
+ if normalize_status_name(status["status"]) == "done":
411
+ try:
412
+ return json.loads(status["result"])
413
+ except (TypeError, ValueError, json.JSONDecodeError):
414
+ return status["result"]
415
+
416
+ if normalize_status_name(status["status"]) == "cancelled":
417
+ raise ToolplaneCancelledError(
418
+ f"Request was cancelled (request_id={request_id})"
419
+ )
420
+
421
+ if normalize_status_name(status["status"]) == "failure":
422
+ raise ToolplaneError(
423
+ f"Tool execution failed: {status.get('error', 'Unknown error')}"
424
+ )
425
+
426
+ time.sleep(0.5)
427
+
428
+ raise ToolplaneTimeoutError(
429
+ f"Tool execution timed out after {timeout}s (request_id={request_id}, "
430
+ f"status={last_status.get('status', 'unknown')})"
431
+ )
432
+
433
+ def cleanup(self):
434
+ """Cleanup this session."""
435
+ try:
436
+ # Cleanup tools
437
+ self.tool_manager.cleanup_session_tools(self.session_id)
438
+
439
+ # Unregister machine
440
+ if self.machine_id:
441
+ self.machine_manager.unregister_machine(
442
+ self.session_id, reason="session_context_cleanup"
443
+ )
444
+
445
+ # Remove from session manager
446
+ self.session_manager.remove_session_context(self.session_id)
447
+
448
+ except Exception as e:
449
+ logger.warning("Error cleaning up session %s: %s", self.session_id, e)
450
+
451
+ def poll_requests(self):
452
+ """Poll for requests in this session."""
453
+ if not self.machine_id:
454
+ return
455
+
456
+ try:
457
+ tools = self.tool_manager.get_session_tools(self.session_id)
458
+ streaming_tools = self.tool_manager.streaming_tools.get(
459
+ self.session_id, set()
460
+ )
461
+
462
+ self.request_manager.poll_session_requests(
463
+ self.session_id, self.machine_id, tools, streaming_tools
464
+ )
465
+ except Exception as e:
466
+ logger.warning(
467
+ "Error polling requests for session %s: %s", self.session_id, e
468
+ )
469
+
470
+ # New methods for user session management
471
+ def list_user_sessions(
472
+ self, user_id: str, page_size: int = 10, page_token: int = 0, filter: str = ""
473
+ ) -> Dict[str, Any]:
474
+ """List user sessions with pagination and filtering."""
475
+ return self.session_manager.list_user_sessions(
476
+ user_id, page_size, page_token, filter
477
+ )
478
+
479
+ def bulk_delete_sessions(
480
+ self, user_id: str, session_ids: Optional[List[str]] = None, filter: str = ""
481
+ ) -> Dict[str, Any]:
482
+ """Bulk delete sessions for a user."""
483
+ return self.session_manager.bulk_delete_sessions(user_id, session_ids, filter)
484
+
485
+ def get_session_stats(self, user_id: str) -> Dict[str, int]:
486
+ """Get session statistics for a user."""
487
+ return self.session_manager.get_session_stats(user_id)
488
+
489
+ def invalidate_session(self, reason: str = "") -> bool:
490
+ """Invalidate a session."""
491
+ return self.session_manager.invalidate_session(self.session_id, reason)
@@ -0,0 +1,101 @@
1
+ """HTTP task management for Toolplane client."""
2
+
3
+ from typing import Any, Dict, List
4
+
5
+ from ..common.utils import format_error_message
6
+ from ..core.errors import TaskError
7
+ from .http_connection import HTTPConnectionManager
8
+
9
+
10
+ class HTTPTaskManager:
11
+ """Manages task lifecycle for the HTTP client."""
12
+
13
+ def __init__(self, connection_manager: HTTPConnectionManager):
14
+ """Initialize HTTP task manager."""
15
+ self.connection_manager = connection_manager
16
+
17
+ def _normalize_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
18
+ return {
19
+ "id": task.get("id", ""),
20
+ "session_id": task.get("sessionId", task.get("session_id", "")),
21
+ "tool_name": task.get("toolName", task.get("tool_name", "")),
22
+ "status": task.get("status", ""),
23
+ "input": task.get("input", ""),
24
+ "result": task.get("result", ""),
25
+ "result_type": task.get("resultType", task.get("result_type", "")),
26
+ "error": task.get("error", ""),
27
+ "created_at": task.get("createdAt", task.get("created_at", "")),
28
+ "updated_at": task.get("updatedAt", task.get("updated_at", "")),
29
+ "completed_at": task.get("completedAt", task.get("completed_at", "")),
30
+ }
31
+
32
+ def create_task(
33
+ self,
34
+ session_id: str,
35
+ tool_name: str,
36
+ input_data: str,
37
+ idempotency_key: str = "",
38
+ ) -> Dict[str, Any]:
39
+ """Create a task for a session.
40
+
41
+ idempotency_key, when set, dedups creates within the session:
42
+ retrying with the same key returns the original task without
43
+ re-executing it.
44
+ """
45
+ try:
46
+ self.connection_manager.ensure_connected()
47
+ payload = {
48
+ "sessionId": session_id,
49
+ "toolName": tool_name,
50
+ "input": input_data,
51
+ }
52
+ if idempotency_key:
53
+ payload["idempotencyKey"] = idempotency_key
54
+ response = self.connection_manager.create_task(payload)
55
+ task = response.get("task", response)
56
+ if not isinstance(task, dict):
57
+ raise TaskError(
58
+ f"Unexpected task payload for session {session_id}: {task}"
59
+ )
60
+ return self._normalize_task(task)
61
+ except Exception as exc:
62
+ raise TaskError(format_error_message(exc, "Failed to create task"))
63
+
64
+ def get_task(self, session_id: str, task_id: str) -> Dict[str, Any]:
65
+ """Get a task for a session."""
66
+ try:
67
+ self.connection_manager.ensure_connected()
68
+ response = self.connection_manager.get_task(session_id, task_id)
69
+ task = response.get("task", response)
70
+ if not isinstance(task, dict):
71
+ raise TaskError(
72
+ f"Unexpected task payload for session {session_id}: {task}"
73
+ )
74
+ return self._normalize_task(task)
75
+ except Exception as exc:
76
+ raise TaskError(format_error_message(exc, f"Failed to get task {task_id}"))
77
+
78
+ def list_tasks(self, session_id: str) -> List[Dict[str, Any]]:
79
+ """List tasks for a session."""
80
+ try:
81
+ self.connection_manager.ensure_connected()
82
+ response = self.connection_manager.list_tasks(session_id)
83
+ tasks = response.get("tasks", [])
84
+ return [self._normalize_task(task) for task in tasks]
85
+ except Exception as exc:
86
+ raise TaskError(
87
+ format_error_message(
88
+ exc, f"Failed to list tasks for session {session_id}"
89
+ )
90
+ )
91
+
92
+ def cancel_task(self, session_id: str, task_id: str) -> bool:
93
+ """Cancel a task for a session."""
94
+ try:
95
+ self.connection_manager.ensure_connected()
96
+ response = self.connection_manager.cancel_task(session_id, task_id)
97
+ return response.get("success", False)
98
+ except Exception as exc:
99
+ raise TaskError(
100
+ format_error_message(exc, f"Failed to cancel task {task_id}")
101
+ )