FlyingNunchucks commited on
Commit
7014e14
·
1 Parent(s): 3cf0a85

Add deterministic five-task mission runtime

Browse files
Files changed (1) hide show
  1. src/auction_coordination/runtime.py +591 -0
src/auction_coordination/runtime.py ADDED
@@ -0,0 +1,591 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic end-to-end mission runtime for Agent 11.
2
+
3
+ The runtime advances the synthetic diligence task graph and records protocol
4
+ traffic. It never chooses a worker. Peers decide whether to compete, the
5
+ auction protocol admits responses, and deterministic settlement selects each
6
+ winner.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from datetime import datetime, timedelta
13
+ from enum import Enum
14
+ from statistics import mean
15
+
16
+ from .auction import AuctionRoundProtocol
17
+ from .bidding import BidAction, LocalBidPolicy, PeerBidSettings
18
+ from .execution import DeterministicTaskExecutor, ExecutionError
19
+ from .models import FailureClass, TaskAnnouncement, TaskAward, TaskFailure, WorkProduct
20
+ from .reauction import BoundedReauctionPolicy, RecoveryAction
21
+ from .registry import PeerRegistry
22
+ from .scenario import TaskBlueprint
23
+ from .settlement import DeterministicSettlement, SettlementResult
24
+
25
+
26
+ class MissionEventType(str, Enum):
27
+ """Protocol-level traffic recorded by the deterministic mission runtime."""
28
+
29
+ TASK_ANNOUNCEMENT = "task_announcement"
30
+ REAUCTION_ANNOUNCEMENT = "reauction_announcement"
31
+ BID = "bid"
32
+ BID_ABSTENTION = "bid_abstention"
33
+ AUCTION_CLOSED = "auction_closed"
34
+ TASK_AWARD = "task_award"
35
+ TASK_RESULT = "task_result"
36
+ TASK_FAILURE = "task_failure"
37
+ ESCALATION = "escalation"
38
+
39
+
40
+ class TaskRunStatus(str, Enum):
41
+ COMPLETED = "completed"
42
+ ESCALATED = "escalated"
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class MissionEvent:
47
+ sequence: int
48
+ event_type: MissionEventType
49
+ task_id: str
50
+ auction_round: int
51
+ occurred_at: datetime
52
+ actor_id: str | None
53
+ accepted: bool | None
54
+ detail: str
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class TaskMetrics:
59
+ task_id: str
60
+ capability: str
61
+ status: TaskRunStatus
62
+ eligible_peers: int
63
+ bids: int
64
+ abstentions: int
65
+ rejected_responses: int
66
+ auction_rounds: int
67
+ reauction_count: int
68
+ no_valid_bid_rounds: int
69
+ execution_failures: int
70
+ messages: int
71
+ winning_peer: str | None
72
+ winning_score: float | None
73
+ second_place_score: float | None
74
+ winning_margin: float | None
75
+ winning_estimated_cost: float | None
76
+ execution_cost: float
77
+ allocation_efficiency: float | None
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class TaskRunResult:
82
+ blueprint: TaskBlueprint
83
+ status: TaskRunStatus
84
+ final_announcement: TaskAnnouncement
85
+ final_award: TaskAward | None
86
+ work_product: WorkProduct | None
87
+ failures: tuple[TaskFailure, ...]
88
+ metrics: TaskMetrics
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class MissionMetrics:
93
+ mission_success: bool
94
+ tasks_attempted: int
95
+ tasks_completed: int
96
+ tasks_reauctioned: int
97
+ escalated_tasks: int
98
+ failed_auctions: int
99
+ execution_failures: int
100
+ total_bids: int
101
+ total_abstentions: int
102
+ rejected_responses: int
103
+ total_messages: int
104
+ average_bids_per_task: float
105
+ average_messages_per_task: float
106
+ total_synthetic_execution_cost: float
107
+ allocation_efficiency: float | None
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class MissionResult:
112
+ task_results: tuple[TaskRunResult, ...]
113
+ work_products: tuple[WorkProduct, ...]
114
+ events: tuple[MissionEvent, ...]
115
+ metrics: MissionMetrics
116
+
117
+
118
+ class DeterministicMissionRuntime:
119
+ """Run a bounded five-task distributed-auction mission deterministically."""
120
+
121
+ def __init__(
122
+ self,
123
+ *,
124
+ registry: PeerRegistry,
125
+ bid_settings: dict[str, PeerBidSettings],
126
+ task_blueprints: tuple[TaskBlueprint, ...],
127
+ executor: DeterministicTaskExecutor | None = None,
128
+ failure_injections: frozenset[tuple[str, int]] = frozenset(),
129
+ ) -> None:
130
+ self._registry = registry
131
+ self._bid_settings = dict(bid_settings)
132
+ self._task_blueprints = task_blueprints
133
+ self._executor = executor or DeterministicTaskExecutor(registry)
134
+ self._failure_injections = failure_injections
135
+ self._bid_policy = LocalBidPolicy(registry)
136
+ self._settlement = DeterministicSettlement(registry)
137
+ self._reauction = BoundedReauctionPolicy()
138
+ self._validate_configuration()
139
+
140
+ def run(self, *, started_at: datetime) -> MissionResult:
141
+ """Execute the configured mission in deterministic dependency order."""
142
+
143
+ if started_at.tzinfo is None or started_at.utcoffset() is None:
144
+ raise ValueError("started_at must include timezone information")
145
+
146
+ sequence = 0
147
+ clock_offset = 0
148
+ events: list[MissionEvent] = []
149
+ task_results: list[TaskRunResult] = []
150
+ completed: dict[str, WorkProduct] = {}
151
+
152
+ def tick() -> datetime:
153
+ nonlocal clock_offset
154
+ value = started_at + timedelta(seconds=clock_offset)
155
+ clock_offset += 1
156
+ return value
157
+
158
+ def emit(
159
+ event_type: MissionEventType,
160
+ *,
161
+ task_id: str,
162
+ auction_round: int,
163
+ detail: str,
164
+ actor_id: str | None = None,
165
+ accepted: bool | None = None,
166
+ ) -> None:
167
+ nonlocal sequence
168
+ sequence += 1
169
+ events.append(
170
+ MissionEvent(
171
+ sequence=sequence,
172
+ event_type=event_type,
173
+ task_id=task_id,
174
+ auction_round=auction_round,
175
+ occurred_at=tick(),
176
+ actor_id=actor_id,
177
+ accepted=accepted,
178
+ detail=detail,
179
+ )
180
+ )
181
+
182
+ for blueprint in self._task_blueprints:
183
+ required_input_ids = tuple(
184
+ completed[task_id].work_product_id
185
+ for task_id in blueprint.dependency_task_ids
186
+ )
187
+ announcement = TaskAnnouncement(
188
+ task_id=blueprint.task_id,
189
+ auction_id=blueprint.auction_id,
190
+ auction_round=1,
191
+ requested_capability=blueprint.capability,
192
+ summary=blueprint.summary,
193
+ minimum_capability_score=blueprint.minimum_capability_score,
194
+ maximum_cost=blueprint.maximum_cost,
195
+ required_input_ids=required_input_ids,
196
+ max_auction_rounds=blueprint.max_auction_rounds,
197
+ opened_at=tick(),
198
+ )
199
+
200
+ result = self._run_task(
201
+ blueprint=blueprint,
202
+ initial_announcement=announcement,
203
+ available_inputs=tuple(completed.values()),
204
+ tick=tick,
205
+ emit=emit,
206
+ task_event_start=len(events),
207
+ events=events,
208
+ )
209
+ task_results.append(result)
210
+
211
+ if result.work_product is not None:
212
+ completed[blueprint.task_id] = result.work_product
213
+ if result.status is TaskRunStatus.ESCALATED:
214
+ break
215
+
216
+ metrics = self._mission_metrics(tuple(task_results))
217
+ work_products = tuple(
218
+ completed[blueprint.task_id]
219
+ for blueprint in self._task_blueprints
220
+ if blueprint.task_id in completed
221
+ )
222
+ return MissionResult(
223
+ task_results=tuple(task_results),
224
+ work_products=work_products,
225
+ events=tuple(events),
226
+ metrics=metrics,
227
+ )
228
+
229
+ def _run_task(
230
+ self,
231
+ *,
232
+ blueprint: TaskBlueprint,
233
+ initial_announcement: TaskAnnouncement,
234
+ available_inputs: tuple[WorkProduct, ...],
235
+ tick,
236
+ emit,
237
+ task_event_start: int,
238
+ events: list[MissionEvent],
239
+ ) -> TaskRunResult:
240
+ announcement = initial_announcement
241
+ protocol = AuctionRoundProtocol(announcement, self._registry)
242
+ failures: list[TaskFailure] = []
243
+ bids = 0
244
+ abstentions = 0
245
+ rejected = 0
246
+ rounds = 0
247
+ reauctions = 0
248
+ no_valid_bid_rounds = 0
249
+ execution_failures = 0
250
+ execution_cost = 0.0
251
+ efficiencies: list[float] = []
252
+ final_settlement: SettlementResult | None = None
253
+ final_award: TaskAward | None = None
254
+
255
+ while True:
256
+ rounds += 1
257
+ announce_type = (
258
+ MissionEventType.TASK_ANNOUNCEMENT
259
+ if announcement.auction_round == 1
260
+ else MissionEventType.REAUCTION_ANNOUNCEMENT
261
+ )
262
+ emit(
263
+ announce_type,
264
+ task_id=announcement.task_id,
265
+ auction_round=announcement.auction_round,
266
+ detail=(
267
+ f"opened auction round {announcement.auction_round} for "
268
+ f"{announcement.requested_capability}"
269
+ ),
270
+ )
271
+
272
+ for agent_id in sorted(self._bid_settings):
273
+ settings = self._bid_settings[agent_id]
274
+ decision = self._bid_policy.evaluate(
275
+ announcement=announcement,
276
+ settings=settings,
277
+ submitted_at=tick(),
278
+ )
279
+ if decision.action is BidAction.BID:
280
+ assert decision.bid is not None
281
+ receipt = protocol.submit_bid(decision.bid)
282
+ event_type = MissionEventType.BID
283
+ else:
284
+ assert decision.abstention is not None
285
+ receipt = protocol.submit_abstention(decision.abstention)
286
+ event_type = MissionEventType.BID_ABSTENTION
287
+
288
+ emit(
289
+ event_type,
290
+ task_id=announcement.task_id,
291
+ auction_round=announcement.auction_round,
292
+ actor_id=agent_id,
293
+ accepted=receipt.accepted,
294
+ detail=f"{receipt.code.value}: {receipt.detail}",
295
+ )
296
+ if receipt.accepted and event_type is MissionEventType.BID:
297
+ bids += 1
298
+ elif receipt.accepted:
299
+ abstentions += 1
300
+ else:
301
+ rejected += 1
302
+
303
+ protocol.close()
304
+ emit(
305
+ MissionEventType.AUCTION_CLOSED,
306
+ task_id=announcement.task_id,
307
+ auction_round=announcement.auction_round,
308
+ detail=(
309
+ f"closed with {len(protocol.accepted_bids())} admitted bids and "
310
+ f"{len(protocol.accepted_abstentions())} admitted abstentions"
311
+ ),
312
+ )
313
+
314
+ if not protocol.accepted_bids():
315
+ no_valid_bid_rounds += 1
316
+ emit(
317
+ MissionEventType.ESCALATION,
318
+ task_id=announcement.task_id,
319
+ auction_round=announcement.auction_round,
320
+ detail="no valid bids remained after protocol admission",
321
+ )
322
+ return self._task_result(
323
+ blueprint=blueprint,
324
+ status=TaskRunStatus.ESCALATED,
325
+ announcement=announcement,
326
+ final_award=None,
327
+ work_product=None,
328
+ failures=tuple(failures),
329
+ bids=bids,
330
+ abstentions=abstentions,
331
+ rejected=rejected,
332
+ rounds=rounds,
333
+ reauctions=reauctions,
334
+ no_valid_bid_rounds=no_valid_bid_rounds,
335
+ execution_failures=execution_failures,
336
+ execution_cost=execution_cost,
337
+ efficiencies=efficiencies,
338
+ final_settlement=None,
339
+ message_count=len(events) - task_event_start,
340
+ )
341
+
342
+ settlement = self._settlement.settle(protocol=protocol, awarded_at=tick())
343
+ final_settlement = settlement
344
+ final_award = settlement.award
345
+ winner = settlement.winning_scorecard
346
+ execution_cost += winner.estimated_cost
347
+ best_valid_utility = max(card.total_score for card in settlement.ranked_scorecards)
348
+ efficiencies.append(
349
+ 1.0 if best_valid_utility == 0.0 else winner.total_score / best_valid_utility
350
+ )
351
+ emit(
352
+ MissionEventType.TASK_AWARD,
353
+ task_id=announcement.task_id,
354
+ auction_round=announcement.auction_round,
355
+ actor_id=settlement.award.awarded_agent_id,
356
+ detail=(
357
+ f"awarded at score {settlement.award.winning_score:.4f} and synthetic "
358
+ f"cost {winner.estimated_cost:.2f}"
359
+ ),
360
+ )
361
+
362
+ try:
363
+ if (announcement.task_id, announcement.auction_round) in self._failure_injections:
364
+ raise ExecutionError(
365
+ "injected deterministic execution failure",
366
+ failure_class=FailureClass.EXECUTION_ERROR,
367
+ )
368
+ product = self._executor.execute(
369
+ announcement=announcement,
370
+ award=settlement.award,
371
+ completed_at=tick(),
372
+ available_inputs=available_inputs,
373
+ )
374
+ except ExecutionError as exc:
375
+ execution_failures += 1
376
+ failure = self._reauction.record_failure(
377
+ award=settlement.award,
378
+ failure_class=exc.failure_class,
379
+ detail=str(exc),
380
+ failed_at=tick(),
381
+ )
382
+ failures.append(failure)
383
+ emit(
384
+ MissionEventType.TASK_FAILURE,
385
+ task_id=announcement.task_id,
386
+ auction_round=announcement.auction_round,
387
+ actor_id=settlement.award.awarded_agent_id,
388
+ detail=f"{failure.failure_class.value}: {failure.detail}",
389
+ )
390
+ plan = self._reauction.plan(
391
+ announcement=announcement,
392
+ award=settlement.award,
393
+ failure=failure,
394
+ reopened_at=tick(),
395
+ )
396
+ if plan.action is RecoveryAction.ESCALATE:
397
+ emit(
398
+ MissionEventType.ESCALATION,
399
+ task_id=announcement.task_id,
400
+ auction_round=announcement.auction_round,
401
+ detail=plan.reason,
402
+ )
403
+ return self._task_result(
404
+ blueprint=blueprint,
405
+ status=TaskRunStatus.ESCALATED,
406
+ announcement=announcement,
407
+ final_award=final_award,
408
+ work_product=None,
409
+ failures=tuple(failures),
410
+ bids=bids,
411
+ abstentions=abstentions,
412
+ rejected=rejected,
413
+ rounds=rounds,
414
+ reauctions=reauctions,
415
+ no_valid_bid_rounds=no_valid_bid_rounds,
416
+ execution_failures=execution_failures,
417
+ execution_cost=execution_cost,
418
+ efficiencies=efficiencies,
419
+ final_settlement=final_settlement,
420
+ message_count=len(events) - task_event_start,
421
+ )
422
+
423
+ reauctions += 1
424
+ protocol = self._reauction.open_reauction_round(
425
+ plan=plan,
426
+ registry=self._registry,
427
+ )
428
+ assert plan.next_announcement is not None
429
+ announcement = plan.next_announcement
430
+ continue
431
+
432
+ emit(
433
+ MissionEventType.TASK_RESULT,
434
+ task_id=announcement.task_id,
435
+ auction_round=announcement.auction_round,
436
+ actor_id=product.agent_id,
437
+ detail=f"validated work product {product.work_product_id}",
438
+ )
439
+ return self._task_result(
440
+ blueprint=blueprint,
441
+ status=TaskRunStatus.COMPLETED,
442
+ announcement=announcement,
443
+ final_award=final_award,
444
+ work_product=product,
445
+ failures=tuple(failures),
446
+ bids=bids,
447
+ abstentions=abstentions,
448
+ rejected=rejected,
449
+ rounds=rounds,
450
+ reauctions=reauctions,
451
+ no_valid_bid_rounds=no_valid_bid_rounds,
452
+ execution_failures=execution_failures,
453
+ execution_cost=execution_cost,
454
+ efficiencies=efficiencies,
455
+ final_settlement=final_settlement,
456
+ message_count=len(events) - task_event_start,
457
+ )
458
+
459
+ def _task_result(
460
+ self,
461
+ *,
462
+ blueprint: TaskBlueprint,
463
+ status: TaskRunStatus,
464
+ announcement: TaskAnnouncement,
465
+ final_award: TaskAward | None,
466
+ work_product: WorkProduct | None,
467
+ failures: tuple[TaskFailure, ...],
468
+ bids: int,
469
+ abstentions: int,
470
+ rejected: int,
471
+ rounds: int,
472
+ reauctions: int,
473
+ no_valid_bid_rounds: int,
474
+ execution_failures: int,
475
+ execution_cost: float,
476
+ efficiencies: list[float],
477
+ final_settlement: SettlementResult | None,
478
+ message_count: int,
479
+ ) -> TaskRunResult:
480
+ winning_score = None
481
+ second_place_score = None
482
+ winning_margin = None
483
+ winning_cost = None
484
+ winning_peer = None
485
+ if final_settlement is not None:
486
+ winning_score = final_settlement.award.winning_score
487
+ second_place_score = final_settlement.second_place_score
488
+ winning_peer = final_settlement.award.awarded_agent_id
489
+ winning_cost = final_settlement.winning_scorecard.estimated_cost
490
+ if second_place_score is not None:
491
+ winning_margin = winning_score - second_place_score
492
+
493
+ metrics = TaskMetrics(
494
+ task_id=blueprint.task_id,
495
+ capability=blueprint.capability,
496
+ status=status,
497
+ eligible_peers=len(
498
+ self._registry.eligible_profiles(
499
+ blueprint.capability,
500
+ blueprint.minimum_capability_score,
501
+ )
502
+ ),
503
+ bids=bids,
504
+ abstentions=abstentions,
505
+ rejected_responses=rejected,
506
+ auction_rounds=rounds,
507
+ reauction_count=reauctions,
508
+ no_valid_bid_rounds=no_valid_bid_rounds,
509
+ execution_failures=execution_failures,
510
+ messages=message_count,
511
+ winning_peer=winning_peer,
512
+ winning_score=winning_score,
513
+ second_place_score=second_place_score,
514
+ winning_margin=winning_margin,
515
+ winning_estimated_cost=winning_cost,
516
+ execution_cost=round(execution_cost, 12),
517
+ allocation_efficiency=(
518
+ round(mean(efficiencies), 12) if efficiencies else None
519
+ ),
520
+ )
521
+ return TaskRunResult(
522
+ blueprint=blueprint,
523
+ status=status,
524
+ final_announcement=announcement,
525
+ final_award=final_award,
526
+ work_product=work_product,
527
+ failures=failures,
528
+ metrics=metrics,
529
+ )
530
+
531
+ def _mission_metrics(self, results: tuple[TaskRunResult, ...]) -> MissionMetrics:
532
+ attempted = len(results)
533
+ completed = sum(result.status is TaskRunStatus.COMPLETED for result in results)
534
+ efficiencies = [
535
+ result.metrics.allocation_efficiency
536
+ for result in results
537
+ if result.metrics.allocation_efficiency is not None
538
+ ]
539
+ mission_success = attempted == len(self._task_blueprints) and completed == attempted
540
+ return MissionMetrics(
541
+ mission_success=mission_success,
542
+ tasks_attempted=attempted,
543
+ tasks_completed=completed,
544
+ tasks_reauctioned=sum(result.metrics.reauction_count > 0 for result in results),
545
+ escalated_tasks=sum(result.status is TaskRunStatus.ESCALATED for result in results),
546
+ failed_auctions=sum(result.metrics.no_valid_bid_rounds for result in results),
547
+ execution_failures=sum(result.metrics.execution_failures for result in results),
548
+ total_bids=sum(result.metrics.bids for result in results),
549
+ total_abstentions=sum(result.metrics.abstentions for result in results),
550
+ rejected_responses=sum(result.metrics.rejected_responses for result in results),
551
+ total_messages=sum(result.metrics.messages for result in results),
552
+ average_bids_per_task=(
553
+ round(sum(result.metrics.bids for result in results) / attempted, 12)
554
+ if attempted
555
+ else 0.0
556
+ ),
557
+ average_messages_per_task=(
558
+ round(sum(result.metrics.messages for result in results) / attempted, 12)
559
+ if attempted
560
+ else 0.0
561
+ ),
562
+ total_synthetic_execution_cost=round(
563
+ sum(result.metrics.execution_cost for result in results), 12
564
+ ),
565
+ allocation_efficiency=(round(mean(efficiencies), 12) if efficiencies else None),
566
+ )
567
+
568
+ def _validate_configuration(self) -> None:
569
+ if not self._task_blueprints:
570
+ raise ValueError("mission requires at least one task blueprint")
571
+ task_ids = [blueprint.task_id for blueprint in self._task_blueprints]
572
+ if len(task_ids) != len(set(task_ids)):
573
+ raise ValueError("task blueprint IDs must be unique")
574
+
575
+ seen: set[str] = set()
576
+ for blueprint in self._task_blueprints:
577
+ missing = [
578
+ dependency
579
+ for dependency in blueprint.dependency_task_ids
580
+ if dependency not in seen
581
+ ]
582
+ if missing:
583
+ raise ValueError(
584
+ f"task {blueprint.task_id} depends on unavailable prior tasks: "
585
+ f"{', '.join(missing)}"
586
+ )
587
+ seen.add(blueprint.task_id)
588
+
589
+ registered_ids = {profile.agent_id for profile in self._registry.all_profiles()}
590
+ if set(self._bid_settings) != registered_ids:
591
+ raise ValueError("bid settings must cover exactly the registered peer identities")