flock-core 0.5.8__py3-none-any.whl → 0.5.10__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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (52) hide show
  1. flock/agent.py +149 -62
  2. flock/api/themes.py +6 -2
  3. flock/artifact_collector.py +6 -3
  4. flock/batch_accumulator.py +3 -1
  5. flock/cli.py +3 -1
  6. flock/components.py +45 -56
  7. flock/context_provider.py +531 -0
  8. flock/correlation_engine.py +8 -4
  9. flock/dashboard/collector.py +48 -29
  10. flock/dashboard/events.py +10 -4
  11. flock/dashboard/launcher.py +3 -1
  12. flock/dashboard/models/graph.py +9 -3
  13. flock/dashboard/service.py +143 -72
  14. flock/dashboard/websocket.py +17 -4
  15. flock/engines/dspy_engine.py +174 -98
  16. flock/engines/examples/simple_batch_engine.py +9 -3
  17. flock/examples.py +6 -2
  18. flock/frontend/src/services/indexeddb.test.ts +4 -4
  19. flock/frontend/src/services/indexeddb.ts +1 -1
  20. flock/helper/cli_helper.py +14 -1
  21. flock/logging/auto_trace.py +6 -1
  22. flock/logging/formatters/enum_builder.py +3 -1
  23. flock/logging/formatters/theme_builder.py +32 -17
  24. flock/logging/formatters/themed_formatter.py +38 -22
  25. flock/logging/logging.py +21 -7
  26. flock/logging/telemetry.py +9 -3
  27. flock/logging/telemetry_exporter/duckdb_exporter.py +27 -25
  28. flock/logging/trace_and_logged.py +14 -5
  29. flock/mcp/__init__.py +3 -6
  30. flock/mcp/client.py +49 -19
  31. flock/mcp/config.py +12 -6
  32. flock/mcp/manager.py +6 -2
  33. flock/mcp/servers/sse/flock_sse_server.py +9 -3
  34. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +6 -2
  35. flock/mcp/tool.py +18 -6
  36. flock/mcp/types/handlers.py +3 -1
  37. flock/mcp/types/types.py +9 -3
  38. flock/orchestrator.py +204 -50
  39. flock/orchestrator_component.py +15 -5
  40. flock/patches/dspy_streaming_patch.py +12 -4
  41. flock/registry.py +9 -3
  42. flock/runtime.py +69 -18
  43. flock/service.py +19 -6
  44. flock/store.py +29 -10
  45. flock/subscription.py +6 -4
  46. flock/utilities.py +41 -13
  47. flock/utility/output_utility_component.py +31 -11
  48. {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/METADATA +134 -4
  49. {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/RECORD +52 -51
  50. {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/WHEEL +0 -0
  51. {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/entry_points.txt +0 -0
  52. {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/licenses/LICENSE +0 -0
@@ -54,7 +54,9 @@ class RunRecord:
54
54
  error_message: str | None = None
55
55
 
56
56
  def to_graph_run(self) -> GraphRun:
57
- status = self.status if self.status in {"active", "completed", "error"} else "active"
57
+ status = (
58
+ self.status if self.status in {"active", "completed", "error"} else "active"
59
+ )
58
60
  return GraphRun(
59
61
  run_id=self.run_id,
60
62
  agent_name=self.agent_name,
@@ -102,7 +104,10 @@ class DashboardEventCollector(AgentComponent):
102
104
 
103
105
  # Use PrivateAttr for non-Pydantic fields (AgentComponent extends BaseModel)
104
106
  _events: deque[
105
- AgentActivatedEvent | MessagePublishedEvent | AgentCompletedEvent | AgentErrorEvent
107
+ AgentActivatedEvent
108
+ | MessagePublishedEvent
109
+ | AgentCompletedEvent
110
+ | AgentErrorEvent
106
111
  ] = PrivateAttr(default=None)
107
112
 
108
113
  # Track run start times for duration calculation
@@ -114,7 +119,9 @@ class DashboardEventCollector(AgentComponent):
114
119
  # Graph assembly helpers
115
120
  _graph_lock: asyncio.Lock = PrivateAttr(default_factory=asyncio.Lock)
116
121
  _run_registry: dict[str, RunRecord] = PrivateAttr(default_factory=dict)
117
- _artifact_consumers: dict[str, set[str]] = PrivateAttr(default_factory=lambda: defaultdict(set))
122
+ _artifact_consumers: dict[str, set[str]] = PrivateAttr(
123
+ default_factory=lambda: defaultdict(set)
124
+ )
118
125
  _agent_status: dict[str, str] = PrivateAttr(default_factory=dict)
119
126
  _agent_snapshots: dict[str, AgentSnapshot] = PrivateAttr(default_factory=dict)
120
127
 
@@ -185,13 +192,15 @@ class DashboardEventCollector(AgentComponent):
185
192
  await self._update_agent_snapshot_locked(agent)
186
193
 
187
194
  # Build subscription info from agent's subscriptions
188
- subscription_info = SubscriptionInfo(from_agents=[], channels=[], mode="both")
195
+ subscription_info = SubscriptionInfo(from_agents=[], tags=[], mode="both")
189
196
 
190
197
  if agent.subscriptions:
191
198
  # Get first subscription's config (agents typically have one)
192
199
  sub = agent.subscriptions[0]
193
- subscription_info.from_agents = list(sub.from_agents) if sub.from_agents else []
194
- subscription_info.channels = list(sub.channels) if sub.channels else []
200
+ subscription_info.from_agents = (
201
+ list(sub.from_agents) if sub.from_agents else []
202
+ )
203
+ subscription_info.tags = list(sub.tags) if sub.tags else []
195
204
  subscription_info.mode = sub.mode
196
205
 
197
206
  # Create and store event
@@ -210,7 +219,9 @@ class DashboardEventCollector(AgentComponent):
210
219
  )
211
220
 
212
221
  self._events.append(event)
213
- logger.info(f"Agent activated: {agent.name} (correlation_id={event.correlation_id})")
222
+ logger.info(
223
+ f"Agent activated: {agent.name} (correlation_id={event.correlation_id})"
224
+ )
214
225
 
215
226
  # Broadcast via WebSocket if manager is configured
216
227
  if self._websocket_manager:
@@ -220,7 +231,9 @@ class DashboardEventCollector(AgentComponent):
220
231
 
221
232
  return inputs
222
233
 
223
- async def on_post_publish(self, agent: "Agent", ctx: Context, artifact: "Artifact") -> None:
234
+ async def on_post_publish(
235
+ self, agent: "Agent", ctx: Context, artifact: "Artifact"
236
+ ) -> None:
224
237
  """Emit message_published event when artifact is published.
225
238
 
226
239
  Args:
@@ -391,7 +404,9 @@ class DashboardEventCollector(AgentComponent):
391
404
  }
392
405
  runs = [record.to_graph_run() for record in self._run_registry.values()]
393
406
  agent_status = dict(self._agent_status)
394
- return GraphState(consumptions=consumptions, runs=runs, agent_status=agent_status)
407
+ return GraphState(
408
+ consumptions=consumptions, runs=runs, agent_status=agent_status
409
+ )
395
410
 
396
411
  async def snapshot_agent_registry(self) -> dict[str, AgentSnapshot]:
397
412
  """Return a snapshot of all known agents (active and inactive)."""
@@ -456,21 +471,17 @@ class DashboardEventCollector(AgentComponent):
456
471
  async def _update_agent_snapshot_locked(self, agent: "Agent") -> None:
457
472
  now = datetime.now(UTC)
458
473
  description = agent.description or ""
459
- subscriptions = sorted(
460
- {
461
- type_name
462
- for subscription in getattr(agent, "subscriptions", [])
463
- for type_name in getattr(subscription, "type_names", [])
464
- }
465
- )
466
- output_types = sorted(
467
- {
468
- output.spec.type_name
469
- for output in getattr(agent, "outputs", [])
470
- if getattr(output, "spec", None) is not None
471
- and getattr(output.spec, "type_name", "")
472
- }
473
- )
474
+ subscriptions = sorted({
475
+ type_name
476
+ for subscription in getattr(agent, "subscriptions", [])
477
+ for type_name in getattr(subscription, "type_names", [])
478
+ })
479
+ output_types = sorted({
480
+ output.spec.type_name
481
+ for output in getattr(agent, "outputs", [])
482
+ if getattr(output, "spec", None) is not None
483
+ and getattr(output.spec, "type_name", "")
484
+ })
474
485
  labels = sorted(agent.labels)
475
486
 
476
487
  signature_payload = {
@@ -519,7 +530,9 @@ class DashboardEventCollector(AgentComponent):
519
530
  first_seen=snapshot.first_seen,
520
531
  last_seen=snapshot.last_seen,
521
532
  signature=snapshot.signature,
522
- logic_operations=[dict(op) for op in snapshot.logic_operations], # Phase 1.2
533
+ logic_operations=[
534
+ dict(op) for op in snapshot.logic_operations
535
+ ], # Phase 1.2
523
536
  )
524
537
 
525
538
  def _snapshot_to_record(self, snapshot: AgentSnapshot) -> AgentSnapshotRecord:
@@ -545,19 +558,25 @@ class DashboardEventCollector(AgentComponent):
545
558
  """
546
559
  # Get visibility kind from class name, stripping "Visibility" suffix
547
560
  class_name = type(visibility).__name__
548
- kind = class_name[: -len("Visibility")] if class_name.endswith("Visibility") else class_name
561
+ kind = class_name.removesuffix("Visibility")
549
562
 
550
563
  spec = VisibilitySpec(kind=kind)
551
564
 
552
565
  # Extract type-specific fields
553
566
  if kind == "Private":
554
- spec.agents = list(visibility.agents) if hasattr(visibility, "agents") else []
567
+ spec.agents = (
568
+ list(visibility.agents) if hasattr(visibility, "agents") else []
569
+ )
555
570
  elif kind == "Labelled":
556
571
  spec.required_labels = (
557
- list(visibility.required_labels) if hasattr(visibility, "required_labels") else []
572
+ list(visibility.required_labels)
573
+ if hasattr(visibility, "required_labels")
574
+ else []
558
575
  )
559
576
  elif kind == "Tenant":
560
- spec.tenant_id = visibility.tenant_id if hasattr(visibility, "tenant_id") else None
577
+ spec.tenant_id = (
578
+ visibility.tenant_id if hasattr(visibility, "tenant_id") else None
579
+ )
561
580
 
562
581
  return spec
563
582
 
flock/dashboard/events.py CHANGED
@@ -14,7 +14,7 @@ class SubscriptionInfo(BaseModel):
14
14
  """Subscription configuration for an agent."""
15
15
 
16
16
  from_agents: list[str] = Field(default_factory=list)
17
- channels: list[str] = Field(default_factory=list)
17
+ tags: list[str] = Field(default_factory=list)
18
18
  mode: str = "both" # "both" | "events" | "direct"
19
19
 
20
20
 
@@ -123,8 +123,12 @@ class StreamingOutputEvent(BaseModel):
123
123
  is_final: bool = False # True when agent completes this output stream
124
124
 
125
125
  # Artifact tracking (Phase 6: for message streaming preview)
126
- artifact_id: str | None = None # Pre-generated artifact ID for streaming message nodes
127
- artifact_type: str | None = None # Artifact type name (e.g., "__main__.BookOutline")
126
+ artifact_id: str | None = (
127
+ None # Pre-generated artifact ID for streaming message nodes
128
+ )
129
+ artifact_type: str | None = (
130
+ None # Artifact type name (e.g., "__main__.BookOutline")
131
+ )
128
132
 
129
133
 
130
134
  class AgentCompletedEvent(BaseModel):
@@ -149,7 +153,9 @@ class AgentCompletedEvent(BaseModel):
149
153
  artifacts_produced: list[str] = Field(default_factory=list) # [artifact_id]
150
154
 
151
155
  # Metrics and state
152
- metrics: dict[str, Any] = Field(default_factory=dict) # {"tokens_used": 1234, "cost": 0.05}
156
+ metrics: dict[str, Any] = Field(
157
+ default_factory=dict
158
+ ) # {"tokens_used": 1234, "cost": 0.05}
153
159
  final_state: dict[str, Any] = Field(default_factory=dict) # Context.state snapshot
154
160
 
155
161
 
@@ -94,7 +94,9 @@ class DashboardLauncher:
94
94
 
95
95
  def _start_dev_server(self) -> None:
96
96
  """Start npm dev server for hot-reload development."""
97
- print(f"[Dashboard] Starting dev server (DASHBOARD_DEV=1) on port {self.port}...")
97
+ print(
98
+ f"[Dashboard] Starting dev server (DASHBOARD_DEV=1) on port {self.port}..."
99
+ )
98
100
 
99
101
  try:
100
102
  self._npm_process = subprocess.Popen(
@@ -104,7 +104,9 @@ class GraphStatistics(BaseModel):
104
104
  consumed_by_agent: dict[str, GraphAgentMetrics] = Field(
105
105
  default_factory=dict, alias="consumedByAgent"
106
106
  )
107
- artifact_summary: dict[str, Any] = Field(default_factory=dict, alias="artifactSummary")
107
+ artifact_summary: dict[str, Any] = Field(
108
+ default_factory=dict, alias="artifactSummary"
109
+ )
108
110
 
109
111
 
110
112
  class GraphArtifact(BaseModel):
@@ -128,8 +130,12 @@ class GraphRun(BaseModel):
128
130
  agent_name: str = Field(alias="agentName")
129
131
  correlation_id: str | None = Field(default=None, alias="correlationId")
130
132
  status: Literal["active", "completed", "error"] = "active"
131
- consumed_artifacts: list[str] = Field(default_factory=list, alias="consumedArtifacts")
132
- produced_artifacts: list[str] = Field(default_factory=list, alias="producedArtifacts")
133
+ consumed_artifacts: list[str] = Field(
134
+ default_factory=list, alias="consumedArtifacts"
135
+ )
136
+ produced_artifacts: list[str] = Field(
137
+ default_factory=list, alias="producedArtifacts"
138
+ )
133
139
  duration_ms: float | None = Field(default=None, alias="durationMs")
134
140
  started_at: datetime | None = Field(default=None, alias="startedAt")
135
141
  completed_at: datetime | None = Field(default=None, alias="completedAt")
@@ -143,7 +143,9 @@ class DashboardHTTPService(BlackboardHTTPService):
143
143
  return await self.graph_assembler.build_snapshot(request)
144
144
 
145
145
  dashboard_dir = Path(__file__).parent
146
- frontend_root = dashboard_dir.parent / ("frontend_v2" if self.use_v2 else "frontend")
146
+ frontend_root = dashboard_dir.parent / (
147
+ "frontend_v2" if self.use_v2 else "frontend"
148
+ )
147
149
  static_dir = dashboard_dir / ("static_v2" if self.use_v2 else "static")
148
150
 
149
151
  possible_dirs = [
@@ -244,14 +246,18 @@ class DashboardHTTPService(BlackboardHTTPService):
244
246
  # NEW Phase 1.2: Logic operations configuration
245
247
  logic_operations = []
246
248
  for idx, subscription in enumerate(agent.subscriptions):
247
- logic_config = _build_logic_config(agent, subscription, idx, orchestrator)
249
+ logic_config = _build_logic_config(
250
+ agent, subscription, idx, orchestrator
251
+ )
248
252
  if logic_config: # Only include if has join/batch
249
253
  logic_operations.append(logic_config)
250
254
 
251
255
  agent_data = {
252
256
  "name": agent.name,
253
257
  "description": agent.description or "",
254
- "status": _compute_agent_status(agent, orchestrator), # NEW: Dynamic status
258
+ "status": _compute_agent_status(
259
+ agent, orchestrator
260
+ ), # NEW: Dynamic status
255
261
  "subscriptions": consumed_types,
256
262
  "output_types": produced_types,
257
263
  }
@@ -314,7 +320,9 @@ class DashboardHTTPService(BlackboardHTTPService):
314
320
  try:
315
321
  instance = model_class(**content)
316
322
  except ValidationError as e:
317
- raise HTTPException(status_code=422, detail=f"Validation error: {e!s}")
323
+ raise HTTPException(
324
+ status_code=422, detail=f"Validation error: {e!s}"
325
+ )
318
326
 
319
327
  # Generate correlation ID
320
328
  correlation_id = str(uuid4())
@@ -384,13 +392,17 @@ class DashboardHTTPService(BlackboardHTTPService):
384
392
  # Get agent from orchestrator
385
393
  agent = orchestrator.get_agent(agent_name)
386
394
  except KeyError:
387
- raise HTTPException(status_code=404, detail=f"Agent not found: {agent_name}")
395
+ raise HTTPException(
396
+ status_code=404, detail=f"Agent not found: {agent_name}"
397
+ )
388
398
 
389
399
  try:
390
400
  # Parse input type and create instance
391
401
  input_type = input_data.get("type")
392
402
  if not input_type:
393
- raise HTTPException(status_code=400, detail="input.type is required")
403
+ raise HTTPException(
404
+ status_code=400, detail="input.type is required"
405
+ )
394
406
 
395
407
  # Resolve type from registry
396
408
  model_class = type_registry.resolve(input_type)
@@ -402,7 +414,9 @@ class DashboardHTTPService(BlackboardHTTPService):
402
414
  try:
403
415
  instance = model_class(**payload)
404
416
  except ValidationError as e:
405
- raise HTTPException(status_code=422, detail=f"Validation error: {e!s}")
417
+ raise HTTPException(
418
+ status_code=422, detail=f"Validation error: {e!s}"
419
+ )
406
420
 
407
421
  # Invoke agent
408
422
  outputs = await orchestrator.invoke(agent, instance)
@@ -426,7 +440,9 @@ class DashboardHTTPService(BlackboardHTTPService):
426
440
  except HTTPException:
427
441
  raise
428
442
  except KeyError:
429
- raise HTTPException(status_code=422, detail=f"Unknown type: {input_type}")
443
+ raise HTTPException(
444
+ status_code=422, detail=f"Unknown type: {input_type}"
445
+ )
430
446
  except Exception as e:
431
447
  logger.exception(f"Error invoking agent: {e}")
432
448
  raise HTTPException(status_code=500, detail=str(e))
@@ -438,7 +454,9 @@ class DashboardHTTPService(BlackboardHTTPService):
438
454
  Returns:
439
455
  501 Not Implemented
440
456
  """
441
- raise HTTPException(status_code=501, detail="Pause functionality coming in Phase 12")
457
+ raise HTTPException(
458
+ status_code=501, detail="Pause functionality coming in Phase 12"
459
+ )
442
460
 
443
461
  @app.post("/api/control/resume")
444
462
  async def resume_orchestrator() -> dict[str, Any]:
@@ -447,7 +465,9 @@ class DashboardHTTPService(BlackboardHTTPService):
447
465
  Returns:
448
466
  501 Not Implemented
449
467
  """
450
- raise HTTPException(status_code=501, detail="Resume functionality coming in Phase 12")
468
+ raise HTTPException(
469
+ status_code=501, detail="Resume functionality coming in Phase 12"
470
+ )
451
471
 
452
472
  @app.get("/api/traces")
453
473
  async def get_traces() -> list[dict[str, Any]]:
@@ -516,10 +536,14 @@ class DashboardHTTPService(BlackboardHTTPService):
516
536
  "status_code": row[10], # status_code
517
537
  "description": row[11], # status_description
518
538
  },
519
- "attributes": json.loads(row[12]) if row[12] else {}, # attributes
539
+ "attributes": json.loads(row[12])
540
+ if row[12]
541
+ else {}, # attributes
520
542
  "events": json.loads(row[13]) if row[13] else [], # events
521
543
  "links": json.loads(row[14]) if row[14] else [], # links
522
- "resource": json.loads(row[15]) if row[15] else {}, # resource
544
+ "resource": json.loads(row[15])
545
+ if row[15]
546
+ else {}, # resource
523
547
  }
524
548
 
525
549
  # Add parent_id if exists
@@ -619,10 +643,22 @@ class DashboardHTTPService(BlackboardHTTPService):
619
643
  # Security: Only allow SELECT queries
620
644
  query_upper = query.upper().strip()
621
645
  if not query_upper.startswith("SELECT"):
622
- return {"error": "Only SELECT queries are allowed", "results": [], "columns": []}
646
+ return {
647
+ "error": "Only SELECT queries are allowed",
648
+ "results": [],
649
+ "columns": [],
650
+ }
623
651
 
624
652
  # Check for dangerous keywords
625
- dangerous = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE", "TRUNCATE"]
653
+ dangerous = [
654
+ "DROP",
655
+ "DELETE",
656
+ "INSERT",
657
+ "UPDATE",
658
+ "ALTER",
659
+ "CREATE",
660
+ "TRUNCATE",
661
+ ]
626
662
  if any(keyword in query_upper for keyword in dangerous):
627
663
  return {
628
664
  "error": "Query contains forbidden operations",
@@ -632,12 +668,20 @@ class DashboardHTTPService(BlackboardHTTPService):
632
668
 
633
669
  db_path = Path(".flock/traces.duckdb")
634
670
  if not db_path.exists():
635
- return {"error": "Trace database not found", "results": [], "columns": []}
671
+ return {
672
+ "error": "Trace database not found",
673
+ "results": [],
674
+ "columns": [],
675
+ }
636
676
 
637
677
  try:
638
678
  with duckdb.connect(str(db_path), read_only=True) as conn:
639
679
  result = conn.execute(query).fetchall()
640
- columns = [desc[0] for desc in conn.description] if conn.description else []
680
+ columns = (
681
+ [desc[0] for desc in conn.description]
682
+ if conn.description
683
+ else []
684
+ )
641
685
 
642
686
  # Convert to JSON-serializable format
643
687
  results = []
@@ -652,7 +696,11 @@ class DashboardHTTPService(BlackboardHTTPService):
652
696
  row_dict[col] = val
653
697
  results.append(row_dict)
654
698
 
655
- return {"results": results, "columns": columns, "row_count": len(results)}
699
+ return {
700
+ "results": results,
701
+ "columns": columns,
702
+ "row_count": len(results),
703
+ }
656
704
  except Exception as e:
657
705
  logger.exception(f"DuckDB query error: {e}")
658
706
  return {"error": str(e), "results": [], "columns": []}
@@ -691,7 +739,9 @@ class DashboardHTTPService(BlackboardHTTPService):
691
739
  try:
692
740
  with duckdb.connect(str(db_path), read_only=True) as conn:
693
741
  # Get total spans
694
- total_spans = conn.execute("SELECT COUNT(*) FROM spans").fetchone()[0]
742
+ total_spans = conn.execute("SELECT COUNT(*) FROM spans").fetchone()[
743
+ 0
744
+ ]
695
745
 
696
746
  # Get total unique traces
697
747
  total_traces = conn.execute(
@@ -777,7 +827,9 @@ class DashboardHTTPService(BlackboardHTTPService):
777
827
  "events": [event.model_dump() for event in history],
778
828
  }
779
829
  except Exception as e:
780
- logger.exception(f"Failed to get streaming history for {agent_name}: {e}")
830
+ logger.exception(
831
+ f"Failed to get streaming history for {agent_name}: {e}"
832
+ )
781
833
  raise HTTPException(
782
834
  status_code=500, detail=f"Failed to get streaming history: {e!s}"
783
835
  )
@@ -818,24 +870,25 @@ class DashboardHTTPService(BlackboardHTTPService):
818
870
 
819
871
  # 1. Get messages PRODUCED by this node
820
872
  produced_filter = FilterConfig(produced_by={node_id})
821
- produced_artifacts, _produced_count = await orchestrator.store.query_artifacts(
873
+ (
874
+ produced_artifacts,
875
+ _produced_count,
876
+ ) = await orchestrator.store.query_artifacts(
822
877
  produced_filter, limit=100, offset=0, embed_meta=False
823
878
  )
824
879
 
825
880
  for artifact in produced_artifacts:
826
- messages.append(
827
- {
828
- "id": str(artifact.id),
829
- "type": artifact.type,
830
- "direction": "published",
831
- "payload": artifact.payload,
832
- "timestamp": artifact.created_at.isoformat(),
833
- "correlation_id": str(artifact.correlation_id)
834
- if artifact.correlation_id
835
- else None,
836
- "produced_by": artifact.produced_by,
837
- }
838
- )
881
+ messages.append({
882
+ "id": str(artifact.id),
883
+ "type": artifact.type,
884
+ "direction": "published",
885
+ "payload": artifact.payload,
886
+ "timestamp": artifact.created_at.isoformat(),
887
+ "correlation_id": str(artifact.correlation_id)
888
+ if artifact.correlation_id
889
+ else None,
890
+ "produced_by": artifact.produced_by,
891
+ })
839
892
 
840
893
  # 2. Get messages CONSUMED by this node
841
894
  # Query all artifacts with consumption metadata
@@ -848,29 +901,35 @@ class DashboardHTTPService(BlackboardHTTPService):
848
901
  artifact = envelope.artifact
849
902
  for consumption in envelope.consumptions:
850
903
  if consumption.consumer == node_id:
851
- messages.append(
852
- {
853
- "id": str(artifact.id),
854
- "type": artifact.type,
855
- "direction": "consumed",
856
- "payload": artifact.payload,
857
- "timestamp": artifact.created_at.isoformat(),
858
- "correlation_id": str(artifact.correlation_id)
859
- if artifact.correlation_id
860
- else None,
861
- "produced_by": artifact.produced_by,
862
- "consumed_at": consumption.consumed_at.isoformat(),
863
- }
864
- )
904
+ messages.append({
905
+ "id": str(artifact.id),
906
+ "type": artifact.type,
907
+ "direction": "consumed",
908
+ "payload": artifact.payload,
909
+ "timestamp": artifact.created_at.isoformat(),
910
+ "correlation_id": str(artifact.correlation_id)
911
+ if artifact.correlation_id
912
+ else None,
913
+ "produced_by": artifact.produced_by,
914
+ "consumed_at": consumption.consumed_at.isoformat(),
915
+ })
865
916
 
866
917
  # Sort by timestamp (most recent first)
867
- messages.sort(key=lambda m: m.get("consumed_at", m["timestamp"]), reverse=True)
918
+ messages.sort(
919
+ key=lambda m: m.get("consumed_at", m["timestamp"]), reverse=True
920
+ )
868
921
 
869
- return {"node_id": node_id, "messages": messages, "total": len(messages)}
922
+ return {
923
+ "node_id": node_id,
924
+ "messages": messages,
925
+ "total": len(messages),
926
+ }
870
927
 
871
928
  except Exception as e:
872
929
  logger.exception(f"Failed to get message history for {node_id}: {e}")
873
- raise HTTPException(status_code=500, detail=f"Failed to get message history: {e!s}")
930
+ raise HTTPException(
931
+ status_code=500, detail=f"Failed to get message history: {e!s}"
932
+ )
874
933
 
875
934
  @app.get("/api/agents/{agent_id}/runs")
876
935
  async def get_agent_runs(agent_id: str) -> dict[str, Any]:
@@ -919,7 +978,9 @@ class DashboardHTTPService(BlackboardHTTPService):
919
978
 
920
979
  except Exception as e:
921
980
  logger.exception(f"Failed to get run history for {agent_id}: {e}")
922
- raise HTTPException(status_code=500, detail=f"Failed to get run history: {e!s}")
981
+ raise HTTPException(
982
+ status_code=500, detail=f"Failed to get run history: {e!s}"
983
+ )
923
984
 
924
985
  def _register_theme_routes(self) -> None:
925
986
  """Register theme API endpoints for dashboard customization."""
@@ -947,7 +1008,9 @@ class DashboardHTTPService(BlackboardHTTPService):
947
1008
  return {"themes": theme_names}
948
1009
  except Exception as e:
949
1010
  logger.exception(f"Failed to list themes: {e}")
950
- raise HTTPException(status_code=500, detail=f"Failed to list themes: {e!s}")
1011
+ raise HTTPException(
1012
+ status_code=500, detail=f"Failed to list themes: {e!s}"
1013
+ )
951
1014
 
952
1015
  @app.get("/api/themes/{theme_name}")
953
1016
  async def get_theme(theme_name: str) -> dict[str, Any]:
@@ -966,12 +1029,16 @@ class DashboardHTTPService(BlackboardHTTPService):
966
1029
  """
967
1030
  try:
968
1031
  # Sanitize theme name to prevent path traversal
969
- theme_name = theme_name.replace("/", "").replace("\\", "").replace("..", "")
1032
+ theme_name = (
1033
+ theme_name.replace("/", "").replace("\\", "").replace("..", "")
1034
+ )
970
1035
 
971
1036
  theme_path = themes_dir / f"{theme_name}.toml"
972
1037
 
973
1038
  if not theme_path.exists():
974
- raise HTTPException(status_code=404, detail=f"Theme '{theme_name}' not found")
1039
+ raise HTTPException(
1040
+ status_code=404, detail=f"Theme '{theme_name}' not found"
1041
+ )
975
1042
 
976
1043
  # Load TOML theme
977
1044
  theme_data = toml.load(theme_path)
@@ -981,7 +1048,9 @@ class DashboardHTTPService(BlackboardHTTPService):
981
1048
  raise
982
1049
  except Exception as e:
983
1050
  logger.exception(f"Failed to load theme '{theme_name}': {e}")
984
- raise HTTPException(status_code=500, detail=f"Failed to load theme: {e!s}")
1051
+ raise HTTPException(
1052
+ status_code=500, detail=f"Failed to load theme: {e!s}"
1053
+ )
985
1054
 
986
1055
  async def start(self) -> None:
987
1056
  """Start the dashboard service.
@@ -1088,22 +1157,22 @@ def _get_correlation_groups(
1088
1157
  if collected_types.get(type_name, 0) < required_count
1089
1158
  ]
1090
1159
 
1091
- result.append(
1092
- {
1093
- "correlation_key": str(corr_key),
1094
- "created_at": group.created_at_time.isoformat() if group.created_at_time else None,
1095
- "elapsed_seconds": round(elapsed, 1),
1096
- "expires_in_seconds": round(expires_in_seconds, 1)
1097
- if expires_in_seconds is not None
1098
- else None,
1099
- "expires_in_artifacts": expires_in_artifacts,
1100
- "collected_types": collected_types,
1101
- "required_types": dict(group.type_counts),
1102
- "waiting_for": waiting_for,
1103
- "is_complete": group.is_complete(),
1104
- "is_expired": group.is_expired(engine.global_sequence),
1105
- }
1106
- )
1160
+ result.append({
1161
+ "correlation_key": str(corr_key),
1162
+ "created_at": group.created_at_time.isoformat()
1163
+ if group.created_at_time
1164
+ else None,
1165
+ "elapsed_seconds": round(elapsed, 1),
1166
+ "expires_in_seconds": round(expires_in_seconds, 1)
1167
+ if expires_in_seconds is not None
1168
+ else None,
1169
+ "expires_in_artifacts": expires_in_artifacts,
1170
+ "collected_types": collected_types,
1171
+ "required_types": dict(group.type_counts),
1172
+ "waiting_for": waiting_for,
1173
+ "is_complete": group.is_complete(),
1174
+ "is_expired": group.is_expired(engine.global_sequence),
1175
+ })
1107
1176
 
1108
1177
  return result
1109
1178
 
@@ -1328,7 +1397,9 @@ def _build_logic_config( # noqa: F821
1328
1397
  config["batch"]["timeout_seconds"] = int(batch_spec.timeout.total_seconds())
1329
1398
 
1330
1399
  # Get waiting state from BatchEngine
1331
- batch_state = _get_batch_state(orchestrator._batch_engine, agent.name, idx, batch_spec)
1400
+ batch_state = _get_batch_state(
1401
+ orchestrator._batch_engine, agent.name, idx, batch_spec
1402
+ )
1332
1403
  if batch_state:
1333
1404
  if "waiting_state" not in config:
1334
1405
  config["waiting_state"] = {"is_waiting": True}