flock-core 0.5.3__py3-none-any.whl → 0.5.5__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 (44) hide show
  1. flock/agent.py +20 -1
  2. flock/artifact_collector.py +2 -3
  3. flock/batch_accumulator.py +4 -4
  4. flock/components.py +32 -0
  5. flock/correlation_engine.py +9 -4
  6. flock/dashboard/collector.py +4 -0
  7. flock/dashboard/events.py +74 -0
  8. flock/dashboard/graph_builder.py +272 -0
  9. flock/dashboard/models/graph.py +3 -1
  10. flock/dashboard/service.py +363 -14
  11. flock/dashboard/static_v2/assets/index-DFRnI_mt.js +1 -1
  12. flock/dashboard/static_v2/index.html +3 -3
  13. flock/engines/dspy_engine.py +41 -3
  14. flock/engines/examples/__init__.py +6 -0
  15. flock/engines/examples/simple_batch_engine.py +61 -0
  16. flock/frontend/README.md +4 -4
  17. flock/frontend/docs/DESIGN_SYSTEM.md +1 -1
  18. flock/frontend/package-lock.json +2 -2
  19. flock/frontend/package.json +2 -2
  20. flock/frontend/src/components/controls/PublishControl.test.tsx +11 -11
  21. flock/frontend/src/components/controls/PublishControl.tsx +1 -1
  22. flock/frontend/src/components/graph/AgentNode.tsx +4 -0
  23. flock/frontend/src/components/graph/GraphCanvas.tsx +4 -0
  24. flock/frontend/src/components/graph/LogicOperationsDisplay.tsx +463 -0
  25. flock/frontend/src/components/graph/PendingBatchEdge.tsx +141 -0
  26. flock/frontend/src/components/graph/PendingJoinEdge.tsx +144 -0
  27. flock/frontend/src/components/settings/SettingsPanel.css +1 -1
  28. flock/frontend/src/components/settings/ThemeSelector.tsx +2 -2
  29. flock/frontend/src/services/graphService.ts +3 -1
  30. flock/frontend/src/services/indexeddb.ts +1 -1
  31. flock/frontend/src/services/websocket.ts +99 -1
  32. flock/frontend/src/store/graphStore.test.ts +2 -1
  33. flock/frontend/src/store/graphStore.ts +36 -5
  34. flock/frontend/src/styles/variables.css +1 -1
  35. flock/frontend/src/types/graph.ts +86 -0
  36. flock/orchestrator.py +268 -13
  37. flock/patches/__init__.py +1 -0
  38. flock/patches/dspy_streaming_patch.py +1 -0
  39. flock/runtime.py +3 -0
  40. {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/METADATA +11 -1
  41. {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/RECORD +44 -39
  42. {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/WHEEL +0 -0
  43. {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/entry_points.txt +0 -0
  44. {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/licenses/LICENSE +0 -0
flock/agent.py CHANGED
@@ -249,7 +249,26 @@ class Agent(metaclass=AutoTracedMeta):
249
249
  accumulated_metrics: dict[str, float] = {}
250
250
  for engine in engines:
251
251
  current_inputs = await engine.on_pre_evaluate(self, ctx, current_inputs)
252
- result = await engine.evaluate(self, ctx, current_inputs)
252
+ use_batch_mode = bool(getattr(ctx, "is_batch", False))
253
+ try:
254
+ if use_batch_mode:
255
+ logger.debug(
256
+ "Agent %s: routing %d artifacts to %s.evaluate_batch",
257
+ self.name,
258
+ len(current_inputs.artifacts),
259
+ engine.__class__.__name__,
260
+ )
261
+ result = await engine.evaluate_batch(self, ctx, current_inputs)
262
+ else:
263
+ result = await engine.evaluate(self, ctx, current_inputs)
264
+ except NotImplementedError:
265
+ if use_batch_mode:
266
+ logger.error(
267
+ "Agent %s: engine %s does not implement evaluate_batch()",
268
+ self.name,
269
+ engine.__class__.__name__,
270
+ )
271
+ raise
253
272
 
254
273
  # AUTO-WRAP: If engine returns BaseModel instead of EvalResult, wrap it
255
274
  from flock.runtime import EvalResult as ER
@@ -110,9 +110,8 @@ class ArtifactCollector:
110
110
 
111
111
  del self._waiting_pools[pool_key] # Clear for next cycle
112
112
  return (True, artifacts)
113
- else:
114
- # Incomplete - still waiting for more artifacts
115
- return (False, [])
113
+ # Incomplete - still waiting for more artifacts
114
+ return (False, [])
116
115
 
117
116
  def get_waiting_status(
118
117
  self, agent: Agent, subscription_index: int
@@ -11,10 +11,10 @@ Supports BatchSpec-based batching:
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
- from collections import defaultdict
15
- from datetime import datetime, timedelta
14
+ from datetime import datetime
16
15
  from typing import TYPE_CHECKING
17
16
 
17
+
18
18
  if TYPE_CHECKING:
19
19
  from flock.artifacts import Artifact
20
20
  from flock.subscription import BatchSpec, Subscription
@@ -184,7 +184,7 @@ class BatchEngine:
184
184
  # This will work correctly if all groups are the same size
185
185
 
186
186
  # Actually, let's track group count properly:
187
- if not hasattr(accumulator, '_group_count'):
187
+ if not hasattr(accumulator, "_group_count"):
188
188
  accumulator._group_count = 0
189
189
 
190
190
  accumulator._group_count += 1
@@ -249,4 +249,4 @@ class BatchEngine:
249
249
  return results
250
250
 
251
251
 
252
- __all__ = ["BatchEngine", "BatchAccumulator"]
252
+ __all__ = ["BatchAccumulator", "BatchEngine"]
flock/components.py CHANGED
@@ -109,6 +109,38 @@ class EngineComponent(AgentComponent):
109
109
  """Override this method in your engine implementation."""
110
110
  raise NotImplementedError
111
111
 
112
+ async def evaluate_batch(self, agent: Agent, ctx: Context, inputs: EvalInputs) -> EvalResult:
113
+ """Process batch of accumulated artifacts (BatchSpec).
114
+
115
+ Override this method if your engine supports batch processing.
116
+
117
+ Args:
118
+ agent: Agent instance executing this engine
119
+ ctx: Execution context (ctx.is_batch will be True)
120
+ inputs: EvalInputs with inputs.artifacts containing batch items
121
+
122
+ Returns:
123
+ EvalResult with processed artifacts
124
+
125
+ Raises:
126
+ NotImplementedError: If engine doesn't support batching
127
+
128
+ Example:
129
+ >>> async def evaluate_batch(self, agent, ctx, inputs):
130
+ ... events = inputs.all_as(Event) # Get ALL items
131
+ ... results = await bulk_process(events)
132
+ ... return EvalResult.from_objects(*results, agent=agent)
133
+ """
134
+ raise NotImplementedError(
135
+ f"{self.__class__.__name__} does not support batch processing.\n\n"
136
+ f"To fix this:\n"
137
+ f"1. Remove BatchSpec from agent subscription, OR\n"
138
+ f"2. Implement evaluate_batch() in {self.__class__.__name__}, OR\n"
139
+ f"3. Use a batch-aware engine (e.g., CustomBatchEngine)\n\n"
140
+ f"Agent: {agent.name}\n"
141
+ f"Engine: {self.__class__.__name__}"
142
+ )
143
+
112
144
  async def fetch_conversation_context(
113
145
  self,
114
146
  ctx: Context,
@@ -14,6 +14,7 @@ from collections import defaultdict
14
14
  from datetime import datetime, timedelta
15
15
  from typing import TYPE_CHECKING, Any
16
16
 
17
+
17
18
  if TYPE_CHECKING:
18
19
  from flock.artifacts import Artifact
19
20
  from flock.subscription import JoinSpec, Subscription
@@ -51,7 +52,9 @@ class CorrelationGroup:
51
52
  def add_artifact(self, artifact: Artifact, current_sequence: int) -> None:
52
53
  """Add artifact to this correlation group's waiting pool."""
53
54
  if self.created_at_time is None:
54
- self.created_at_time = datetime.now()
55
+ from datetime import timezone
56
+
57
+ self.created_at_time = datetime.now(timezone.utc)
55
58
 
56
59
  self.waiting_artifacts[artifact.type].append(artifact)
57
60
 
@@ -67,11 +70,13 @@ class CorrelationGroup:
67
70
  if isinstance(self.window_spec, int):
68
71
  # Count window: expired if current sequence exceeds created + window
69
72
  return (current_sequence - self.created_at_sequence) > self.window_spec
70
- elif isinstance(self.window_spec, timedelta):
73
+ if isinstance(self.window_spec, timedelta):
71
74
  # Time window: expired if current time exceeds created + window
72
75
  if self.created_at_time is None:
73
76
  return False
74
- elapsed = datetime.now() - self.created_at_time
77
+ from datetime import timezone
78
+
79
+ elapsed = datetime.now(timezone.utc) - self.created_at_time
75
80
  return elapsed > self.window_spec
76
81
  return False
77
82
 
@@ -154,7 +159,7 @@ class CorrelationEngine:
154
159
 
155
160
  try:
156
161
  correlation_key = join_spec.by(payload_instance)
157
- except Exception as e:
162
+ except Exception:
158
163
  # Key extraction failed - skip this artifact
159
164
  # TODO: Log warning?
160
165
  return None
@@ -80,6 +80,9 @@ class AgentSnapshot:
80
80
  first_seen: datetime
81
81
  last_seen: datetime
82
82
  signature: str
83
+ logic_operations: list[dict] = field(
84
+ default_factory=list
85
+ ) # Phase 1.2: JoinSpec/BatchSpec config
83
86
 
84
87
 
85
88
  class DashboardEventCollector(AgentComponent):
@@ -514,6 +517,7 @@ class DashboardEventCollector(AgentComponent):
514
517
  first_seen=snapshot.first_seen,
515
518
  last_seen=snapshot.last_seen,
516
519
  signature=snapshot.signature,
520
+ logic_operations=[dict(op) for op in snapshot.logic_operations], # Phase 1.2
517
521
  )
518
522
 
519
523
  def _snapshot_to_record(self, snapshot: AgentSnapshot) -> AgentSnapshotRecord:
flock/dashboard/events.py CHANGED
@@ -177,10 +177,84 @@ class AgentErrorEvent(BaseModel):
177
177
  failed_at: str # ISO timestamp of failure
178
178
 
179
179
 
180
+ class CorrelationGroupUpdatedEvent(BaseModel):
181
+ """Event emitted when artifact added to correlation group.
182
+
183
+ Phase 1.2: Logic Operations UX Enhancement
184
+ Emitted when an artifact is added to a JoinSpec correlation group
185
+ that has not yet collected all required types.
186
+ """
187
+
188
+ # Event metadata
189
+ timestamp: str = Field(
190
+ default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
191
+ )
192
+
193
+ # Agent identification
194
+ agent_name: str
195
+ subscription_index: int
196
+
197
+ # Correlation group
198
+ correlation_key: str # "patient_123"
199
+
200
+ # Progress
201
+ collected_types: dict[str, int] # {"XRayImage": 1, "LabResults": 0}
202
+ required_types: dict[str, int] # {"XRayImage": 1, "LabResults": 1}
203
+ waiting_for: list[str] # ["LabResults"]
204
+
205
+ # Window progress
206
+ elapsed_seconds: float
207
+ expires_in_seconds: float | None # For time windows
208
+ expires_in_artifacts: int | None # For count windows
209
+
210
+ # Artifact that triggered this event
211
+ artifact_id: str
212
+ artifact_type: str
213
+
214
+ is_complete: bool # Will trigger agent in next orchestrator cycle
215
+
216
+
217
+ class BatchItemAddedEvent(BaseModel):
218
+ """Event emitted when artifact added to batch accumulator.
219
+
220
+ Phase 1.2: Logic Operations UX Enhancement
221
+ Emitted when an artifact is added to a BatchSpec accumulator
222
+ that has not yet reached its flush threshold.
223
+ """
224
+
225
+ # Event metadata
226
+ timestamp: str = Field(
227
+ default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
228
+ )
229
+
230
+ # Agent identification
231
+ agent_name: str
232
+ subscription_index: int
233
+
234
+ # Batch progress
235
+ items_collected: int
236
+ items_target: int | None # None if no size limit
237
+ items_remaining: int | None
238
+
239
+ # Timeout progress
240
+ elapsed_seconds: float
241
+ timeout_seconds: float | None
242
+ timeout_remaining_seconds: float | None
243
+
244
+ # Trigger prediction
245
+ will_flush: str # "on_size" | "on_timeout" | "unknown"
246
+
247
+ # Artifact that triggered this event
248
+ artifact_id: str
249
+ artifact_type: str
250
+
251
+
180
252
  __all__ = [
181
253
  "AgentActivatedEvent",
182
254
  "AgentCompletedEvent",
183
255
  "AgentErrorEvent",
256
+ "BatchItemAddedEvent",
257
+ "CorrelationGroupUpdatedEvent",
184
258
  "MessagePublishedEvent",
185
259
  "StreamingOutputEvent",
186
260
  "SubscriptionInfo",
@@ -73,6 +73,9 @@ class GraphAssembler(metaclass=AutoTracedMeta):
73
73
  agent_snapshots,
74
74
  )
75
75
  edges = self._derive_agent_edges(artifacts)
76
+ # Phase 1.5: Add pending edges for artifacts waiting in correlation/batch
77
+ pending_edges = self._derive_pending_edges(artifacts)
78
+ edges.extend(pending_edges)
76
79
  else:
77
80
  nodes = self._build_message_nodes(artifacts)
78
81
  edges = self._derive_blackboard_edges(artifacts, graph_state)
@@ -192,6 +195,14 @@ class GraphAssembler(metaclass=AutoTracedMeta):
192
195
  consumed = consumed_metrics.get(agent.name)
193
196
  snapshot = agent_snapshots.get(agent.name)
194
197
 
198
+ # Phase 1.2: Build logic_operations from live agent subscriptions
199
+ logic_operations = []
200
+ for idx, subscription in enumerate(agent.subscriptions):
201
+ if subscription.join or subscription.batch:
202
+ logic_config = self._build_logic_config_for_subscription(agent, subscription, idx)
203
+ if logic_config:
204
+ logic_operations.append(logic_config)
205
+
195
206
  node_data = {
196
207
  "name": agent.name,
197
208
  "status": agent_status.get(agent.name, "idle"),
@@ -206,6 +217,7 @@ class GraphAssembler(metaclass=AutoTracedMeta):
206
217
  "firstSeen": snapshot.first_seen.isoformat() if snapshot else None,
207
218
  "lastSeen": snapshot.last_seen.isoformat() if snapshot else None,
208
219
  "signature": snapshot.signature if snapshot else None,
220
+ "logicOperations": logic_operations, # Phase 1.2
209
221
  }
210
222
 
211
223
  nodes.append(
@@ -241,6 +253,7 @@ class GraphAssembler(metaclass=AutoTracedMeta):
241
253
  "firstSeen": snapshot.first_seen.isoformat(),
242
254
  "lastSeen": snapshot.last_seen.isoformat(),
243
255
  "signature": snapshot.signature,
256
+ "logicOperations": list(snapshot.logic_operations), # Phase 1.2: From snapshot
244
257
  }
245
258
 
246
259
  nodes.append(
@@ -378,6 +391,196 @@ class GraphAssembler(metaclass=AutoTracedMeta):
378
391
 
379
392
  return edges
380
393
 
394
+ def _derive_pending_edges(
395
+ self,
396
+ artifacts: Mapping[str, GraphArtifact],
397
+ ) -> list[GraphEdge]:
398
+ """
399
+ Phase 1.5: Build pending edges for artifacts waiting in correlation/batch.
400
+
401
+ Creates visual edges (purple/orange dashed) from artifact producers to agents
402
+ that have those artifacts waiting in JoinSpec/BatchSpec queues.
403
+
404
+ Returns edges with type "pending_join" or "pending_batch".
405
+ """
406
+ edges: list[GraphEdge] = []
407
+ edge_counter = 0
408
+
409
+ # Build map of artifact_id -> artifact for quick lookup
410
+ artifact_map = {art.artifact_id: art for art in artifacts.values()}
411
+
412
+ # Iterate through all active agents with JoinSpec/BatchSpec subscriptions
413
+ for agent in self._orchestrator.agents:
414
+ for sub_idx, subscription in enumerate(agent.subscriptions):
415
+ # JoinSpec pending edges
416
+ if subscription.join:
417
+ pending_join_edges = self._build_pending_join_edges(
418
+ agent.name,
419
+ sub_idx,
420
+ subscription,
421
+ artifact_map,
422
+ edge_counter
423
+ )
424
+ edges.extend(pending_join_edges)
425
+ edge_counter += len(pending_join_edges)
426
+
427
+ # BatchSpec pending edges
428
+ if subscription.batch:
429
+ pending_batch_edges = self._build_pending_batch_edges(
430
+ agent.name,
431
+ sub_idx,
432
+ subscription,
433
+ artifact_map,
434
+ edge_counter
435
+ )
436
+ edges.extend(pending_batch_edges)
437
+ edge_counter += len(pending_batch_edges)
438
+
439
+ return edges
440
+
441
+ def _build_pending_join_edges(
442
+ self,
443
+ agent_name: str,
444
+ sub_idx: int,
445
+ subscription: "Subscription", # noqa: F821
446
+ artifact_map: Mapping[str, GraphArtifact],
447
+ start_counter: int,
448
+ ) -> list[GraphEdge]:
449
+ """Build pending edges for JoinSpec correlation groups."""
450
+ from flock.dashboard.service import _get_correlation_groups
451
+
452
+ edges: list[GraphEdge] = []
453
+ correlation_groups = _get_correlation_groups(
454
+ self._orchestrator._correlation_engine,
455
+ agent_name,
456
+ sub_idx
457
+ )
458
+
459
+ if not correlation_groups:
460
+ return edges
461
+
462
+ edge_counter = start_counter
463
+
464
+ # For each correlation group, create edges for waiting artifacts
465
+ for group in correlation_groups:
466
+ # Get artifacts in this correlation group's waiting pool
467
+ correlation_key = group.get("correlation_key")
468
+ if not correlation_key:
469
+ continue
470
+
471
+ # Get the actual correlation group from engine
472
+ pool_key = (agent_name, sub_idx)
473
+ groups_dict = self._orchestrator._correlation_engine.correlation_groups.get(pool_key, {})
474
+ corr_group = groups_dict.get(correlation_key)
475
+
476
+ if not corr_group:
477
+ continue
478
+
479
+ # Build edges for each artifact type in the waiting pool
480
+ for artifact_type, artifact_list in corr_group.waiting_artifacts.items():
481
+ for artifact in artifact_list:
482
+ artifact_id = str(artifact.id)
483
+ graph_artifact = artifact_map.get(artifact_id)
484
+
485
+ if not graph_artifact:
486
+ continue
487
+
488
+ producer = graph_artifact.produced_by or "external"
489
+
490
+ # Create pending join edge
491
+ edge_id = f"pending_join__{producer}__{agent_name}__{artifact_id}__{edge_counter}"
492
+ edge_counter += 1
493
+
494
+ edges.append(
495
+ GraphEdge(
496
+ id=edge_id,
497
+ source=producer,
498
+ target=agent_name,
499
+ type="pending_join",
500
+ label=f"⋈ {correlation_key}",
501
+ data={
502
+ "artifactId": artifact_id,
503
+ "artifactType": artifact_type,
504
+ "correlationKey": correlation_key,
505
+ "subscriptionIndex": sub_idx,
506
+ "waitingFor": group.get("waiting_for", []),
507
+ "labelOffset": 0.0,
508
+ },
509
+ marker_end=GraphMarker(),
510
+ hidden=False,
511
+ )
512
+ )
513
+
514
+ return edges
515
+
516
+ def _build_pending_batch_edges(
517
+ self,
518
+ agent_name: str,
519
+ sub_idx: int,
520
+ subscription: "Subscription", # noqa: F821
521
+ artifact_map: Mapping[str, GraphArtifact],
522
+ start_counter: int,
523
+ ) -> list[GraphEdge]:
524
+ """Build pending edges for BatchSpec accumulation."""
525
+ from flock.dashboard.service import _get_batch_state
526
+
527
+ edges: list[GraphEdge] = []
528
+ batch_state = _get_batch_state(
529
+ self._orchestrator._batch_engine,
530
+ agent_name,
531
+ sub_idx,
532
+ subscription.batch
533
+ )
534
+
535
+ if not batch_state:
536
+ return edges
537
+
538
+ # Get the batch accumulator
539
+ batch_key = (agent_name, sub_idx)
540
+ accumulator = self._orchestrator._batch_engine.batches.get(batch_key)
541
+
542
+ if not accumulator or not accumulator.artifacts:
543
+ return edges
544
+
545
+ edge_counter = start_counter
546
+
547
+ # Create edges for each artifact in the batch
548
+ for artifact in accumulator.artifacts:
549
+ artifact_id = str(artifact.id)
550
+ graph_artifact = artifact_map.get(artifact_id)
551
+
552
+ if not graph_artifact:
553
+ continue
554
+
555
+ producer = graph_artifact.produced_by or "external"
556
+ artifact_type = graph_artifact.artifact_type
557
+
558
+ # Create pending batch edge
559
+ edge_id = f"pending_batch__{producer}__{agent_name}__{artifact_id}__{edge_counter}"
560
+ edge_counter += 1
561
+
562
+ edges.append(
563
+ GraphEdge(
564
+ id=edge_id,
565
+ source=producer,
566
+ target=agent_name,
567
+ type="pending_batch",
568
+ label=f"⊞ {batch_state['items_collected']}/{batch_state['items_target'] or '∞'}",
569
+ data={
570
+ "artifactId": artifact_id,
571
+ "artifactType": artifact_type,
572
+ "subscriptionIndex": sub_idx,
573
+ "itemsCollected": batch_state["items_collected"],
574
+ "itemsTarget": batch_state["items_target"],
575
+ "labelOffset": 0.0,
576
+ },
577
+ marker_end=GraphMarker(),
578
+ hidden=False,
579
+ )
580
+ )
581
+
582
+ return edges
583
+
381
584
  def _derive_blackboard_edges(
382
585
  self,
383
586
  artifacts: Mapping[str, GraphArtifact],
@@ -561,3 +764,72 @@ class GraphAssembler(metaclass=AutoTracedMeta):
561
764
  for index, edge_id in enumerate(edge_ids):
562
765
  offsets[edge_id] = index * step - offset_range / 2
563
766
  return offsets
767
+
768
+ def _build_logic_config_for_subscription(self, agent, subscription, idx):
769
+ """Build logic operations config for a subscription (Phase 1.2 + 1.2.1: with waiting_state)."""
770
+ if not subscription.join and not subscription.batch:
771
+ return None
772
+
773
+ config = {
774
+ "subscription_index": idx,
775
+ "subscription_types": list(subscription.type_names),
776
+ }
777
+
778
+ # JoinSpec configuration
779
+ if subscription.join:
780
+ join_spec = subscription.join
781
+ window_type = "time" if isinstance(join_spec.within, timedelta) else "count"
782
+ window_value = (
783
+ int(join_spec.within.total_seconds())
784
+ if isinstance(join_spec.within, timedelta)
785
+ else join_spec.within
786
+ )
787
+
788
+ config["join"] = {
789
+ "correlation_strategy": "by_key",
790
+ "window_type": window_type,
791
+ "window_value": window_value,
792
+ "window_unit": "seconds" if window_type == "time" else "artifacts",
793
+ "required_types": list(subscription.type_names),
794
+ "type_counts": dict(subscription.type_counts),
795
+ }
796
+
797
+ # Phase 1.2.1: Get waiting state from CorrelationEngine
798
+ from flock.dashboard.service import _get_correlation_groups
799
+ correlation_groups = _get_correlation_groups(
800
+ self._orchestrator._correlation_engine, agent.name, idx
801
+ )
802
+ if correlation_groups:
803
+ config["waiting_state"] = {
804
+ "is_waiting": True,
805
+ "correlation_groups": correlation_groups,
806
+ }
807
+
808
+ # BatchSpec configuration
809
+ if subscription.batch:
810
+ batch_spec = subscription.batch
811
+ strategy = (
812
+ "hybrid"
813
+ if batch_spec.size and batch_spec.timeout
814
+ else "size"
815
+ if batch_spec.size
816
+ else "timeout"
817
+ )
818
+
819
+ config["batch"] = {
820
+ "strategy": strategy,
821
+ }
822
+ if batch_spec.size:
823
+ config["batch"]["size"] = batch_spec.size
824
+ if batch_spec.timeout:
825
+ config["batch"]["timeout_seconds"] = int(batch_spec.timeout.total_seconds())
826
+
827
+ # Phase 1.2.1: Get waiting state from BatchEngine
828
+ from flock.dashboard.service import _get_batch_state
829
+ batch_state = _get_batch_state(self._orchestrator._batch_engine, agent.name, idx, batch_spec)
830
+ if batch_state:
831
+ if "waiting_state" not in config:
832
+ config["waiting_state"] = {"is_waiting": True}
833
+ config["waiting_state"]["batch_state"] = batch_state
834
+
835
+ return config
@@ -79,7 +79,9 @@ class GraphEdge(BaseModel):
79
79
  id: str
80
80
  source: str
81
81
  target: str
82
- type: Literal["message_flow", "transformation"]
82
+ type: Literal[
83
+ "message_flow", "transformation", "pending_join", "pending_batch"
84
+ ] # Phase 1.5: Added pending edge types
83
85
  label: str | None = None
84
86
  data: dict[str, Any] = Field(default_factory=dict)
85
87
  marker_end: GraphMarker | None = Field(default=None, alias="markerEnd")