gobby 0.2.5__py3-none-any.whl → 0.2.6__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.
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex.py +43 -3
- gobby/agents/runner.py +8 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +9 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +2 -8
- gobby/cli/installers/shared.py +71 -8
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +3 -3
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/app.py +63 -1
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +6 -14
- gobby/hooks/event_handlers.py +145 -2
- gobby/hooks/hook_manager.py +48 -2
- gobby/hooks/skill_manager.py +130 -0
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +95 -33
- gobby/mcp_proxy/instructions.py +54 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -5
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/clones.py +903 -0
- gobby/mcp_proxy/tools/memory.py +1 -24
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/session_messages.py +1 -2
- gobby/mcp_proxy/tools/skills/__init__.py +631 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +60 -29
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +5 -0
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/manager.py +11 -2
- gobby/prompts/defaults/handoff/compact.md +63 -0
- gobby/prompts/defaults/handoff/session_end.md +57 -0
- gobby/prompts/defaults/memory/extract.md +61 -0
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +23 -8
- gobby/servers/routes/admin.py +280 -0
- gobby/servers/routes/mcp/tools.py +241 -52
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +64 -5
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +258 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +180 -6
- gobby/storage/sessions.py +73 -0
- gobby/storage/skills.py +749 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +41 -6
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +39 -4
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/validation.py +24 -15
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/workflows/actions.py +84 -2
- gobby/workflows/context_actions.py +43 -0
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/engine.py +13 -2
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/loader.py +19 -6
- gobby/workflows/memory_actions.py +74 -0
- gobby/workflows/summary_actions.py +17 -0
- gobby/workflows/task_enforcement_actions.py +448 -6
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/METADATA +82 -21
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/RECORD +136 -107
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/WHEEL +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"""Task orchestration tools: wait (wait_for_task, wait_for_any_task, wait_for_all_tasks).
|
|
2
|
+
|
|
3
|
+
Provides blocking wait operations for task completion with timeout support.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
14
|
+
from gobby.storage.tasks import TaskNotFoundError
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Default timeout and poll interval
|
|
22
|
+
DEFAULT_TIMEOUT = 300.0 # 5 minutes
|
|
23
|
+
DEFAULT_POLL_INTERVAL = 5.0 # 5 seconds
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def register_wait(
|
|
27
|
+
registry: InternalToolRegistry,
|
|
28
|
+
task_manager: LocalTaskManager,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Register wait tools for task completion.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
registry: The tool registry to add tools to
|
|
35
|
+
task_manager: Task manager for checking task status
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def _resolve_task_id(task_ref: str) -> str:
|
|
39
|
+
"""Resolve a task reference to its UUID."""
|
|
40
|
+
from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
|
|
41
|
+
|
|
42
|
+
return resolve_task_id_for_mcp(task_manager, task_ref)
|
|
43
|
+
|
|
44
|
+
def _is_task_complete(task_id: str) -> tuple[bool, dict[str, Any] | None]:
|
|
45
|
+
"""
|
|
46
|
+
Check if a task is complete.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Tuple of (is_complete, task_info_dict or None)
|
|
50
|
+
"""
|
|
51
|
+
task = task_manager.get_task(task_id)
|
|
52
|
+
if task is None:
|
|
53
|
+
return False, None
|
|
54
|
+
|
|
55
|
+
task_info = {
|
|
56
|
+
"id": task.id,
|
|
57
|
+
"seq_num": task.seq_num,
|
|
58
|
+
"title": task.title,
|
|
59
|
+
"status": task.status,
|
|
60
|
+
"closed_at": task.closed_at,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Consider task complete if status is "closed" or "review"
|
|
64
|
+
# (review tasks have completed their work, just awaiting human approval)
|
|
65
|
+
is_complete = task.status in ("closed", "review")
|
|
66
|
+
return is_complete, task_info
|
|
67
|
+
|
|
68
|
+
async def wait_for_task(
|
|
69
|
+
task_id: str,
|
|
70
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
71
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
72
|
+
) -> dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Wait for a single task to complete.
|
|
75
|
+
|
|
76
|
+
Blocks until the task reaches "closed" or "review" status, or timeout expires.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
task_id: Task reference (#N, N, path, or UUID)
|
|
80
|
+
timeout: Maximum wait time in seconds (default: 300)
|
|
81
|
+
poll_interval: Time between status checks in seconds (default: 5)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Dict with:
|
|
85
|
+
- success: Whether the operation succeeded
|
|
86
|
+
- completed: Whether the task completed
|
|
87
|
+
- timed_out: Whether timeout was reached
|
|
88
|
+
- task: Task info dict (if found)
|
|
89
|
+
- wait_time: How long we waited
|
|
90
|
+
"""
|
|
91
|
+
# Validate poll_interval
|
|
92
|
+
if poll_interval <= 0:
|
|
93
|
+
poll_interval = DEFAULT_POLL_INTERVAL
|
|
94
|
+
|
|
95
|
+
start_time = time.monotonic()
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
resolved_id = _resolve_task_id(task_id)
|
|
99
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
100
|
+
return {
|
|
101
|
+
"success": False,
|
|
102
|
+
"error": f"Task not found: {task_id} ({e})",
|
|
103
|
+
}
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return {
|
|
106
|
+
"success": False,
|
|
107
|
+
"error": f"Failed to resolve task: {task_id} ({e})",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Check initial state
|
|
111
|
+
try:
|
|
112
|
+
is_complete, task_info = _is_task_complete(resolved_id)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return {
|
|
115
|
+
"success": False,
|
|
116
|
+
"error": f"Failed to check task status: {e}",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if task_info is None:
|
|
120
|
+
return {
|
|
121
|
+
"success": False,
|
|
122
|
+
"error": f"Task not found: {task_id}",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if is_complete:
|
|
126
|
+
return {
|
|
127
|
+
"success": True,
|
|
128
|
+
"completed": True,
|
|
129
|
+
"timed_out": False,
|
|
130
|
+
"task": task_info,
|
|
131
|
+
"wait_time": 0.0,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Poll until complete or timeout
|
|
135
|
+
while True:
|
|
136
|
+
elapsed = time.monotonic() - start_time
|
|
137
|
+
|
|
138
|
+
if elapsed >= timeout:
|
|
139
|
+
# Re-fetch latest task state before returning timeout
|
|
140
|
+
try:
|
|
141
|
+
_, task_info = _is_task_complete(resolved_id)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.warning(f"Error fetching final task status on timeout: {e}")
|
|
144
|
+
return {
|
|
145
|
+
"success": True,
|
|
146
|
+
"completed": False,
|
|
147
|
+
"timed_out": True,
|
|
148
|
+
"task": task_info,
|
|
149
|
+
"wait_time": elapsed,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await asyncio.sleep(poll_interval)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
is_complete, task_info = _is_task_complete(resolved_id)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.warning(f"Error checking task status: {e}")
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
if is_complete:
|
|
161
|
+
return {
|
|
162
|
+
"success": True,
|
|
163
|
+
"completed": True,
|
|
164
|
+
"timed_out": False,
|
|
165
|
+
"task": task_info,
|
|
166
|
+
"wait_time": time.monotonic() - start_time,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
registry.register(
|
|
170
|
+
name="wait_for_task",
|
|
171
|
+
description=(
|
|
172
|
+
"Wait for a single task to complete. "
|
|
173
|
+
"Blocks until task reaches 'closed' or 'review' status, or timeout expires."
|
|
174
|
+
),
|
|
175
|
+
input_schema={
|
|
176
|
+
"type": "object",
|
|
177
|
+
"properties": {
|
|
178
|
+
"task_id": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"description": "Task reference: #N, N (seq_num), path (1.2.3), or UUID",
|
|
181
|
+
},
|
|
182
|
+
"timeout": {
|
|
183
|
+
"type": "number",
|
|
184
|
+
"description": f"Maximum wait time in seconds (default: {DEFAULT_TIMEOUT})",
|
|
185
|
+
},
|
|
186
|
+
"poll_interval": {
|
|
187
|
+
"type": "number",
|
|
188
|
+
"description": f"Time between status checks in seconds (default: {DEFAULT_POLL_INTERVAL})",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
"required": ["task_id"],
|
|
192
|
+
},
|
|
193
|
+
func=wait_for_task,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
async def wait_for_any_task(
|
|
197
|
+
task_ids: list[str],
|
|
198
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
199
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
200
|
+
) -> dict[str, Any]:
|
|
201
|
+
"""
|
|
202
|
+
Wait for any one of multiple tasks to complete.
|
|
203
|
+
|
|
204
|
+
Blocks until at least one task reaches "closed" or "review" status, or timeout expires.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
task_ids: List of task references (#N, N, path, or UUID)
|
|
208
|
+
timeout: Maximum wait time in seconds (default: 300)
|
|
209
|
+
poll_interval: Time between status checks in seconds (default: 5)
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Dict with:
|
|
213
|
+
- success: Whether the operation succeeded
|
|
214
|
+
- completed_task_id: ID of the first completed task (or None)
|
|
215
|
+
- timed_out: Whether timeout was reached
|
|
216
|
+
- wait_time: How long we waited
|
|
217
|
+
"""
|
|
218
|
+
if not task_ids:
|
|
219
|
+
return {
|
|
220
|
+
"success": False,
|
|
221
|
+
"error": "No task IDs provided - task_ids list is empty",
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# Validate poll_interval
|
|
225
|
+
if poll_interval <= 0:
|
|
226
|
+
poll_interval = DEFAULT_POLL_INTERVAL
|
|
227
|
+
|
|
228
|
+
start_time = time.monotonic()
|
|
229
|
+
|
|
230
|
+
# Resolve all task IDs upfront
|
|
231
|
+
resolved_ids = []
|
|
232
|
+
for task_ref in task_ids:
|
|
233
|
+
try:
|
|
234
|
+
resolved_id = _resolve_task_id(task_ref)
|
|
235
|
+
resolved_ids.append(resolved_id)
|
|
236
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
237
|
+
logger.warning(f"Could not resolve task {task_ref}: {e}")
|
|
238
|
+
# Continue with other tasks
|
|
239
|
+
|
|
240
|
+
if not resolved_ids:
|
|
241
|
+
return {
|
|
242
|
+
"success": False,
|
|
243
|
+
"error": "None of the provided task IDs could be resolved",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Check if any are already complete
|
|
247
|
+
for resolved_id in resolved_ids:
|
|
248
|
+
try:
|
|
249
|
+
is_complete, task_info = _is_task_complete(resolved_id)
|
|
250
|
+
if is_complete:
|
|
251
|
+
return {
|
|
252
|
+
"success": True,
|
|
253
|
+
"completed_task_id": resolved_id,
|
|
254
|
+
"task": task_info,
|
|
255
|
+
"timed_out": False,
|
|
256
|
+
"wait_time": 0.0,
|
|
257
|
+
}
|
|
258
|
+
except Exception as e:
|
|
259
|
+
logger.warning(f"Error checking task {resolved_id}: {e}")
|
|
260
|
+
|
|
261
|
+
# Poll until one completes or timeout
|
|
262
|
+
while True:
|
|
263
|
+
elapsed = time.monotonic() - start_time
|
|
264
|
+
|
|
265
|
+
if elapsed >= timeout:
|
|
266
|
+
return {
|
|
267
|
+
"success": True,
|
|
268
|
+
"completed_task_id": None,
|
|
269
|
+
"timed_out": True,
|
|
270
|
+
"wait_time": elapsed,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await asyncio.sleep(poll_interval)
|
|
274
|
+
|
|
275
|
+
for resolved_id in resolved_ids:
|
|
276
|
+
try:
|
|
277
|
+
is_complete, task_info = _is_task_complete(resolved_id)
|
|
278
|
+
if is_complete:
|
|
279
|
+
return {
|
|
280
|
+
"success": True,
|
|
281
|
+
"completed_task_id": resolved_id,
|
|
282
|
+
"task": task_info,
|
|
283
|
+
"timed_out": False,
|
|
284
|
+
"wait_time": time.monotonic() - start_time,
|
|
285
|
+
}
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.warning(f"Error checking task {resolved_id}: {e}")
|
|
288
|
+
|
|
289
|
+
registry.register(
|
|
290
|
+
name="wait_for_any_task",
|
|
291
|
+
description=(
|
|
292
|
+
"Wait for any one of multiple tasks to complete. "
|
|
293
|
+
"Returns as soon as the first task reaches 'closed' or 'review' status."
|
|
294
|
+
),
|
|
295
|
+
input_schema={
|
|
296
|
+
"type": "object",
|
|
297
|
+
"properties": {
|
|
298
|
+
"task_ids": {
|
|
299
|
+
"type": "array",
|
|
300
|
+
"items": {"type": "string"},
|
|
301
|
+
"description": "List of task references",
|
|
302
|
+
},
|
|
303
|
+
"timeout": {
|
|
304
|
+
"type": "number",
|
|
305
|
+
"description": f"Maximum wait time in seconds (default: {DEFAULT_TIMEOUT})",
|
|
306
|
+
},
|
|
307
|
+
"poll_interval": {
|
|
308
|
+
"type": "number",
|
|
309
|
+
"description": f"Time between status checks in seconds (default: {DEFAULT_POLL_INTERVAL})",
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
"required": ["task_ids"],
|
|
313
|
+
},
|
|
314
|
+
func=wait_for_any_task,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
async def wait_for_all_tasks(
|
|
318
|
+
task_ids: list[str],
|
|
319
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
320
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
321
|
+
) -> dict[str, Any]:
|
|
322
|
+
"""
|
|
323
|
+
Wait for all tasks to complete.
|
|
324
|
+
|
|
325
|
+
Blocks until all tasks reach "closed" or "review" status, or timeout expires.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
task_ids: List of task references (#N, N, path, or UUID)
|
|
329
|
+
timeout: Maximum wait time in seconds (default: 300)
|
|
330
|
+
poll_interval: Time between status checks in seconds (default: 5)
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Dict with:
|
|
334
|
+
- success: Whether the operation succeeded
|
|
335
|
+
- all_completed: Whether all tasks completed
|
|
336
|
+
- completed_count: Number of completed tasks
|
|
337
|
+
- pending_count: Number of still-pending tasks
|
|
338
|
+
- timed_out: Whether timeout was reached
|
|
339
|
+
- completed_tasks: List of completed task IDs
|
|
340
|
+
- pending_tasks: List of pending task IDs
|
|
341
|
+
- wait_time: How long we waited
|
|
342
|
+
"""
|
|
343
|
+
if not task_ids:
|
|
344
|
+
# Empty list is vacuously true - all (zero) tasks are complete
|
|
345
|
+
return {
|
|
346
|
+
"success": True,
|
|
347
|
+
"all_completed": True,
|
|
348
|
+
"completed_count": 0,
|
|
349
|
+
"pending_count": 0,
|
|
350
|
+
"timed_out": False,
|
|
351
|
+
"completed_tasks": [],
|
|
352
|
+
"pending_tasks": [],
|
|
353
|
+
"wait_time": 0.0,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# Validate poll_interval
|
|
357
|
+
if poll_interval <= 0:
|
|
358
|
+
poll_interval = DEFAULT_POLL_INTERVAL
|
|
359
|
+
|
|
360
|
+
start_time = time.monotonic()
|
|
361
|
+
|
|
362
|
+
# Resolve all task IDs upfront
|
|
363
|
+
resolved_ids = []
|
|
364
|
+
for task_ref in task_ids:
|
|
365
|
+
try:
|
|
366
|
+
resolved_id = _resolve_task_id(task_ref)
|
|
367
|
+
resolved_ids.append(resolved_id)
|
|
368
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
369
|
+
logger.warning(f"Could not resolve task {task_ref}: {e}")
|
|
370
|
+
|
|
371
|
+
if not resolved_ids:
|
|
372
|
+
return {
|
|
373
|
+
"success": False,
|
|
374
|
+
"error": "None of the provided task IDs could be resolved",
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
def check_all_complete() -> tuple[list[str], list[str]]:
|
|
378
|
+
"""Check which tasks are complete. Returns (completed, pending)."""
|
|
379
|
+
completed = []
|
|
380
|
+
pending = []
|
|
381
|
+
for resolved_id in resolved_ids:
|
|
382
|
+
try:
|
|
383
|
+
is_complete, _ = _is_task_complete(resolved_id)
|
|
384
|
+
if is_complete:
|
|
385
|
+
completed.append(resolved_id)
|
|
386
|
+
else:
|
|
387
|
+
pending.append(resolved_id)
|
|
388
|
+
except Exception as e:
|
|
389
|
+
logger.warning(f"Error checking task {resolved_id}: {e}")
|
|
390
|
+
pending.append(resolved_id) # Assume not complete on error
|
|
391
|
+
return completed, pending
|
|
392
|
+
|
|
393
|
+
# Check initial state
|
|
394
|
+
completed, pending = check_all_complete()
|
|
395
|
+
|
|
396
|
+
if not pending:
|
|
397
|
+
return {
|
|
398
|
+
"success": True,
|
|
399
|
+
"all_completed": True,
|
|
400
|
+
"completed_count": len(completed),
|
|
401
|
+
"pending_count": 0,
|
|
402
|
+
"timed_out": False,
|
|
403
|
+
"completed_tasks": completed,
|
|
404
|
+
"pending_tasks": [],
|
|
405
|
+
"wait_time": 0.0,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# Poll until all complete or timeout
|
|
409
|
+
while True:
|
|
410
|
+
elapsed = time.monotonic() - start_time
|
|
411
|
+
|
|
412
|
+
if elapsed >= timeout:
|
|
413
|
+
completed, pending = check_all_complete()
|
|
414
|
+
return {
|
|
415
|
+
"success": True,
|
|
416
|
+
"all_completed": False,
|
|
417
|
+
"completed_count": len(completed),
|
|
418
|
+
"pending_count": len(pending),
|
|
419
|
+
"timed_out": True,
|
|
420
|
+
"completed_tasks": completed,
|
|
421
|
+
"pending_tasks": pending,
|
|
422
|
+
"wait_time": elapsed,
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await asyncio.sleep(poll_interval)
|
|
426
|
+
|
|
427
|
+
completed, pending = check_all_complete()
|
|
428
|
+
|
|
429
|
+
if not pending:
|
|
430
|
+
return {
|
|
431
|
+
"success": True,
|
|
432
|
+
"all_completed": True,
|
|
433
|
+
"completed_count": len(completed),
|
|
434
|
+
"pending_count": 0,
|
|
435
|
+
"timed_out": False,
|
|
436
|
+
"completed_tasks": completed,
|
|
437
|
+
"pending_tasks": [],
|
|
438
|
+
"wait_time": time.monotonic() - start_time,
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
registry.register(
|
|
442
|
+
name="wait_for_all_tasks",
|
|
443
|
+
description=(
|
|
444
|
+
"Wait for all tasks to complete. "
|
|
445
|
+
"Blocks until all tasks reach 'closed' or 'review' status, or timeout expires."
|
|
446
|
+
),
|
|
447
|
+
input_schema={
|
|
448
|
+
"type": "object",
|
|
449
|
+
"properties": {
|
|
450
|
+
"task_ids": {
|
|
451
|
+
"type": "array",
|
|
452
|
+
"items": {"type": "string"},
|
|
453
|
+
"description": "List of task references",
|
|
454
|
+
},
|
|
455
|
+
"timeout": {
|
|
456
|
+
"type": "number",
|
|
457
|
+
"description": f"Maximum wait time in seconds (default: {DEFAULT_TIMEOUT})",
|
|
458
|
+
},
|
|
459
|
+
"poll_interval": {
|
|
460
|
+
"type": "number",
|
|
461
|
+
"description": f"Time between status checks in seconds (default: {DEFAULT_POLL_INTERVAL})",
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
"required": ["task_ids"],
|
|
465
|
+
},
|
|
466
|
+
func=wait_for_all_tasks,
|
|
467
|
+
)
|
|
@@ -733,10 +733,9 @@ DO NOT use list_sessions to find your session - it won't work with multiple acti
|
|
|
733
733
|
return {
|
|
734
734
|
"found": True,
|
|
735
735
|
"session_id": session.id,
|
|
736
|
-
"external_id": session.external_id,
|
|
737
|
-
"source": session.source,
|
|
738
736
|
"project_id": session.project_id,
|
|
739
737
|
"status": session.status,
|
|
738
|
+
"agent_run_id": session.agent_run_id,
|
|
740
739
|
}
|
|
741
740
|
|
|
742
741
|
@registry.tool(
|