flock-core 0.5.11__py3-none-any.whl → 0.5.21__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 (94) hide show
  1. flock/__init__.py +1 -1
  2. flock/agent/__init__.py +30 -0
  3. flock/agent/builder_helpers.py +192 -0
  4. flock/agent/builder_validator.py +169 -0
  5. flock/agent/component_lifecycle.py +325 -0
  6. flock/agent/context_resolver.py +141 -0
  7. flock/agent/mcp_integration.py +212 -0
  8. flock/agent/output_processor.py +304 -0
  9. flock/api/__init__.py +20 -0
  10. flock/{api_models.py → api/models.py} +0 -2
  11. flock/{service.py → api/service.py} +3 -3
  12. flock/cli.py +2 -2
  13. flock/components/__init__.py +41 -0
  14. flock/components/agent/__init__.py +22 -0
  15. flock/{components.py → components/agent/base.py} +4 -3
  16. flock/{utility/output_utility_component.py → components/agent/output_utility.py} +12 -7
  17. flock/components/orchestrator/__init__.py +22 -0
  18. flock/{orchestrator_component.py → components/orchestrator/base.py} +5 -293
  19. flock/components/orchestrator/circuit_breaker.py +95 -0
  20. flock/components/orchestrator/collection.py +143 -0
  21. flock/components/orchestrator/deduplication.py +78 -0
  22. flock/core/__init__.py +30 -0
  23. flock/core/agent.py +953 -0
  24. flock/{artifacts.py → core/artifacts.py} +1 -1
  25. flock/{context_provider.py → core/context_provider.py} +3 -3
  26. flock/core/orchestrator.py +1102 -0
  27. flock/{store.py → core/store.py} +99 -454
  28. flock/{subscription.py → core/subscription.py} +1 -1
  29. flock/dashboard/collector.py +5 -5
  30. flock/dashboard/events.py +1 -1
  31. flock/dashboard/graph_builder.py +7 -7
  32. flock/dashboard/routes/__init__.py +21 -0
  33. flock/dashboard/routes/control.py +327 -0
  34. flock/dashboard/routes/helpers.py +340 -0
  35. flock/dashboard/routes/themes.py +76 -0
  36. flock/dashboard/routes/traces.py +521 -0
  37. flock/dashboard/routes/websocket.py +108 -0
  38. flock/dashboard/service.py +43 -1316
  39. flock/engines/dspy/__init__.py +20 -0
  40. flock/engines/dspy/artifact_materializer.py +216 -0
  41. flock/engines/dspy/signature_builder.py +474 -0
  42. flock/engines/dspy/streaming_executor.py +812 -0
  43. flock/engines/dspy_engine.py +45 -1330
  44. flock/engines/examples/simple_batch_engine.py +2 -2
  45. flock/engines/streaming/__init__.py +3 -0
  46. flock/engines/streaming/sinks.py +489 -0
  47. flock/examples.py +7 -7
  48. flock/logging/logging.py +1 -16
  49. flock/models/__init__.py +10 -0
  50. flock/orchestrator/__init__.py +45 -0
  51. flock/{artifact_collector.py → orchestrator/artifact_collector.py} +3 -3
  52. flock/orchestrator/artifact_manager.py +168 -0
  53. flock/{batch_accumulator.py → orchestrator/batch_accumulator.py} +2 -2
  54. flock/orchestrator/component_runner.py +389 -0
  55. flock/orchestrator/context_builder.py +167 -0
  56. flock/{correlation_engine.py → orchestrator/correlation_engine.py} +2 -2
  57. flock/orchestrator/event_emitter.py +167 -0
  58. flock/orchestrator/initialization.py +184 -0
  59. flock/orchestrator/lifecycle_manager.py +226 -0
  60. flock/orchestrator/mcp_manager.py +202 -0
  61. flock/orchestrator/scheduler.py +189 -0
  62. flock/orchestrator/server_manager.py +234 -0
  63. flock/orchestrator/tracing.py +147 -0
  64. flock/storage/__init__.py +10 -0
  65. flock/storage/artifact_aggregator.py +158 -0
  66. flock/storage/in_memory/__init__.py +6 -0
  67. flock/storage/in_memory/artifact_filter.py +114 -0
  68. flock/storage/in_memory/history_aggregator.py +115 -0
  69. flock/storage/sqlite/__init__.py +10 -0
  70. flock/storage/sqlite/agent_history_queries.py +154 -0
  71. flock/storage/sqlite/consumption_loader.py +100 -0
  72. flock/storage/sqlite/query_builder.py +112 -0
  73. flock/storage/sqlite/query_params_builder.py +91 -0
  74. flock/storage/sqlite/schema_manager.py +168 -0
  75. flock/storage/sqlite/summary_queries.py +194 -0
  76. flock/utils/__init__.py +14 -0
  77. flock/utils/async_utils.py +67 -0
  78. flock/{runtime.py → utils/runtime.py} +3 -3
  79. flock/utils/time_utils.py +53 -0
  80. flock/utils/type_resolution.py +38 -0
  81. flock/{utilities.py → utils/utilities.py} +2 -2
  82. flock/utils/validation.py +57 -0
  83. flock/utils/visibility.py +79 -0
  84. flock/utils/visibility_utils.py +134 -0
  85. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/METADATA +19 -5
  86. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/RECORD +92 -34
  87. flock/agent.py +0 -1578
  88. flock/orchestrator.py +0 -1983
  89. /flock/{visibility.py → core/visibility.py} +0 -0
  90. /flock/{system_artifacts.py → models/system_artifacts.py} +0 -0
  91. /flock/{helper → utils}/cli_helper.py +0 -0
  92. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/WHEEL +0 -0
  93. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/entry_points.txt +0 -0
  94. {flock_core-0.5.11.dist-info → flock_core-0.5.21.dist-info}/licenses/LICENSE +0 -0
@@ -9,18 +9,17 @@ from typing import TYPE_CHECKING
9
9
 
10
10
  from pydantic import BaseModel, Field
11
11
 
12
- from flock.components import TracedModelMeta
12
+ from flock.components.agent.base import TracedModelMeta
13
13
  from flock.logging.logging import get_logger
14
14
 
15
15
 
16
16
  if TYPE_CHECKING:
17
- from flock.agent import Agent
18
- from flock.artifacts import Artifact
19
- from flock.orchestrator import Flock
20
- from flock.subscription import Subscription
17
+ from flock.core import Agent, Flock
18
+ from flock.core.artifacts import Artifact
19
+ from flock.core.subscription import Subscription
21
20
 
22
21
  # Initialize logger for components
23
- logger = get_logger("flock.component")
22
+ logger = get_logger("flock.components.orchestrator")
24
23
 
25
24
 
26
25
  class ScheduleDecision(str, Enum):
@@ -401,295 +400,8 @@ class OrchestratorComponent(BaseModel, metaclass=TracedModelMeta):
401
400
  """
402
401
 
403
402
 
404
- class BuiltinCollectionComponent(OrchestratorComponent):
405
- """Built-in component that handles AND gates, correlation, and batching.
406
-
407
- This component wraps the existing ArtifactCollector, CorrelationEngine,
408
- and BatchEngine to provide collection logic via the component system.
409
-
410
- Priority: 100 (runs after user components for circuit breaking/dedup)
411
- """
412
-
413
- priority: int = 100 # Run late (after circuit breaker/dedup components)
414
- name: str = "builtin_collection"
415
-
416
- async def on_collect_artifacts(
417
- self,
418
- orchestrator: Flock,
419
- artifact: Artifact,
420
- agent: Agent,
421
- subscription: Subscription,
422
- ) -> CollectionResult | None:
423
- """Handle AND gates, correlation (JoinSpec), and batching (BatchSpec).
424
-
425
- This maintains backward compatibility with existing collection engines
426
- while allowing user components to override collection logic.
427
- """
428
- from datetime import timedelta
429
-
430
- # Check if subscription has required attributes (defensive for mocks/tests)
431
- if (
432
- not hasattr(subscription, "join")
433
- or not hasattr(subscription, "type_models")
434
- or not hasattr(subscription, "batch")
435
- ):
436
- # Fallback: immediate with single artifact
437
- return CollectionResult.immediate([artifact])
438
-
439
- # JoinSpec CORRELATION: Check if subscription has correlated AND gate
440
- if subscription.join is not None:
441
- subscription_index = agent.subscriptions.index(subscription)
442
- completed_group = orchestrator._correlation_engine.add_artifact(
443
- artifact=artifact,
444
- subscription=subscription,
445
- subscription_index=subscription_index,
446
- )
447
-
448
- # Start correlation cleanup task if time-based window
449
- if (
450
- isinstance(subscription.join.within, timedelta)
451
- and orchestrator._correlation_cleanup_task is None
452
- ):
453
- import asyncio
454
-
455
- orchestrator._correlation_cleanup_task = asyncio.create_task(
456
- orchestrator._correlation_cleanup_loop()
457
- )
458
-
459
- if completed_group is None:
460
- # Still waiting for correlation to complete
461
- await orchestrator._emit_correlation_updated_event(
462
- agent_name=agent.name,
463
- subscription_index=subscription_index,
464
- artifact=artifact,
465
- )
466
- return CollectionResult.waiting()
467
-
468
- # Correlation complete!
469
- artifacts = completed_group.get_artifacts()
470
- else:
471
- # AND GATE: Use artifact collector for simple AND gates (no correlation)
472
- is_complete, artifacts = orchestrator._artifact_collector.add_artifact(
473
- agent, subscription, artifact
474
- )
475
-
476
- if not is_complete:
477
- return CollectionResult.waiting()
478
-
479
- # BatchSpec BATCHING: Check if subscription has batch accumulator
480
- if subscription.batch is not None:
481
- subscription_index = agent.subscriptions.index(subscription)
482
-
483
- # Treat artifact groups as single batch items
484
- if subscription.join is not None or len(subscription.type_models) > 1:
485
- should_flush = orchestrator._batch_engine.add_artifact_group(
486
- artifacts=artifacts,
487
- subscription=subscription,
488
- subscription_index=subscription_index,
489
- )
490
-
491
- if (
492
- subscription.batch.timeout
493
- and orchestrator._batch_timeout_task is None
494
- ):
495
- import asyncio
496
-
497
- orchestrator._batch_timeout_task = asyncio.create_task(
498
- orchestrator._batch_timeout_checker_loop()
499
- )
500
- else:
501
- # Single type: Add each artifact individually
502
- should_flush = False
503
- for single_artifact in artifacts:
504
- should_flush = orchestrator._batch_engine.add_artifact(
505
- artifact=single_artifact,
506
- subscription=subscription,
507
- subscription_index=subscription_index,
508
- )
509
-
510
- if (
511
- subscription.batch.timeout
512
- and orchestrator._batch_timeout_task is None
513
- ):
514
- import asyncio
515
-
516
- orchestrator._batch_timeout_task = asyncio.create_task(
517
- orchestrator._batch_timeout_checker_loop()
518
- )
519
-
520
- if should_flush:
521
- break
522
-
523
- if not should_flush:
524
- # Batch not full yet
525
- await orchestrator._emit_batch_item_added_event(
526
- agent_name=agent.name,
527
- subscription_index=subscription_index,
528
- subscription=subscription,
529
- artifact=artifact,
530
- )
531
- return CollectionResult.waiting()
532
-
533
- # Flush batch
534
- batched_artifacts = orchestrator._batch_engine.flush_batch(
535
- agent.name, subscription_index
536
- )
537
-
538
- if batched_artifacts is None:
539
- return CollectionResult.waiting()
540
-
541
- artifacts = batched_artifacts
542
-
543
- # Collection complete!
544
- return CollectionResult.immediate(artifacts)
545
-
546
-
547
- class CircuitBreakerComponent(OrchestratorComponent):
548
- """Circuit breaker to prevent runaway agent loops.
549
-
550
- Tracks iteration count per agent and blocks scheduling when limit is reached.
551
- Automatically resets counters when orchestrator becomes idle.
552
-
553
- Priority: 10 (runs early, before deduplication)
554
-
555
- Configuration:
556
- max_iterations: Maximum iterations per agent before circuit breaker trips
557
-
558
- Examples:
559
- >>> # Use default (1000 iterations)
560
- >>> flock = Flock("openai/gpt-4.1")
561
-
562
- >>> # Custom limit
563
- >>> flock = Flock("openai/gpt-4.1")
564
- >>> for component in flock._components:
565
- ... if component.name == "circuit_breaker":
566
- ... component.max_iterations = 500
567
- """
568
-
569
- priority: int = 10 # Run early (before dedup at 20)
570
- name: str = "circuit_breaker"
571
- max_iterations: int = 1000 # Default from orchestrator
572
-
573
- def __init__(self, max_iterations: int = 1000, **kwargs):
574
- """Initialize circuit breaker with iteration limit.
575
-
576
- Args:
577
- max_iterations: Maximum iterations per agent (default 1000)
578
- """
579
- super().__init__(**kwargs)
580
- self.max_iterations = max_iterations
581
- self._iteration_counts: dict[str, int] = {}
582
-
583
- async def on_before_schedule(
584
- self,
585
- orchestrator: Flock,
586
- artifact: Artifact,
587
- agent: Agent,
588
- subscription: Subscription,
589
- ) -> ScheduleDecision:
590
- """Check if agent has exceeded iteration limit.
591
-
592
- Returns SKIP if agent has reached max_iterations, preventing
593
- potential infinite loops.
594
-
595
- Uses orchestrator.max_agent_iterations if available, otherwise
596
- falls back to self.max_iterations.
597
- """
598
- current_count = self._iteration_counts.get(agent.name, 0)
599
-
600
- # Check orchestrator property first (allows runtime modification)
601
- # Fall back to component's max_iterations if orchestrator property doesn't exist
602
- max_limit = self.max_iterations
603
- if hasattr(orchestrator, "max_agent_iterations"):
604
- orch_limit = orchestrator.max_agent_iterations
605
- # Only use orchestrator limit if it's a valid int
606
- if isinstance(orch_limit, int):
607
- max_limit = orch_limit
608
-
609
- if current_count >= max_limit:
610
- # Circuit breaker tripped
611
- return ScheduleDecision.SKIP
612
-
613
- # Increment counter
614
- self._iteration_counts[agent.name] = current_count + 1
615
- return ScheduleDecision.CONTINUE
616
-
617
- async def on_orchestrator_idle(self, orchestrator: Flock) -> None:
618
- """Reset iteration counters when orchestrator becomes idle.
619
-
620
- This prevents the circuit breaker from permanently blocking agents
621
- across different workflow runs.
622
- """
623
- self._iteration_counts.clear()
624
-
625
-
626
- class DeduplicationComponent(OrchestratorComponent):
627
- """Deduplication to prevent agents from processing same artifact multiple times.
628
-
629
- Tracks which artifacts have been processed by which agents and skips
630
- duplicate scheduling attempts.
631
-
632
- Priority: 20 (runs after circuit breaker at 10)
633
-
634
- Examples:
635
- >>> # Auto-added to all orchestrators
636
- >>> flock = Flock("openai/gpt-4.1")
637
-
638
- >>> # Deduplication prevents:
639
- >>> # - Agents re-processing artifacts they already handled
640
- >>> # - Feedback loops from agent self-triggers
641
- >>> # - Duplicate work from retry logic
642
- """
643
-
644
- priority: int = 20 # Run after circuit breaker (10)
645
- name: str = "deduplication"
646
-
647
- def __init__(self, **kwargs):
648
- """Initialize deduplication with empty processed set."""
649
- super().__init__(**kwargs)
650
- self._processed: set[tuple[str, str]] = set() # (artifact_id, agent_name)
651
-
652
- async def on_before_schedule(
653
- self,
654
- orchestrator: Flock,
655
- artifact: Artifact,
656
- agent: Agent,
657
- subscription: Subscription,
658
- ) -> ScheduleDecision:
659
- """Check if artifact has already been processed by this agent.
660
-
661
- Returns SKIP if agent has already processed this artifact,
662
- preventing duplicate work.
663
- """
664
- key = (str(artifact.id), agent.name)
665
-
666
- if key in self._processed:
667
- # Already processed - skip
668
- return ScheduleDecision.SKIP
669
-
670
- # Not yet processed - allow
671
- return ScheduleDecision.CONTINUE
672
-
673
- async def on_before_agent_schedule(
674
- self, orchestrator: Flock, agent: Agent, artifacts: list[Artifact]
675
- ) -> list[Artifact]:
676
- """Mark artifacts as processed before agent execution.
677
-
678
- This ensures artifacts are marked as seen even if agent execution fails,
679
- preventing infinite retry loops.
680
- """
681
- for artifact in artifacts:
682
- key = (str(artifact.id), agent.name)
683
- self._processed.add(key)
684
-
685
- return artifacts
686
-
687
-
688
403
  __all__ = [
689
- "BuiltinCollectionComponent",
690
- "CircuitBreakerComponent",
691
404
  "CollectionResult",
692
- "DeduplicationComponent",
693
405
  "OrchestratorComponent",
694
406
  "OrchestratorComponentConfig",
695
407
  "ScheduleDecision",
@@ -0,0 +1,95 @@
1
+ """Circuit breaker component to prevent runaway agent loops."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from flock.components.orchestrator.base import OrchestratorComponent, ScheduleDecision
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from flock.core import Agent, Flock
12
+ from flock.core.artifacts import Artifact
13
+ from flock.core.subscription import Subscription
14
+
15
+
16
+ class CircuitBreakerComponent(OrchestratorComponent):
17
+ """Circuit breaker to prevent runaway agent loops.
18
+
19
+ Tracks iteration count per agent and blocks scheduling when limit is reached.
20
+ Automatically resets counters when orchestrator becomes idle.
21
+
22
+ Priority: 10 (runs early, before deduplication)
23
+
24
+ Configuration:
25
+ max_iterations: Maximum iterations per agent before circuit breaker trips
26
+
27
+ Examples:
28
+ >>> # Use default (1000 iterations)
29
+ >>> flock = Flock("openai/gpt-4.1")
30
+
31
+ >>> # Custom limit
32
+ >>> flock = Flock("openai/gpt-4.1")
33
+ >>> for component in flock._components:
34
+ ... if component.name == "circuit_breaker":
35
+ ... component.max_iterations = 500
36
+ """
37
+
38
+ priority: int = 10 # Run early (before dedup at 20)
39
+ name: str = "circuit_breaker"
40
+ max_iterations: int = 1000 # Default from orchestrator
41
+
42
+ def __init__(self, max_iterations: int = 1000, **kwargs):
43
+ """Initialize circuit breaker with iteration limit.
44
+
45
+ Args:
46
+ max_iterations: Maximum iterations per agent (default 1000)
47
+ """
48
+ super().__init__(**kwargs)
49
+ self.max_iterations = max_iterations
50
+ self._iteration_counts: dict[str, int] = {}
51
+
52
+ async def on_before_schedule(
53
+ self,
54
+ orchestrator: Flock,
55
+ artifact: Artifact,
56
+ agent: Agent,
57
+ subscription: Subscription,
58
+ ) -> ScheduleDecision:
59
+ """Check if agent has exceeded iteration limit.
60
+
61
+ Returns SKIP if agent has reached max_iterations, preventing
62
+ potential infinite loops.
63
+
64
+ Uses orchestrator.max_agent_iterations if available, otherwise
65
+ falls back to self.max_iterations.
66
+ """
67
+ current_count = self._iteration_counts.get(agent.name, 0)
68
+
69
+ # Check orchestrator property first (allows runtime modification)
70
+ # Fall back to component's max_iterations if orchestrator property doesn't exist
71
+ max_limit = self.max_iterations
72
+ if hasattr(orchestrator, "max_agent_iterations"):
73
+ orch_limit = orchestrator.max_agent_iterations
74
+ # Only use orchestrator limit if it's a valid int
75
+ if isinstance(orch_limit, int):
76
+ max_limit = orch_limit
77
+
78
+ if current_count >= max_limit:
79
+ # Circuit breaker tripped
80
+ return ScheduleDecision.SKIP
81
+
82
+ # Increment counter
83
+ self._iteration_counts[agent.name] = current_count + 1
84
+ return ScheduleDecision.CONTINUE
85
+
86
+ async def on_orchestrator_idle(self, orchestrator: Flock) -> None:
87
+ """Reset iteration counters when orchestrator becomes idle.
88
+
89
+ This prevents the circuit breaker from permanently blocking agents
90
+ across different workflow runs.
91
+ """
92
+ self._iteration_counts.clear()
93
+
94
+
95
+ __all__ = ["CircuitBreakerComponent"]
@@ -0,0 +1,143 @@
1
+ """Built-in collection component for AND gates, correlation, and batching."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from flock.components.orchestrator.base import (
8
+ CollectionResult,
9
+ OrchestratorComponent,
10
+ )
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from flock.core import Agent, Flock
15
+ from flock.core.artifacts import Artifact
16
+ from flock.core.subscription import Subscription
17
+
18
+
19
+ class BuiltinCollectionComponent(OrchestratorComponent):
20
+ """Built-in component that handles AND gates, correlation, and batching.
21
+
22
+ This component wraps the existing ArtifactCollector, CorrelationEngine,
23
+ and BatchEngine to provide collection logic via the component system.
24
+
25
+ Priority: 100 (runs after user components for circuit breaking/dedup)
26
+ """
27
+
28
+ priority: int = 100 # Run late (after circuit breaker/dedup components)
29
+ name: str = "builtin_collection"
30
+
31
+ async def on_collect_artifacts(
32
+ self,
33
+ orchestrator: Flock,
34
+ artifact: Artifact,
35
+ agent: Agent,
36
+ subscription: Subscription,
37
+ ) -> CollectionResult | None:
38
+ """Handle AND gates, correlation (JoinSpec), and batching (BatchSpec).
39
+
40
+ Provides collection logic via the component system, allowing user
41
+ components to override if needed.
42
+ """
43
+ from datetime import timedelta
44
+
45
+ # Check if subscription has required attributes (defensive for mocks/tests)
46
+ if (
47
+ not hasattr(subscription, "join")
48
+ or not hasattr(subscription, "type_models")
49
+ or not hasattr(subscription, "batch")
50
+ ):
51
+ # Fallback: immediate with single artifact
52
+ return CollectionResult.immediate([artifact])
53
+
54
+ # JoinSpec CORRELATION: Check if subscription has correlated AND gate
55
+ if subscription.join is not None:
56
+ subscription_index = agent.subscriptions.index(subscription)
57
+ completed_group = orchestrator._correlation_engine.add_artifact(
58
+ artifact=artifact,
59
+ subscription=subscription,
60
+ subscription_index=subscription_index,
61
+ )
62
+
63
+ # Phase 5A: Start correlation cleanup task if time-based window (delegate to LifecycleManager)
64
+ if isinstance(subscription.join.within, timedelta):
65
+ await orchestrator._lifecycle_manager.start_correlation_cleanup()
66
+
67
+ if completed_group is None:
68
+ # Still waiting for correlation to complete
69
+ await orchestrator._emit_correlation_updated_event(
70
+ agent_name=agent.name,
71
+ subscription_index=subscription_index,
72
+ artifact=artifact,
73
+ )
74
+ return CollectionResult.waiting()
75
+
76
+ # Correlation complete!
77
+ artifacts = completed_group.get_artifacts()
78
+ else:
79
+ # AND GATE: Use artifact collector for simple AND gates (no correlation)
80
+ is_complete, artifacts = orchestrator._artifact_collector.add_artifact(
81
+ agent, subscription, artifact
82
+ )
83
+
84
+ if not is_complete:
85
+ return CollectionResult.waiting()
86
+
87
+ # BatchSpec BATCHING: Check if subscription has batch accumulator
88
+ if subscription.batch is not None:
89
+ subscription_index = agent.subscriptions.index(subscription)
90
+
91
+ # Treat artifact groups as single batch items
92
+ if subscription.join is not None or len(subscription.type_models) > 1:
93
+ should_flush = orchestrator._batch_engine.add_artifact_group(
94
+ artifacts=artifacts,
95
+ subscription=subscription,
96
+ subscription_index=subscription_index,
97
+ )
98
+
99
+ # Phase 5A: Start batch timeout checker if batch has timeout (delegate to LifecycleManager)
100
+ if subscription.batch.timeout:
101
+ await orchestrator._lifecycle_manager.start_batch_timeout_checker()
102
+ else:
103
+ # Single type: Add each artifact individually
104
+ should_flush = False
105
+ for single_artifact in artifacts:
106
+ should_flush = orchestrator._batch_engine.add_artifact(
107
+ artifact=single_artifact,
108
+ subscription=subscription,
109
+ subscription_index=subscription_index,
110
+ )
111
+
112
+ # Phase 5A: Start batch timeout checker if batch has timeout (delegate to LifecycleManager)
113
+ if subscription.batch.timeout:
114
+ await orchestrator._lifecycle_manager.start_batch_timeout_checker()
115
+
116
+ if should_flush:
117
+ break
118
+
119
+ if not should_flush:
120
+ # Batch not full yet
121
+ await orchestrator._emit_batch_item_added_event(
122
+ agent_name=agent.name,
123
+ subscription_index=subscription_index,
124
+ subscription=subscription,
125
+ artifact=artifact,
126
+ )
127
+ return CollectionResult.waiting()
128
+
129
+ # Flush batch
130
+ batched_artifacts = orchestrator._batch_engine.flush_batch(
131
+ agent.name, subscription_index
132
+ )
133
+
134
+ if batched_artifacts is None:
135
+ return CollectionResult.waiting()
136
+
137
+ artifacts = batched_artifacts
138
+
139
+ # Collection complete!
140
+ return CollectionResult.immediate(artifacts)
141
+
142
+
143
+ __all__ = ["BuiltinCollectionComponent"]
@@ -0,0 +1,78 @@
1
+ """Deduplication component to prevent duplicate artifact processing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from flock.components.orchestrator.base import OrchestratorComponent, ScheduleDecision
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from flock.core import Agent, Flock
12
+ from flock.core.artifacts import Artifact
13
+ from flock.core.subscription import Subscription
14
+
15
+
16
+ class DeduplicationComponent(OrchestratorComponent):
17
+ """Deduplication to prevent agents from processing same artifact multiple times.
18
+
19
+ Tracks which artifacts have been processed by which agents and skips
20
+ duplicate scheduling attempts.
21
+
22
+ Priority: 20 (runs after circuit breaker at 10)
23
+
24
+ Examples:
25
+ >>> # Auto-added to all orchestrators
26
+ >>> flock = Flock("openai/gpt-4.1")
27
+
28
+ >>> # Deduplication prevents:
29
+ >>> # - Agents re-processing artifacts they already handled
30
+ >>> # - Feedback loops from agent self-triggers
31
+ >>> # - Duplicate work from retry logic
32
+ """
33
+
34
+ priority: int = 20 # Run after circuit breaker (10)
35
+ name: str = "deduplication"
36
+
37
+ def __init__(self, **kwargs):
38
+ """Initialize deduplication with empty processed set."""
39
+ super().__init__(**kwargs)
40
+ self._processed: set[tuple[str, str]] = set() # (artifact_id, agent_name)
41
+
42
+ async def on_before_schedule(
43
+ self,
44
+ orchestrator: Flock,
45
+ artifact: Artifact,
46
+ agent: Agent,
47
+ subscription: Subscription,
48
+ ) -> ScheduleDecision:
49
+ """Check if artifact has already been processed by this agent.
50
+
51
+ Returns SKIP if agent has already processed this artifact,
52
+ preventing duplicate work.
53
+ """
54
+ key = (str(artifact.id), agent.name)
55
+
56
+ if key in self._processed:
57
+ # Already processed - skip
58
+ return ScheduleDecision.SKIP
59
+
60
+ # Not yet processed - allow
61
+ return ScheduleDecision.CONTINUE
62
+
63
+ async def on_before_agent_schedule(
64
+ self, orchestrator: Flock, agent: Agent, artifacts: list[Artifact]
65
+ ) -> list[Artifact]:
66
+ """Mark artifacts as processed before agent execution.
67
+
68
+ This ensures artifacts are marked as seen even if agent execution fails,
69
+ preventing infinite retry loops.
70
+ """
71
+ for artifact in artifacts:
72
+ key = (str(artifact.id), agent.name)
73
+ self._processed.add(key)
74
+
75
+ return artifacts
76
+
77
+
78
+ __all__ = ["DeduplicationComponent"]
flock/core/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ """Core abstractions and interfaces."""
2
+
3
+ from flock.core.agent import (
4
+ Agent,
5
+ AgentBuilder,
6
+ AgentOutput,
7
+ MCPServerConfig,
8
+ OutputGroup,
9
+ Pipeline,
10
+ PublishBuilder,
11
+ RunHandle,
12
+ )
13
+ from flock.core.orchestrator import BoardHandle, Flock, start_orchestrator
14
+ from flock.core.visibility import AgentIdentity
15
+
16
+
17
+ __all__ = [
18
+ "Agent",
19
+ "AgentBuilder",
20
+ "AgentIdentity",
21
+ "AgentOutput",
22
+ "BoardHandle",
23
+ "Flock",
24
+ "MCPServerConfig",
25
+ "OutputGroup",
26
+ "Pipeline",
27
+ "PublishBuilder",
28
+ "RunHandle",
29
+ "start_orchestrator",
30
+ ]