tamfis-code 0.2.3__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.
- tamfis_code/__init__.py +58 -0
- tamfis_code/__main__.py +4 -0
- tamfis_code/agents.py +371 -0
- tamfis_code/api_client.py +461 -0
- tamfis_code/cli.py +2351 -0
- tamfis_code/completion.py +137 -0
- tamfis_code/config.py +199 -0
- tamfis_code/doctor.py +173 -0
- tamfis_code/enforcer.py +318 -0
- tamfis_code/indexer.py +567 -0
- tamfis_code/instructions.py +121 -0
- tamfis_code/interactive.py +985 -0
- tamfis_code/local_chat.py +117 -0
- tamfis_code/local_tools.py +93 -0
- tamfis_code/mcp.py +538 -0
- tamfis_code/metrics.py +105 -0
- tamfis_code/providers.py +423 -0
- tamfis_code/references.py +336 -0
- tamfis_code/render.py +575 -0
- tamfis_code/runner.py +635 -0
- tamfis_code/runner_local.py +364 -0
- tamfis_code/safety.py +164 -0
- tamfis_code/screenshot.py +382 -0
- tamfis_code/sessions.py +253 -0
- tamfis_code/state.py +449 -0
- tamfis_code/tasks.py +42 -0
- tamfis_code/workspace.py +329 -0
- tamfis_code-0.2.3.dist-info/METADATA +78 -0
- tamfis_code-0.2.3.dist-info/RECORD +32 -0
- tamfis_code-0.2.3.dist-info/WHEEL +5 -0
- tamfis_code-0.2.3.dist-info/entry_points.txt +4 -0
- tamfis_code-0.2.3.dist-info/top_level.txt +1 -0
tamfis_code/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""TamfisGPT Code -- a standalone terminal coding agent.
|
|
2
|
+
|
|
3
|
+
By default, this package calls an LLM provider directly (Hugging Face,
|
|
4
|
+
NVIDIA NIM, OpenRouter, or Ollama -- see providers.py/runner_local.py) and
|
|
5
|
+
runs its own agent loop, tool execution (mcp.py), and local risk
|
|
6
|
+
classification/approval/mutation ledger (safety.py) -- no separate backend
|
|
7
|
+
process required. Every `ask`/`chat`/`audit`/`plan`/`agent`/`exec` command,
|
|
8
|
+
and the interactive REPL, work this way unless `--remote` is passed.
|
|
9
|
+
|
|
10
|
+
`--remote` still supports the original architecture this package started
|
|
11
|
+
as: a thin client to the TamfisGPT Remote Workspace backend (same sessions/
|
|
12
|
+
tasks/tools/approvals/events -- see tier_ii_gateway/api/remote.py), which
|
|
13
|
+
does the equivalent work server-side. That path is kept for continuity but
|
|
14
|
+
is not the primary, developed-going-forward one.
|
|
15
|
+
|
|
16
|
+
See docs/REMOTE_AGENT_MASTER_SPEC.md, Phase 21, for the original --remote
|
|
17
|
+
architecture's spec.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__version__ = "0.2.3"
|
|
21
|
+
|
|
22
|
+
# Bumped whenever a CLI release requires a minimum backend Remote API
|
|
23
|
+
# contract version. Only meaningful for --remote; the standalone path has no
|
|
24
|
+
# server to negotiate a contract version with. There is no server-side
|
|
25
|
+
# version negotiation yet (`tamfis-code doctor --remote` just checks
|
|
26
|
+
# reachability) -- this constant exists so that check has something concrete
|
|
27
|
+
# to compare against once one is added, rather than silently assuming
|
|
28
|
+
# compatibility forever.
|
|
29
|
+
MIN_COMPATIBLE_API_VERSION = "remote-ai-v2"
|
|
30
|
+
|
|
31
|
+
# New exports
|
|
32
|
+
from .completion import ShellCompleter
|
|
33
|
+
from .metrics import MetricsTracker, StreamMetrics
|
|
34
|
+
from .sessions import SessionManager, Session, Message
|
|
35
|
+
from .agents import AgentManager, SubAgent, CodeAnalyzer, TestGenerator, DocGenerator
|
|
36
|
+
from .mcp import MCPServer, ToolDefinition, call_tool
|
|
37
|
+
from .indexer import CodeIndexer, CodeSymbol, CodeFile
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# Existing...
|
|
41
|
+
'ShellCompleter',
|
|
42
|
+
'MetricsTracker',
|
|
43
|
+
'StreamMetrics',
|
|
44
|
+
'SessionManager',
|
|
45
|
+
'Session',
|
|
46
|
+
'Message',
|
|
47
|
+
'AgentManager',
|
|
48
|
+
'SubAgent',
|
|
49
|
+
'CodeAnalyzer',
|
|
50
|
+
'TestGenerator',
|
|
51
|
+
'DocGenerator',
|
|
52
|
+
'MCPServer',
|
|
53
|
+
'ToolDefinition',
|
|
54
|
+
'call_tool',
|
|
55
|
+
'CodeIndexer',
|
|
56
|
+
'CodeSymbol',
|
|
57
|
+
'CodeFile',
|
|
58
|
+
]
|
tamfis_code/__main__.py
ADDED
tamfis_code/agents.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Subagent system for autonomous task execution"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Optional, List, Callable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
import asyncio
|
|
7
|
+
import subprocess
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
import uuid
|
|
11
|
+
from pathlib import Path # <-- Add this import
|
|
12
|
+
|
|
13
|
+
class AgentStatus(Enum):
|
|
14
|
+
IDLE = "idle"
|
|
15
|
+
RUNNING = "running"
|
|
16
|
+
COMPLETED = "completed"
|
|
17
|
+
FAILED = "failed"
|
|
18
|
+
WAITING = "waiting"
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class AgentTask:
|
|
22
|
+
"""A task for a subagent"""
|
|
23
|
+
id: str
|
|
24
|
+
description: str
|
|
25
|
+
parameters: Dict[str, Any] = field(default_factory=dict)
|
|
26
|
+
status: AgentStatus = AgentStatus.IDLE
|
|
27
|
+
result: Optional[Dict[str, Any]] = None
|
|
28
|
+
error: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
class SubAgent:
|
|
31
|
+
"""Base class for subagents"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, name: str, description: str, capabilities: List[str]):
|
|
34
|
+
self.name = name
|
|
35
|
+
self.description = description
|
|
36
|
+
self.capabilities = capabilities
|
|
37
|
+
self.current_task: Optional[AgentTask] = None
|
|
38
|
+
|
|
39
|
+
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
40
|
+
"""Execute a task - to be overridden"""
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
def can_handle(self, task_description: str) -> bool:
|
|
44
|
+
"""Check if agent can handle this task"""
|
|
45
|
+
task_lower = task_description.lower()
|
|
46
|
+
for cap in self.capabilities:
|
|
47
|
+
if cap.lower() in task_lower:
|
|
48
|
+
return True
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
class CodeAnalyzer(SubAgent):
|
|
52
|
+
"""Analyzes code structure and quality"""
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
super().__init__(
|
|
56
|
+
name="code_analyzer",
|
|
57
|
+
description="Analyzes code for patterns, issues, and complexity",
|
|
58
|
+
capabilities=["analyze", "inspect", "complexity", "quality", "metrics"]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
62
|
+
"""Analyze code based on task parameters"""
|
|
63
|
+
file_path = task.parameters.get('file')
|
|
64
|
+
if not file_path:
|
|
65
|
+
return {"error": "No file specified"}
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
with open(file_path, 'r') as f:
|
|
69
|
+
content = f.read()
|
|
70
|
+
|
|
71
|
+
lines = content.split('\n')
|
|
72
|
+
result = {
|
|
73
|
+
"file": file_path,
|
|
74
|
+
"lines": len(lines),
|
|
75
|
+
"characters": len(content),
|
|
76
|
+
"functions": self._count_functions(content),
|
|
77
|
+
"classes": self._count_classes(content),
|
|
78
|
+
"imports": self._count_imports(content),
|
|
79
|
+
"complexity_score": self._calculate_complexity(content),
|
|
80
|
+
"issues": self._find_issues(content),
|
|
81
|
+
}
|
|
82
|
+
return result
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return {"error": str(e)}
|
|
85
|
+
|
|
86
|
+
def _count_functions(self, content: str) -> int:
|
|
87
|
+
return len(re.findall(r'^\s*def\s+\w+\s*\(', content, re.MULTILINE))
|
|
88
|
+
|
|
89
|
+
def _count_classes(self, content: str) -> int:
|
|
90
|
+
return len(re.findall(r'^\s*class\s+\w+', content, re.MULTILINE))
|
|
91
|
+
|
|
92
|
+
def _count_imports(self, content: str) -> int:
|
|
93
|
+
return len(re.findall(r'^\s*(?:from|import)\s+\w+', content, re.MULTILINE))
|
|
94
|
+
|
|
95
|
+
def _calculate_complexity(self, content: str) -> float:
|
|
96
|
+
lines = content.split('\n')
|
|
97
|
+
complexity = 1
|
|
98
|
+
keywords = ['if', 'elif', 'else', 'for', 'while', 'except', 'case', 'switch', '?']
|
|
99
|
+
for line in lines:
|
|
100
|
+
if any(k in line for k in keywords):
|
|
101
|
+
complexity += 1
|
|
102
|
+
return complexity
|
|
103
|
+
|
|
104
|
+
def _find_issues(self, content: str) -> List[Dict[str, Any]]:
|
|
105
|
+
issues = []
|
|
106
|
+
lines = content.split('\n')
|
|
107
|
+
|
|
108
|
+
for i, line in enumerate(lines, 1):
|
|
109
|
+
if len(line) > 120:
|
|
110
|
+
issues.append({
|
|
111
|
+
"line": i,
|
|
112
|
+
"type": "line_too_long",
|
|
113
|
+
"message": f"Line {i} exceeds 120 characters ({len(line)})"
|
|
114
|
+
})
|
|
115
|
+
if line.strip().startswith('#') and 'TODO' in line:
|
|
116
|
+
issues.append({
|
|
117
|
+
"line": i,
|
|
118
|
+
"type": "todo",
|
|
119
|
+
"message": f"TODO: {line.strip()}"
|
|
120
|
+
})
|
|
121
|
+
if line.strip().startswith('#') and 'FIXME' in line:
|
|
122
|
+
issues.append({
|
|
123
|
+
"line": i,
|
|
124
|
+
"type": "fixme",
|
|
125
|
+
"message": f"FIXME: {line.strip()}"
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return issues
|
|
129
|
+
|
|
130
|
+
class TestGenerator(SubAgent):
|
|
131
|
+
"""Generates tests for code"""
|
|
132
|
+
|
|
133
|
+
def __init__(self):
|
|
134
|
+
super().__init__(
|
|
135
|
+
name="test_generator",
|
|
136
|
+
description="Generates unit tests for code",
|
|
137
|
+
capabilities=["test", "unit test", "coverage", "pytest"]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
141
|
+
"""Generate tests for specified file"""
|
|
142
|
+
file_path = task.parameters.get('file')
|
|
143
|
+
if not file_path:
|
|
144
|
+
return {"error": "No file specified"}
|
|
145
|
+
|
|
146
|
+
# In production, this would use LLM to generate tests
|
|
147
|
+
# For now, return a template
|
|
148
|
+
funcs = self._extract_functions(file_path)
|
|
149
|
+
return {
|
|
150
|
+
"file": file_path,
|
|
151
|
+
"functions_found": len(funcs),
|
|
152
|
+
"functions": funcs,
|
|
153
|
+
"test_file": f"test_{Path(file_path).name}",
|
|
154
|
+
"status": "ready_for_generation"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
def _extract_functions(self, file_path: str) -> List[str]:
|
|
158
|
+
try:
|
|
159
|
+
with open(file_path, 'r') as f:
|
|
160
|
+
content = f.read()
|
|
161
|
+
return re.findall(r'def\s+(\w+)\s*\(', content)
|
|
162
|
+
except:
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
class DocGenerator(SubAgent):
|
|
166
|
+
"""Generates documentation for code"""
|
|
167
|
+
|
|
168
|
+
def __init__(self):
|
|
169
|
+
super().__init__(
|
|
170
|
+
name="doc_generator",
|
|
171
|
+
description="Generates documentation for code",
|
|
172
|
+
capabilities=["doc", "documentation", "comment", "docstring"]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
176
|
+
"""Generate documentation for specified file"""
|
|
177
|
+
file_path = task.parameters.get('file')
|
|
178
|
+
if not file_path:
|
|
179
|
+
return {"error": "No file specified"}
|
|
180
|
+
|
|
181
|
+
funcs = self._extract_with_docstrings(file_path)
|
|
182
|
+
return {
|
|
183
|
+
"file": file_path,
|
|
184
|
+
"functions": funcs,
|
|
185
|
+
"missing_docs": [f for f in funcs if not f.get('docstring')],
|
|
186
|
+
"status": "ready_for_review"
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
def _extract_with_docstrings(self, file_path: str) -> List[Dict[str, Any]]:
|
|
190
|
+
try:
|
|
191
|
+
with open(file_path, 'r') as f:
|
|
192
|
+
content = f.read()
|
|
193
|
+
|
|
194
|
+
results = []
|
|
195
|
+
lines = content.split('\n')
|
|
196
|
+
i = 0
|
|
197
|
+
while i < len(lines):
|
|
198
|
+
line = lines[i]
|
|
199
|
+
func_match = re.match(r'^\s*def\s+(\w+)\s*\(', line)
|
|
200
|
+
if func_match:
|
|
201
|
+
func_name = func_match.group(1)
|
|
202
|
+
docstring = None
|
|
203
|
+
# Look ahead for docstring
|
|
204
|
+
j = i + 1
|
|
205
|
+
while j < len(lines) and j < i + 3:
|
|
206
|
+
if '"""' in lines[j] or "'''" in lines[j]:
|
|
207
|
+
docstring = lines[j].strip()
|
|
208
|
+
break
|
|
209
|
+
j += 1
|
|
210
|
+
results.append({
|
|
211
|
+
'name': func_name,
|
|
212
|
+
'line': i + 1,
|
|
213
|
+
'docstring': docstring
|
|
214
|
+
})
|
|
215
|
+
i += 1
|
|
216
|
+
return results
|
|
217
|
+
except:
|
|
218
|
+
return []
|
|
219
|
+
|
|
220
|
+
class DelegatedCodingAgent(SubAgent):
|
|
221
|
+
"""Delegates its task to the real standalone agent loop (runner_local.py --
|
|
222
|
+
the same one `tamfis-code agent`/`exec`/`ask` and the interactive REPL
|
|
223
|
+
drive), instead of a local heuristic.
|
|
224
|
+
|
|
225
|
+
Unlike CodeAnalyzer/TestGenerator/DocGenerator, this one can actually act
|
|
226
|
+
on an arbitrary objective via the model -- calling a provider directly
|
|
227
|
+
and executing tools locally, with its own workspace/session. It's never
|
|
228
|
+
selected by AgentManager's keyword-based `get_agent()` routing
|
|
229
|
+
(capabilities=[]) since it's only meant to be dispatched explicitly via
|
|
230
|
+
`execute_tasks`.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
def __init__(
|
|
234
|
+
self, *, manager, provider, model, console, workspace_root: str, session_id: int,
|
|
235
|
+
approval_policy: str = "ask", mode: str = "agent",
|
|
236
|
+
):
|
|
237
|
+
super().__init__(
|
|
238
|
+
name="delegated_coding_agent",
|
|
239
|
+
description="Delegates a sub-objective to the real standalone agent loop",
|
|
240
|
+
capabilities=[],
|
|
241
|
+
)
|
|
242
|
+
self._manager = manager
|
|
243
|
+
self._provider = provider
|
|
244
|
+
self._model = model
|
|
245
|
+
self._console = console
|
|
246
|
+
self._workspace_root = workspace_root
|
|
247
|
+
self._session_id = session_id
|
|
248
|
+
self._approval_policy = approval_policy
|
|
249
|
+
self._mode = mode
|
|
250
|
+
|
|
251
|
+
async def execute(self, task: AgentTask) -> Dict[str, Any]:
|
|
252
|
+
from .render import StreamRenderer
|
|
253
|
+
from .runner_local import run_local_agent_turn
|
|
254
|
+
|
|
255
|
+
renderer = StreamRenderer(self._console)
|
|
256
|
+
outcome = await run_local_agent_turn(
|
|
257
|
+
self._manager, self._provider, self._model, [{"role": "user", "content": task.description}],
|
|
258
|
+
self._console, renderer,
|
|
259
|
+
workspace_root=self._workspace_root, session_id=self._session_id,
|
|
260
|
+
approval_policy=self._approval_policy, interactive=False,
|
|
261
|
+
read_only=self._mode in {"chat", "audit", "plan"},
|
|
262
|
+
)
|
|
263
|
+
renderer.finish()
|
|
264
|
+
return {"status": outcome.status, "summary": outcome.summary, "error": outcome.error}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class AgentManager:
|
|
268
|
+
"""Manages subagents and task execution"""
|
|
269
|
+
|
|
270
|
+
def __init__(self):
|
|
271
|
+
self.agents: Dict[str, SubAgent] = {}
|
|
272
|
+
self.tasks: Dict[str, AgentTask] = {}
|
|
273
|
+
self._register_default_agents()
|
|
274
|
+
|
|
275
|
+
def _register_default_agents(self):
|
|
276
|
+
"""Register default agents"""
|
|
277
|
+
self.register(CodeAnalyzer())
|
|
278
|
+
self.register(TestGenerator())
|
|
279
|
+
self.register(DocGenerator())
|
|
280
|
+
|
|
281
|
+
def register(self, agent: SubAgent):
|
|
282
|
+
"""Register a subagent"""
|
|
283
|
+
self.agents[agent.name] = agent
|
|
284
|
+
|
|
285
|
+
def list_agents(self) -> List[Dict[str, Any]]:
|
|
286
|
+
"""List all registered agents"""
|
|
287
|
+
return [
|
|
288
|
+
{
|
|
289
|
+
"name": agent.name,
|
|
290
|
+
"description": agent.description,
|
|
291
|
+
"capabilities": agent.capabilities,
|
|
292
|
+
"status": "ready"
|
|
293
|
+
}
|
|
294
|
+
for agent in self.agents.values()
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
def get_agent(self, task_description: str) -> Optional[SubAgent]:
|
|
298
|
+
"""Find an agent that can handle the task"""
|
|
299
|
+
for agent in self.agents.values():
|
|
300
|
+
if agent.can_handle(task_description):
|
|
301
|
+
return agent
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
async def execute_task(self, description: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
|
305
|
+
"""Execute a task using the appropriate agent"""
|
|
306
|
+
import uuid
|
|
307
|
+
|
|
308
|
+
task = AgentTask(
|
|
309
|
+
id=str(uuid.uuid4())[:8],
|
|
310
|
+
description=description,
|
|
311
|
+
parameters=parameters,
|
|
312
|
+
status=AgentStatus.RUNNING
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
agent = self.get_agent(description)
|
|
316
|
+
if not agent:
|
|
317
|
+
task.status = AgentStatus.FAILED
|
|
318
|
+
task.error = "No agent can handle this task"
|
|
319
|
+
return {"error": task.error}
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
agent.current_task = task
|
|
323
|
+
result = await agent.execute(task)
|
|
324
|
+
task.result = result
|
|
325
|
+
task.status = AgentStatus.COMPLETED
|
|
326
|
+
return {"task_id": task.id, "agent": agent.name, "result": result}
|
|
327
|
+
except Exception as e:
|
|
328
|
+
task.status = AgentStatus.FAILED
|
|
329
|
+
task.error = str(e)
|
|
330
|
+
return {"error": str(e), "task_id": task.id}
|
|
331
|
+
|
|
332
|
+
async def execute_tasks(
|
|
333
|
+
self, descriptions: List[str], *,
|
|
334
|
+
manager, provider, model, console, workspace_root, approval_policy: str = "ask",
|
|
335
|
+
mode: str = "agent", max_concurrency: int = 1,
|
|
336
|
+
) -> List[Dict[str, Any]]:
|
|
337
|
+
"""Fan out N sub-objectives concurrently (bounded by max_concurrency),
|
|
338
|
+
each delegated to the standalone agent loop in its own local session.
|
|
339
|
+
|
|
340
|
+
Defaults to sequential (max_concurrency=1): whether concurrent tool
|
|
341
|
+
execution against the same workspace is safe in every case (two
|
|
342
|
+
sub-tasks editing overlapping files, concurrent state.json writers
|
|
343
|
+
from separate local sessions) hasn't been stress-tested -- raise the
|
|
344
|
+
cap only once you've validated that for your own workloads.
|
|
345
|
+
"""
|
|
346
|
+
from .workspace import resolve_local_workspace
|
|
347
|
+
|
|
348
|
+
semaphore = asyncio.Semaphore(max(1, max_concurrency))
|
|
349
|
+
|
|
350
|
+
async def run_one(description: str) -> Dict[str, Any]:
|
|
351
|
+
async with semaphore:
|
|
352
|
+
task = AgentTask(id=f"delegated_{uuid.uuid4().hex[:8]}", description=description, status=AgentStatus.RUNNING)
|
|
353
|
+
self.tasks[task.id] = task
|
|
354
|
+
try:
|
|
355
|
+
workspace = resolve_local_workspace(Path(workspace_root), discover=False)
|
|
356
|
+
agent = DelegatedCodingAgent(
|
|
357
|
+
manager=manager, provider=provider, model=model, console=console,
|
|
358
|
+
workspace_root=workspace.workspace_root, session_id=workspace.session_id,
|
|
359
|
+
approval_policy=approval_policy, mode=mode,
|
|
360
|
+
)
|
|
361
|
+
result = await agent.execute(task)
|
|
362
|
+
task.result = result
|
|
363
|
+
task.status = AgentStatus.COMPLETED if result.get("status") == "completed" else AgentStatus.FAILED
|
|
364
|
+
task.error = result.get("error")
|
|
365
|
+
except Exception as e:
|
|
366
|
+
result = {"error": str(e)}
|
|
367
|
+
task.status = AgentStatus.FAILED
|
|
368
|
+
task.error = str(e)
|
|
369
|
+
return {"task_id": task.id, "description": description, "status": task.status.value, "result": result}
|
|
370
|
+
|
|
371
|
+
return list(await asyncio.gather(*(run_one(description) for description in descriptions)))
|