devsquad 3.6.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 (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. skills/test/handler.py +78 -0
@@ -0,0 +1,513 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Worker - Agent Execution Framework
5
+
6
+ Each Worker is an independent Agent instance that executes tasks
7
+ and exchanges information with other Workers via Scratchpad.
8
+ """
9
+
10
+ import time
11
+ import sys
12
+ import os
13
+ import threading
14
+ from typing import List, Optional, Any, Dict
15
+
16
+ from .models import (
17
+ TaskDefinition,
18
+ WorkerResult,
19
+ TaskNotification,
20
+ ScratchpadEntry,
21
+ EntryType,
22
+ EntryStatus,
23
+ )
24
+ from .scratchpad import Scratchpad
25
+ from .usage_tracker import track_usage
26
+
27
+
28
+ class Worker:
29
+ """
30
+ Worker Agent Instance - Execution Unit for Multi-Role Collaboration
31
+
32
+ Each Worker represents a specific role (architect/tester/developer, etc.),
33
+ independently executes assigned tasks, and exchanges information with
34
+ other Workers via Scratchpad.
35
+
36
+ Core Capabilities:
37
+ - execute(): Execute TaskDefinition, produce WorkerResult
38
+ - write_finding/question/conflict(): Write different types of info to shared scratchpad
39
+ - send_notification(): Send cross-role messages to other Workers
40
+ - vote_on_proposal(): Participate in consensus voting
41
+
42
+ Lifecycle:
43
+ Create -> Receive Task -> Read Context -> Execute Work -> Write Results -> Send Notifications
44
+
45
+ Relationships with Other Components:
46
+ - Scratchpad: Shared data exchange medium (read/write)
47
+ - ConsensusEngine: Participate in consensus via vote_on_proposal
48
+ - Coordinator: Created and managed by Coordinator.spawn_workers()
49
+
50
+ Usage Example:
51
+ worker = Worker(
52
+ worker_id="arch-abc123",
53
+ role_id="architect",
54
+ role_prompt="You are a system architect...",
55
+ scratchpad=scratchpad,
56
+ )
57
+ result = worker.execute(task_definition)
58
+ """
59
+
60
+ def __init__(self, worker_id: str, role_id: str, role_prompt: str,
61
+ scratchpad: Scratchpad, llm_backend=None, stream: bool = False):
62
+ """
63
+ Initialize Worker instance.
64
+
65
+ Args:
66
+ worker_id: Unique worker identifier (format: "{role_id}-{hex}")
67
+ role_id: Role identifier (e.g., "architect", "tester", "solo-coder")
68
+ role_prompt: Role system prompt / instruction template
69
+ scratchpad: Associated shared scratchpad instance
70
+ llm_backend: LLM execution backend (None=MockBackend, returns assembled prompt)
71
+ """
72
+ self.worker_id = worker_id
73
+ self.role_id = role_id
74
+ self.role_prompt = role_prompt
75
+ self.scratchpad = scratchpad
76
+ self.llm_backend = llm_backend
77
+ self.stream = stream
78
+ self._notifications_outbox: List[TaskNotification] = []
79
+ self._notifications_lock = threading.Lock()
80
+ self._entries_written_count = 0
81
+ self._last_assembled_prompt = None
82
+
83
+ def execute(self, task: TaskDefinition) -> WorkerResult:
84
+ """
85
+ Execute assigned task.
86
+
87
+ Full execution flow:
88
+ 1. Build execution context (read relevant findings from Scratchpad)
89
+ 2. Call _do_work() to generate work output
90
+ 3. Write output as FINDING to Scratchpad
91
+ 4. Return WorkerResult with output and statistics
92
+
93
+ Args:
94
+ task: Task definition containing description, role ID, phase ID, etc.
95
+
96
+ Returns:
97
+ WorkerResult: Execution result containing:
98
+ - success: Whether successful
99
+ - output: Output content dictionary
100
+ - error: Error message (on failure)
101
+ - scratchpad_entries_written: Number of entries written
102
+ - notifications_sent: Number of notifications sent
103
+ - duration_seconds: Execution duration
104
+
105
+ Note:
106
+ Even if _do_work() returns an empty string, execute still returns
107
+ success=True, just with empty output.finding_summary. Only marks
108
+ as failed when an exception is raised.
109
+ """
110
+ start_time = time.time()
111
+ try:
112
+ context = self._build_execution_context(task)
113
+
114
+ finding = self._do_work(context)
115
+ if finding:
116
+ entry = ScratchpadEntry(
117
+ worker_id=self.worker_id,
118
+ role_id=self.role_id,
119
+ entry_type=EntryType.FINDING,
120
+ content=finding,
121
+ confidence=0.7,
122
+ tags=[task.task_id, task.stage_id or "", "auto"],
123
+ )
124
+ self.write_finding(entry)
125
+
126
+ output = {
127
+ "worker_id": self.worker_id,
128
+ "role_id": self.role_id,
129
+ "task_id": task.task_id,
130
+ "finding_summary": finding,
131
+ }
132
+
133
+ result = WorkerResult(
134
+ worker_id=self.worker_id,
135
+ task_id=task.task_id,
136
+ success=True,
137
+ output=output,
138
+ scratchpad_entries_written=self._entries_written_count,
139
+ notifications_sent=len(self._notifications_outbox),
140
+ duration_seconds=time.time() - start_time,
141
+ )
142
+ track_usage(f"worker.{self.role_id}.execute", success=True, metadata={
143
+ "task_id": task.task_id,
144
+ "duration": round(time.time() - start_time, 2)
145
+ })
146
+ return result
147
+ except Exception as e:
148
+ print(f" [Worker {self.worker_id}] Error: {e}", file=sys.stderr)
149
+ track_usage(f"worker.{self.role_id}.execute", success=False, metadata={
150
+ "task_id": task.task_id,
151
+ "error": str(e)[:100]
152
+ })
153
+ return WorkerResult(
154
+ worker_id=self.worker_id,
155
+ task_id=task.task_id,
156
+ success=False,
157
+ output={"worker_id": self.worker_id, "role_id": self.role_id,
158
+ "task_id": task.task_id, "error_detail": "Execution failed"},
159
+ error=str(e),
160
+ duration_seconds=time.time() - start_time,
161
+ )
162
+
163
+ def read_scratchpad(self, query: str = "",
164
+ since=None, limit: int = 20) -> List[ScratchpadEntry]:
165
+ """
166
+ Read relevant entries from shared scratchpad.
167
+
168
+ Args:
169
+ query: Keyword query (fuzzy match in content and tags)
170
+ since: Start time filter (only return entries after this time)
171
+ limit: Maximum number of entries to return
172
+
173
+ Returns:
174
+ List[ScratchpadEntry]: Matching entries, sorted by time descending
175
+ """
176
+ return self.scratchpad.read(
177
+ query=query, since=since, limit=limit,
178
+ )
179
+
180
+ def write_finding(self, finding: ScratchpadEntry) -> str:
181
+ """
182
+ Write a finding (FINDING type) to Scratchpad.
183
+
184
+ Automatically sets worker_id and role_id to current Worker identity,
185
+ and increments internal write counter.
186
+
187
+ Args:
188
+ finding: Finding entry to write (ScratchpadEntry instance)
189
+
190
+ Returns:
191
+ str: entry_id after writing
192
+ """
193
+ finding.worker_id = self.worker_id
194
+ finding.role_id = self.role_id
195
+ eid = self.scratchpad.write(finding)
196
+ self._entries_written_count += 1
197
+ return eid
198
+
199
+ def write_question(self, question: str, to_roles: List[str] = None,
200
+ tags: List[str] = None) -> str:
201
+ """
202
+ Write a question to Scratchpad and optionally notify other roles.
203
+
204
+ Creates a QUESTION type entry and writes to scratchpad. If to_roles
205
+ is specified, also generates TaskNotification for target role Workers.
206
+
207
+ Args:
208
+ question: Question content text
209
+ to_roles: Target role ID list (e.g., ["architect", "tester"]), empty means no notification
210
+ tags: Optional tag list
211
+
212
+ Returns:
213
+ str: entry_id after writing
214
+ """
215
+ entry = ScratchpadEntry(
216
+ worker_id=self.worker_id,
217
+ role_id=self.role_id,
218
+ entry_type=EntryType.QUESTION,
219
+ content=question,
220
+ confidence=0.5,
221
+ tags=tags or [],
222
+ )
223
+ eid = self.scratchpad.write(entry)
224
+ self._entries_written_count += 1
225
+
226
+ if to_roles:
227
+ notification = TaskNotification(
228
+ from_worker=self.worker_id,
229
+ to_workers=to_roles,
230
+ notification_type="question",
231
+ summary=question[:100],
232
+ details=question,
233
+ action_required="Please answer this question",
234
+ )
235
+ self.send_notification(notification)
236
+ return eid
237
+
238
+ def write_conflict(self, conflict: str, conflicting_entry_id: str,
239
+ reason: str = "") -> str:
240
+ """
241
+ Write a conflict record (CONFLICT type) to Scratchpad.
242
+
243
+ Called when contradiction with another Worker's output is detected.
244
+ Conflicts trigger subsequent Coordinator.resolve_conflicts() consensus flow.
245
+
246
+ Args:
247
+ conflict: Conflict description text
248
+ conflicting_entry_id: Conflicting peer entry ID
249
+ reason: Conflict reason explanation
250
+
251
+ Returns:
252
+ str: entry_id after writing
253
+ """
254
+ entry = ScratchpadEntry(
255
+ worker_id=self.worker_id,
256
+ role_id=self.role_id,
257
+ entry_type=EntryType.CONFLICT,
258
+ content=f"{conflict}\n\n[Conflict reason] {reason}",
259
+ confidence=0.8,
260
+ tags=["conflict", conflicting_entry_id],
261
+ )
262
+ eid = self.scratchpad.write(entry)
263
+ self._entries_written_count += 1
264
+ return eid
265
+
266
+ def send_notification(self, notification: TaskNotification):
267
+ """
268
+ Send cross-Worker notification.
269
+
270
+ Places notification in internal outbox, waiting for Coordinator
271
+ to collect via get_pending_notifications() and forward to target Workers.
272
+
273
+ Args:
274
+ notification: Notification object containing sender, recipient list, summary, etc.
275
+ """
276
+ with self._notifications_lock:
277
+ self._notifications_outbox.append(notification)
278
+
279
+ def get_pending_notifications(self) -> List[TaskNotification]:
280
+ """
281
+ Get and clear pending notification queue.
282
+
283
+ Called by Coordinator during collect_results(),
284
+ retrieves all accumulated notifications and clears internal buffer.
285
+
286
+ Returns:
287
+ List[TaskNotification]: Pending notification list (queue is empty after call)
288
+ """
289
+ with self._notifications_lock:
290
+ notifications = list(self._notifications_outbox)
291
+ self._notifications_outbox.clear()
292
+ return notifications
293
+
294
+ def get_last_prompt(self) -> Optional[Any]:
295
+ """
296
+ Get the most recent _do_work() prompt assembly result.
297
+
298
+ Generated by PromptAssembler, contains metadata such as
299
+ complexity/variant/token estimate. Can be used for Skillify
300
+ feedback loop: feed successful prompt variants back to the system.
301
+
302
+ Returns:
303
+ Optional[AssembledPrompt]: Most recent assembly result, None if never executed
304
+ """
305
+ return self._last_assembled_prompt
306
+
307
+ def vote_on_proposal(self, proposal_id: str, decision: bool,
308
+ reason: str = "", weight: float = None) -> Dict[str, Any]:
309
+ """
310
+ Vote on a consensus proposal.
311
+
312
+ Creates a Vote object and wraps it in standard return format.
313
+ Weight defaults from ROLE_WEIGHTS global config by role.
314
+
315
+ Args:
316
+ proposal_id: Consensus proposal ID
317
+ decision: Vote decision (True=approve, False=reject)
318
+ reason: Vote reason explanation
319
+ weight: Vote weight (None uses role default weight)
320
+
321
+ Returns:
322
+ Dict[str, Any]: Dictionary containing proposal_id and Vote object
323
+ - proposal_id: Proposal ID
324
+ - vote: Vote data object
325
+ """
326
+ from .models import Vote, ROLE_WEIGHTS
327
+ w = weight or ROLE_WEIGHTS.get(self.role_id, 1.0)
328
+ vote = Vote(
329
+ voter_id=self.worker_id,
330
+ voter_role=self.role_id,
331
+ decision=decision,
332
+ reason=reason,
333
+ weight=w,
334
+ )
335
+ return {"proposal_id": proposal_id, "vote": vote}
336
+
337
+ def _build_execution_context(self, task: TaskDefinition,
338
+ compression_level=None) -> Dict[str, Any]:
339
+ """
340
+ Build execution context (including prompt assembly).
341
+
342
+ Reads relevant findings from Scratchpad and performs dynamic prompt
343
+ trimming via PromptAssembler (based on task complexity and compression level).
344
+
345
+ Args:
346
+ task: Task definition
347
+ compression_level: ContextCompressor compression level (optional, affects prompt style)
348
+
349
+ Returns:
350
+ Dict[str, Any]: Execution context containing task/role_prompt/related_findings/
351
+ worker_id/compression_level
352
+ """
353
+ related = self.read_scratchpad(
354
+ query=task.description[:50], limit=10,
355
+ )
356
+ return {
357
+ "task": task,
358
+ "role_prompt": self.role_prompt,
359
+ "related_findings": [f.content for f in related[:8]],
360
+ "worker_id": self.worker_id,
361
+ "compression_level": compression_level,
362
+ }
363
+
364
+ def _do_work(self, context: Dict[str, Any]) -> str:
365
+ """
366
+ Execute core work - dynamically assemble prompt via PromptAssembler, then execute via LLMBackend.
367
+
368
+ Assembly flow:
369
+ 1. Extract task description, related findings, compression level from context
370
+ 2. Auto-detect complexity via PromptAssembler.detect_complexity()
371
+ 3. Select template variant by complexity (compact/standard/enhanced)
372
+ 4. Apply compression level override (if any)
373
+ 5. Output final work instruction
374
+ 6. If LLMBackend is configured, call LLM; otherwise return assembled instruction
375
+
376
+ Args:
377
+ context: Execution context built by _build_execution_context()
378
+
379
+ Returns:
380
+ str: LLM response text (with backend) or assembled work instruction text (without backend)
381
+ """
382
+ from .prompt_assembler import PromptAssembler
383
+ from .llm_backend import MockBackend
384
+
385
+ task = context["task"]
386
+ assembler = PromptAssembler(role_id=self.role_id,
387
+ base_prompt=context["role_prompt"])
388
+
389
+ result = assembler.assemble(
390
+ task_description=task.description,
391
+ related_findings=context.get("related_findings", []),
392
+ task_id=task.task_id,
393
+ compression_level=context.get("compression_level"),
394
+ )
395
+
396
+ self._last_assembled_prompt = result
397
+
398
+ backend = self.llm_backend or MockBackend()
399
+ if isinstance(backend, MockBackend):
400
+ from .models import ROLE_REGISTRY
401
+ rdef = ROLE_REGISTRY.get(self.role_id)
402
+ role_name = rdef.name if rdef else self.role_id
403
+ return backend.generate(
404
+ result.instruction,
405
+ role_name=role_name,
406
+ task_description=task.description,
407
+ )
408
+
409
+ from .models import ROLE_REGISTRY as _RR
410
+ _rdef = _RR.get(self.role_id)
411
+ _rname = _rdef.name if _rdef else self.role_id
412
+
413
+ try:
414
+ from .llm_cache import get_llm_cache
415
+ cache = get_llm_cache()
416
+ cached = cache.get(result.instruction, "backend", getattr(backend, 'model', 'unknown'))
417
+ if cached:
418
+ print(f" [{_rname}] Cache hit.", file=sys.stderr)
419
+ return cached
420
+ except Exception:
421
+ cache = None
422
+
423
+ print(f" [{_rname}] Calling LLM backend...", file=sys.stderr)
424
+ try:
425
+ if self.stream and hasattr(backend, 'generate_stream'):
426
+ print(f" [{_rname}] Streaming...", file=sys.stderr)
427
+ chunks = []
428
+ for chunk in backend.generate_stream(result.instruction):
429
+ print(chunk, end="", flush=True)
430
+ chunks.append(chunk)
431
+ print()
432
+ response = "".join(chunks)
433
+ else:
434
+ response = backend.generate(result.instruction)
435
+ print(f" [{_rname}] Response received.", file=sys.stderr)
436
+
437
+ if cache and response:
438
+ try:
439
+ cache.set(result.instruction, response, "backend", getattr(backend, 'model', 'unknown'))
440
+ except Exception:
441
+ pass
442
+
443
+ return response
444
+ except Exception as e:
445
+ print(f" [{_rname}] LLM call failed: {e}", file=sys.stderr)
446
+ raise
447
+
448
+
449
+ class WorkerFactory:
450
+ """
451
+ Factory class - batch creation of Worker instances.
452
+
453
+ Provides create() and create_batch() creation methods,
454
+ encapsulating Worker instantiation details so callers
455
+ don't need to know internal construction logic.
456
+
457
+ Usage Example:
458
+ single = WorkerFactory.create("w-1", "architect", prompt, scratchpad)
459
+ batch = WorkerFactory.create_batch([
460
+ {"worker_id": "w-1", "role_id": "architect", ...},
461
+ {"worker_id": "w-2", "role_id": "tester", ...},
462
+ ], scratchpad)
463
+ """
464
+
465
+ @staticmethod
466
+ def create(worker_id: str, role_id: str, role_prompt: str,
467
+ scratchpad: Scratchpad, llm_backend=None, stream: bool = False) -> Worker:
468
+ """
469
+ Create a single Worker instance.
470
+
471
+ Args:
472
+ worker_id: Unique worker identifier
473
+ role_id: Role identifier
474
+ role_prompt: Role prompt
475
+ scratchpad: Shared scratchpad instance
476
+ llm_backend: LLM execution backend
477
+ stream: Whether to enable streaming output
478
+
479
+ Returns:
480
+ Worker: Newly created Worker instance
481
+ """
482
+ return Worker(worker_id, role_id, role_prompt, scratchpad, llm_backend, stream=stream)
483
+
484
+ @staticmethod
485
+ def create_batch(workers_config: List[Dict[str, str]],
486
+ scratchpad: Scratchpad, llm_backend=None) -> List[Worker]:
487
+ """
488
+ Batch create Worker instances.
489
+
490
+ Iterates through config list, creating a Worker for each entry.
491
+ worker_id is auto-generated if not provided (format: "w-{index}").
492
+
493
+ Args:
494
+ workers_config: Worker config list, each entry contains:
495
+ - worker_id (optional): Worker ID
496
+ - role_id (required): Role identifier
497
+ - role_prompt (optional): Role prompt
498
+ scratchpad: Shared Scratchpad instance for all Workers
499
+
500
+ Returns:
501
+ List[Worker]: List of created Worker instances
502
+ """
503
+ workers = []
504
+ for cfg in workers_config:
505
+ w = WorkerFactory.create(
506
+ worker_id=cfg.get("worker_id", f"w-{len(workers)}"),
507
+ role_id=cfg["role_id"],
508
+ role_prompt=cfg.get("role_prompt", ""),
509
+ scratchpad=scratchpad,
510
+ llm_backend=llm_backend,
511
+ )
512
+ workers.append(w)
513
+ return workers